From 50b0456198ab705ccf1c525618c13b5df0428149 Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 16:51:45 +0800 Subject: [PATCH 01/69] =?UTF-8?q?=E7=BB=86=E5=8C=96=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E7=89=88=E5=AE=9E=E7=8E=B0=E9=A1=BA=E5=BA=8F=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=B8=8E=E4=BB=93=E5=BA=93=E7=9B=AE=E5=BD=95=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把第 12 节拆成可验收阶段,目录改为 isa/compiler/vm/host。 Co-authored-by: Cursor --- Doc/README.md | 2 +- Doc/初步计划.md | 273 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 261 insertions(+), 14 deletions(-) diff --git a/Doc/README.md b/Doc/README.md index 3ce586c..d833710 100644 --- a/Doc/README.md +++ b/Doc/README.md @@ -4,6 +4,6 @@ | 文件 | 内容 | |---|---| -| [`初步计划.md`](初步计划.md) | 定案:子集 ST、toml 工程、寄存器 VM、执行模型、ST 子集、实现顺序 | +| [`初步计划.md`](初步计划.md) | 定案:子集 ST、toml 工程、寄存器 VM、执行模型、ST 子集;第 12 节为分阶段实现顺序 | 仓库根目录 [`README.md`](../README.md) 只说明目录和模块边界;语言规则、映像与扫描周期的细节以本目录为准。 diff --git a/Doc/初步计划.md b/Doc/初步计划.md index 93494d5..c3472fb 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -1,4 +1,4 @@ -# 子集 ST + toml 工程 + 寄存器 VM +# StScan:子集 ST + toml 工程 + 寄存器 VM 独立方案说明。调研全文见 `PLC-DSL.md`。 @@ -269,14 +269,15 @@ RET ## 10. 目录 +仓库布局见根目录 `README.md`。实现时按四个 CMake 目标拆,样例不放仓库根: + ```text -project.toml # files / gvl.file / 可选 io -globals.st # 第一版唯一 GVL -motor.st -main.st -compiler/ # lexer parser symbols checker codegen linker -vm/ # 扫描周期、寄存器、映像、FB -host/ # 喂 I、收 Q、Δt、回放 +isa/ # 指令编码、映像头、定宽类型(compiler 与 vm 的合同) +compiler/ # lexer parser symbols checker codegen linker +vm/ # 扫描周期、寄存器、映像、FB +host/ # 喂 I、收 Q、Δt、回放 +examples/line1/ # 第 11 节最小样例(给人看) +tests/ # 冻规则用的正例 / 负例 ``` --- @@ -322,8 +323,254 @@ END_PROGRAM ## 12. 实现顺序 -1. 冻 toml 字段:`files` / `entry` / `cycle_limit` / `dt_ms` / `gvl.file` / 可选 `io.*` -2. 冻关键字表 + 约 20 个样例程序 -3. 冻约 20 条指令编码 -4. 冻符号表字段:名字、种类、类型、地址、源文件 -5. 递归下降解析 → 符号表与链接 → 最后写 VM 循环 +原则:**先冻合同,再解析,再链接,最后写 VM**。`compiler` 与 `vm` 不互链,合同只进 `isa`。每一阶段先有可运行的测试,再进入下一阶段。一人约 4~8 周做到第 11 节 `line1`。 + +第一版不做:图染色、SSA、WHILE 有界证明、异常表、闭包、优化、`REAL`、独立 `stc.exe`。 + +--- + +### 12.0 骨架(约 1 天) + +CMake 四个目标能空编译:`isa`、`compiler`、`vm`、`host`。`host` 先打印版本即可。 + +- 落下 `examples/line1/`:第 3 节 toml + 第 11 节三个 `.st`。 +- `tests/` 接上 CTest,先放一个必过的空测试。 +- 溢出策略当场选定并写进 `isa` 注释:**饱和**或**报错**,只留一种。 + +完成:`cmake --build` 过;`ctest` 能跑。 + +--- + +### 12.1 冻合同(约 2~3 天,先于任何解析器) + +四张表写进代码或 `Doc/`,改表要同步改测试。未冻完不要写递归下降。 + +**toml 字段(只这些)** + +| 字段 | 含义 | 缺省 / 约束 | +|---|---|---| +| `project.name` | 工程名 | 必填 | +| `project.entry` | 入口 | 第一版必须是 `program MAIN` | +| `project.cycle_limit` | 每周期指令上限 | 如 `100000`;超时是故障 | +| `project.dt_ms` | 本周期 Δt | 给 TON / TOF / CTU,不是 wall clock | +| `files.st` | 源文件列表 | 编译前全部已知 | +| `gvl.file` | 唯一允许 `VAR_GLOBAL` 的文件 | 已在 `files.st` 则不重复收录 | +| `[[io.input]]` / `[[io.output]]` | `var` + `channel` + `bit` | 可选;`var` 必须已在 GVL | + +禁止 toml 里用 `[[globals]]` 再声明变量。 + +**关键字(第一版只认这些)** + +`PROGRAM` `FUNCTION` `FUNCTION_BLOCK` `END_PROGRAM` `END_FUNCTION` `END_FUNCTION_BLOCK` +`VAR` `VAR_INPUT` `VAR_OUTPUT` `VAR_GLOBAL` `VAR_EXTERNAL` `END_VAR` +`BOOL` `INT` `TIME` +`IF` `ELSIF` `ELSE` `END_IF` `WHILE` `END_WHILE` +`AND` `OR` `NOT` +`TRUE` `FALSE` +`TON` `TOF` `CTU` + +其余一律当标识符或直接拒绝(`VAR_IN_OUT`、`REF`、`CLASS` 等出明确错误)。 + +**符号表一行** + +`name` / `kind` / `type` / `address` / `source_file` +`kind` 至少:`global` / `external` / `local` / `input` / `output` / `pou` / `fb_instance` / `const`。 + +**指令(进 `isa`,约 20 条)** + +第 7 节那张表:编码宽度、立即数格式、`fn_id` 是立即数、跳转是相对 offset。 +映像头:函数表、常量表、全局槽数、FB 槽布局、`cycle_limit`、`dt_ms`、工程哈希。 +工程哈希:收录的源文件排序后算,写入映像,回放时槽位必须一致。 + +完成:`isa` 能编解码一条指令;头文件即规范,不再另写一份互相打架的文档。 + +--- + +### 12.2 约 20 个样例(与 12.1 同时列出,随后逐个点亮) + +每个用例一个子目录(`project.toml` + `.st`)。先写文件和期望(通过 / 报哪类错),再实现。`examples/line1` 给人看;负例和切片放 `tests/`。 + +| # | 用例 | 期望 | +|---|---|---| +| 1 | 空 `PROGRAM MAIN` + `RET` | 通过 | +| 2 | `BOOL` 赋值、`TRUE`/`FALSE` | 通过 | +| 3 | `AND`/`OR` 短路(右侧有副作用时不执行) | 通过 | +| 4 | `IF` / `ELSIF` / `ELSE` | 通过 | +| 5 | `WHILE` 正常结束 | 通过 | +| 6 | `WHILE` 打满 `cycle_limit` | 运行时故障 | +| 7 | `INT` 四则 + 比较 | 通过 | +| 8 | `TIME` 字面量 | 通过 | +| 9 | GVL + `VAR_EXTERNAL` | 通过,同一槽 | +| 10 | 非 GVL 文件写 `VAR_GLOBAL` | 编译错误 | +| 11 | 同名 `VAR_GLOBAL` | 链接失败 | +| 12 | `io.var` 未在 GVL | 编译错误 | +| 13 | `FUNCTION` 纯计算 + `CALL` | 通过 | +| 14 | `FUNCTION` 写全局 | 编译错误 | +| 15 | FB 实例 + `fb.Q` + 命名输入 | 通过 | +| 16 | 未声明实例就调 FB | 编译错误 | +| 17 | `TON` + 固定 Δt | 到时 `Q` 为真 | +| 18 | `TOF` / `CTU` | 行为与 Δt 一致 | +| 19 | 函数体循环调用 / 无界递归 | 编译错误 | +| 20 | `line1`(第 11 节) | I 组合 → `Q0_0` 与急停语义一致 | + +完成:目录和期望表齐;此时大部分还编不过,属正常。 + +--- + +### 12.3 toml 读取(`compiler`,约 2 天) + +还不碰 `.st` 正文。 + +- 解析第 3 节字段;未知键报错。 +- 拼出编译文件集合:`files.st` ∪ `gvl.file`,去重、记绝对/相对路径。 +- 校验:缺文件、`entry` 不是 `program MAIN`、`io` 缺 `var`/`channel`/`bit`。 +- 算出工程哈希(文件集合排序后);先对源文件算,映像还没有也没关系。 + +完成:对 `examples/line1/project.toml` 打出文件列表和哈希;缺文件的负例过。 + +--- + +### 12.4 词法(`compiler`,约 2 天) + +- `(* *)` 注释、标识符、关键字、整数、`TIME` 字面量、`:=`、比较符、括号。 +- 关键字与标识符分词;大小写规则写死(建议与 IEC 一样不敏感,实现时统一折成一种内部形)。 +- 每个 token 带 `source_file` + 行列,供以后报错。 + +完成:把第 11 节三个文件 lex 成稳定 token 流;非法字符报错。 + +--- + +### 12.5 递归下降语法(`compiler`,约 4~6 天) + +只出 AST,不分配寄存器、不写映像。 + +顺序: + +1. POU 外壳:`PROGRAM` / `FUNCTION` / `FUNCTION_BLOCK` 与对应 `END_*` +2. 变量段:`VAR` / `VAR_INPUT` / `VAR_OUTPUT` / `VAR_GLOBAL` / `VAR_EXTERNAL` +3. 类型名只接受 `BOOL` `INT` `TIME`;内置 FB 名 `TON` `TOF` `CTU` +4. 语句:赋值、`IF`、`WHILE`、FB 调用 `fb(in := …)`、字段 `fb.Q` +5. 表达式:`NOT`、`AND`/`OR`(AST 里保留短路语义)、比较、加减乘除 + +拒绝时给明确错误,不要 silently 忽略:`VAR_IN_OUT`、指针/`REF`、`CLASS`、`ANY`、用户类型套娃。 + +完成:`line1` 三个文件能 parse;用例 10、14、19 若已能在语法/结构层拒绝则先红后绿。 + +--- + +### 12.6 符号表与链接(`compiler`,约 3~4 天) + +```text +收集导出 → 校验 GVL → 解析 VAR_EXTERNAL → 定地址 → 校验 io.* +``` + +- 先扫完全部 POU 再解析引用,禁止边解析边执行。 +- 仅 `gvl.file` 可出现 `VAR_GLOBAL`;声明顺序 = 全局槽顺序。 +- `VAR_EXTERNAL` 接已有槽,种类/类型必须一致,不是新变量。 +- `FUNCTION` 标成无状态;`FUNCTION_BLOCK` 必须先有实例才能调用。 +- `CALL` 目标解析成 `fn_id`(立即数),无函数指针。 +- 同名全局、未声明 `io.var`、重复 POU 名 → 失败。 +- FB 实例:按类型算出固定布局(输入/输出/内部/内置 TON 等字段偏移)。 + +完成:用例 9~12、16 按表变绿;符号表能打印 `name kind type address file`。 + +--- + +### 12.7 类型检查(`compiler`,约 2 天) + +- `:=`、比较、算术的类型规则;`AND`/`OR`/`NOT` 只吃 `BOOL`。 +- `FUNCTION` 禁止写全局(含经 `VAR_EXTERNAL`)。 +- FB 调用:命名输入类型匹配;`fb.Q` 等字段存在且类型对。 +- 第一版无隐式宽化;`INT` 与 `TIME` 不混用。 + +完成:类型错误有稳定报文;合法 `line1` AST 带类型。 + +--- + +### 12.8 寄存器码(`compiler`,约 5~7 天) + +局部和 `VAR` 固定寄存器;表达式临时值往后编号;`nregs` 写进函数头。跳转先留洞,再回填。`AND`/`OR` **必须编成跳转**,禁止两边都算。 + +建议按指令面切片,每片都能 `isa` 编解码、必要时用临时 VM 桩执行: + +1. `MOVE` `LOADK` `RET`,空 `MAIN` 出映像(对应用例 1~2) +2. `LOAD_I` `STORE_Q` `LOAD_GLOBAL` `STORE_GLOBAL` `LOAD_M` `STORE_M` +3. `NOT`,短路 `AND`/`OR`(用例 3) +4. `CMP_*` `JMP` `JT`/`JF` → `IF`(用例 4) +5. `WHILE` + 映像里带上 `cycle_limit`(用例 5;超限等 VM) +6. `ADD`/`SUB`/`MUL`/`DIV`(用例 7~8) +7. `CALL fn_id` + 有界帧(用例 13) +8. FB 实例块 + 字段偏移;`CAL_TON` / `CAL_TOF` / `CAL_CTU`(用例 15、17、18) + +完成:`line1` 能写出映像(函数字节码 + 全局/FB 槽 + 工程哈希);跳转回填无悬空。 + +--- + +### 12.9 VM 循环(`vm`,约 3~4 天) + +此时才写执行循环。只认 `isa` 映像。 + +```text +采样 I → pc=0 执行到 RET → 写回 Q → 用本周期 Δt 推进 TON/TOF/CTU +``` + +- 译码 + 大 `switch`;寄存器文件按函数头 `nregs` 分配。 +- 每周期计指令数,超过 `cycle_limit` 报故障(用例 6)。 +- 调用栈有界;`CALL`/`RET` 恢复 `pc` 与帧。 +- 无 GC、无堆、无线程。同一映像 + 同一组 `I` + 同一 `Δt` → `Q`/`M`/FB 一致。 +- 内置 FB:`CAL_TON`/`CAL_TOF`/`CAL_CTU` 只用映像里的 Δt,不读系统时钟。 + +完成:对手工拼的小映像能跑赋值和跳转;再接编译器产出的映像。 + +--- + +### 12.10 host(约 2~3 天) + +一条命令:读 toml → `compiler` → `vm` 跑 N 个周期。不拆第二个可执行文件。 + +- 按 `[[io.*]]` 从宿主缓冲采样 / 写回,不创造变量。 +- 把 `dt_ms` 每周期传给 VM。 +- 最小可观测:打印本周期 I/Q,或把寄存器/映像 dump 出来。 +- 回放:读预先录制的 I 序列(文本即可),不接真实硬件。 + +完成:`host examples/line1/project.toml` 能编译并跑若干周期。 + +--- + +### 12.11 闭环与冻测试(约 1 周) + +- 点亮第 12.2 节 20 个用例;负例检查报错类别,不查英文原文。 +- `line1` 固定输入表:急停、start/stop 组合 → 期望 `Q0_0`。 +- 回放:同一 I 序列跑两遍,槽位与哈希一致。 +- 单步:能停在指令边界,看到寄存器和 I/Q/M(可先做打印,不必做 UI)。 + +完成标准(第一版可交付):**`MAIN` + 一个 Motor FB + I/O + 全局**,扫描周期可重复。 + +--- + +### 12.12 建议日历 + +| 周 | 做完 | +|---|---| +| 1 | 12.0~12.4:骨架、合同、样例清单、toml、词法 | +| 2 | 12.5:AST,能 parse `line1`,拒绝多余特性 | +| 3 | 12.6~12.7:链接、定址、类型、io 校验 | +| 4 | 12.8 前半 + 12.9 前半:赋值/短路/IF 出映像,VM 能跑 | +| 5 | 12.8 后半 + 12.10:CALL、FB、host 跑起来 | +| 6 | 定时器/计数器、io、Δt、`line1` 语义 | +| 7~8 | 20 用例、回放、单步、补漏 | + +周 4 结束时就应能跑一个「读 I、写 Q」的假 MAIN。不要等 FB 和 TON 齐了才第一次执行。 + +--- + +### 12.13 依赖方向(全程遵守) + +```text +host → compiler → isa +host → vm → isa +``` + +- 指令或映像布局有变:先改 `isa` 和编解码测试,再改两端。 +- 新 ST 特性:先加 12.2 的用例行,再改关键字 / AST / 检查 / 码 / VM。 +- 不要为了一个 pass 新建 CMake 库。 From 89f99e65e8fd55a4a6338788d674b6dfcd0d009f Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 16:51:51 +0800 Subject: [PATCH 02/69] =?UTF-8?q?=E5=BB=BA=E7=AB=8B=20StScan=20CMake=20?= =?UTF-8?q?=E5=B7=A5=E7=A8=8B=EF=BC=8C=E5=B9=B6=E5=9C=A8=20cmake=20?= =?UTF-8?q?=E4=B8=AD=E8=A7=84=E5=AE=9A=20C11/C++11=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 顶层挂上四个模块目录,语言标准集中在 Standards.cmake。 Co-authored-by: Cursor --- CMakeLists.txt | 15 +++++++++++++++ README.md | 4 ++-- cmake/README.md | 8 +++++--- cmake/Standards.cmake | 8 ++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 cmake/Standards.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index e69de29..e5b9b80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -0,0 +1,15 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(StScan + VERSION 0.1 + DESCRIPTION "子集 ST 编译为寄存器 VM,按扫描周期执行" + LANGUAGES C CXX) + +include("${CMAKE_SOURCE_DIR}/cmake/Standards.cmake") + +add_subdirectory(isa) +add_subdirectory(compiler) +add_subdirectory(vm) +add_subdirectory(host) \ No newline at end of file diff --git a/README.md b/README.md index ce3f380..9debe64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 子集 ST + 寄存器 VM +# StScan -IEC 风格的子集 ST:用 `project.toml` 组织多个 `.st`,编译成单一映像,由寄存器虚拟机按扫描周期执行。 +子集 ST + 扫描周期虚拟机。IEC 风格的子集 ST:用 `project.toml` 组织多个 `.st`,编译成单一映像,由寄存器虚拟机按扫描周期执行。 方案见 [`Doc/初步计划.md`](Doc/初步计划.md)。 diff --git a/cmake/README.md b/cmake/README.md index 4606336..a9e6efd 100644 --- a/cmake/README.md +++ b/cmake/README.md @@ -1,7 +1,9 @@ # cmake -顶层 CMake 的辅助脚本,**不是**业务模块。 +顶层 CMake 的辅助脚本,**不是**业务模块。由根目录 `CMakeLists.txt` 在 `project()` 之后 `include()`。 -放警告选项、C++ 标准、输出目录等共用片段,由根目录 `CMakeLists.txt` `include()`。不要按词法、VM、host 再拆成业务 CMake module。 +| 文件 | 内容 | +|---|---| +| `Standards.cmake` | C11、C++11;`STANDARD_REQUIRED`;关闭 GNU 扩展 | -第一版尽量少文件;没有共用逻辑时本目录可以为空。 +不要按词法、VM、host 再拆成业务 CMake module。改语言标准只改本目录,不要在各子目录再 `set(CMAKE_CXX_STANDARD …)`。 diff --git a/cmake/Standards.cmake b/cmake/Standards.cmake new file mode 100644 index 0000000..e4dcdd9 --- /dev/null +++ b/cmake/Standards.cmake @@ -0,0 +1,8 @@ +# C / C++ 语言标准。在 project() 之后 include,对后续目标生效。 +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) From 74e5f43da96349a8e92c8b7b29923b2114034ee3 Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 17:17:52 +0800 Subject: [PATCH 03/69] =?UTF-8?q?=E5=B0=86=20CMake=20=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E5=90=8D=E4=BB=8E=20StScan=20=E6=94=B9=E4=B8=BA=20Stator?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5b9b80..b263b91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21) # 工程名、版本号、工程描述 -project(StScan +project(Stator VERSION 0.1 DESCRIPTION "子集 ST 编译为寄存器 VM,按扫描周期执行" LANGUAGES C CXX) From 583f14967a374a40ac8c04ed0db4872442cfe3c9 Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 17:18:30 +0800 Subject: [PATCH 04/69] =?UTF-8?q?=E5=86=BB=20isa=20=E7=9A=84=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E7=BC=96=E7=A0=81=E3=80=81=E6=98=A0=E5=83=8F=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=B8=8E=E5=AE=9A=E5=AE=BD=E7=B1=BB=E5=9E=8B=EF=BC=8C?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E6=94=B9=E4=B8=BA=E5=BC=95=E7=94=A8=E8=AF=A5?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Doc/isa/指令与映像.md | 160 ++++++++++++++++++++++++++++++++++++++++++ Doc/初步计划.md | 8 +-- isa/CMakeLists.txt | 0 isa/README.md | 16 +---- 4 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 Doc/isa/指令与映像.md create mode 100644 isa/CMakeLists.txt diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md new file mode 100644 index 0000000..c4aed38 --- /dev/null +++ b/Doc/isa/指令与映像.md @@ -0,0 +1,160 @@ +# 指令与映像 + +isa 模块规范:指令集 + 映像 + 定宽类型。不是编译器,不把 `.st` 编成映像。 + +`compiler` 按本文件写出字节,`vm` 只认这些字节。两边不互链。 + +全工程定案见 [`初步计划.md`](../初步计划.md)。**类型、指令编码、映像布局以本文为准**,不要另写一份互相打架的表。头文件实现本文,不另当规范。 + +```text +host → compiler → isa +host → vm → isa +``` + +CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/`。 + +--- + +## 做 / 不做 + +**做** + +- 操作码与 32 位指令布局 +- 映像头与段(函数字节码、全局 / FB 槽、工程哈希) +- `BOOL` / `INT` / `TIME` 宽度与溢出规则 +- 编解码、工程哈希、一条指令的文本转储(给 host 打印) + +**不做** + +- toml 字段、关键字表、符号表、AST +- 扫描周期循环、I/O 采样 +- 短路语义(`AND`/`OR` 指令只组合已算好的值;短路由 `compiler` 编成跳转) + +指令表可作为内部 IR 的文本形态,不必当主语言。 + +--- + +## 特性 + +| 项 | 第一版 | +|---|---| +| `BOOL` | `uint8_t`,只允许 0/1 | +| `INT` | `int16_t`(IEC `INT`) | +| `TIME` | `int32_t` 毫秒;与 `INT` 不混用 | +| 整数溢出 | **饱和**(夹到 `INT` 最小/最大) | +| 字节序 | 小端 | +| 指令宽度 | 32 bit,一种主布局 | +| 寄存器 | 下标 0..255;每函数实际个数写在函数头 `nregs` | +| 跳转 | 相对 offset,单位是**指令条数**,不是字节 | +| `CALL` | 立即数 `fn_id`(u16),无函数指针 | +| 映像魔数 | `STSC`,版本 `1` | +| 工程哈希 | 源文件路径排序后对内容做 FNV-1a 64(非密码学) | +| I/O 绑定 | **不进映像**;编译期把 `var` 收成槽号,channel/bit 留在 host 读 toml | +| 不做 | `REAL`、隐式宽化、独立 `stc.exe` | + +--- + +## 类型 + +C++11 定宽: + +- `BOOL`:`uint8_t`,0/1 +- `INT`:`int16_t`;加减乘除溢出饱和 +- `TIME`:`int32_t` 毫秒 + +`compiler` 与 `vm` 都按同一规则,不再另选溢出策略。 + +--- + +## 指令 + +定宽 32 bit: + +```text +[ op:8 | rd:8 | a:8 | b:8 ] +``` + +- 三寄存器:`a`/`b` 为 `ra`/`rb`(`MOVE`/`NOT` 只用 `rd`+`a`) +- `LOADK`:`a|b` 为 `const_id`(u16) +- `JMP` / `JT` / `JF`:`a|b` 为有符号相对 offset(指令条数) +- `CALL`:`a|b` 为 `fn_id`(u16) + +操作码一次列全(`CMP_xx` 冻下面六种): + +```text +MOVE rd, rs +LOADK rd, const_id +NOT rd, rs +AND/OR rd, ra, rb (* 已算好的值;短路用跳转 *) +ADD/SUB/MUL/DIV rd, ra, rb +CMP_EQ/NE/LT/LE/GT/GE rd, ra, rb +JMP offset +JT/JF r, offset +LOAD_I / STORE_Q / LOAD_M / STORE_M +LOAD_GLOBAL / STORE_GLOBAL +CAL_TON / CAL_TOF / CAL_CTU +CALL fn_id +RET +``` + +局部 / `VAR` = 固定寄存器;表达式临时值往后编号;`nregs` 写进函数头。 +`I`/`Q`/`M` 走映像指令,不是通用寄存器。 +FB 实例固定布局,字段偏移编译期算死。 + +--- + +## 映像 + +魔数 `STSC`,版本 `1`。 + +头: + +- `cycle_limit`、`dt_ms` +- 工程哈希 +- 全局槽数、I/Q/M 槽数 +- 常量表 / 函数表 / 字节码 / FB 布局的偏移 +- 入口 `MAIN` 的 `fn_id` + +函数表一行:`fn_id`、`nregs`、字节码偏移与长度。 + +工程哈希:收录源文件路径排序后对内容做 **FNV-1a 64**,只保证回放槽位一致,不是密码学哈希。 + +`[[io.input]]` / `[[io.output]]` 不进映像。 + +--- + +## 目录与 CMake + +```text +isa/ + CMakeLists.txt + README.md # 代码目录入口,指向本文 + include/isa/ + Types.hpp # 宽度、饱和、槽值表示 + Op.hpp # 操作码枚举 + 助记符 + Instr.hpp # 32-bit 打包/拆字段(inline) + Image.hpp # 头、段、函数表/常量表/FB 布局 + Encode.hpp # 编解码、哈希、反汇编一行 + src/ + Encode.cpp + Image.cpp +``` + +```cmake +add_library(isa STATIC src/Encode.cpp src/Image.cpp) +target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +``` + +`compiler` / `vm`:`target_link_libraries(... PUBLIC isa)`。 + +--- + +## 实现顺序(对齐初步计划 12.0~12.1) + +1. `Types.hpp`:三类型 + `sat_add` 等饱和函数(inline 即可) +2. `Op.hpp` + `Instr.hpp`:枚举与 32-bit 编解码 +3. `Encode.cpp`:`encode` / `decode` / `disasm` 一条指令 +4. `Image.hpp` + `Image.cpp`:头布局、FNV-1a、读写缓冲(先能写空映像 + 一条 `RET`) +5. 测试随后在 `tests/` 加编解码往返;本阶段以头文件和库能链为准 + +不在本模块做:VM `switch`、toml、关键字、符号表。 diff --git a/Doc/初步计划.md b/Doc/初步计划.md index c3472fb..eb099b0 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -202,6 +202,8 @@ motor.st / main.st ## 7. 寄存器机与指令 +编码宽度、操作码取值、映像头与定宽类型以 [`isa/指令与映像.md`](isa/指令与映像.md) 为准。本节只列助记符。 + - 局部 / `VAR` = 固定寄存器;表达式临时值往后编号;`nregs` 写进函数头。 - `I/Q/M` 走映像指令,不是通用寄存器。 - 每个 FB 实例一块固定布局,字段偏移编译期算死。 @@ -378,11 +380,9 @@ CMake 四个目标能空编译:`isa`、`compiler`、`vm`、`host`。`host` 先 **指令(进 `isa`,约 20 条)** -第 7 节那张表:编码宽度、立即数格式、`fn_id` 是立即数、跳转是相对 offset。 -映像头:函数表、常量表、全局槽数、FB 槽布局、`cycle_limit`、`dt_ms`、工程哈希。 -工程哈希:收录的源文件排序后算,写入映像,回放时槽位必须一致。 +布局、操作码、映像、哈希、饱和以 [`isa/指令与映像.md`](isa/指令与映像.md) 为准。第 7 节只列助记符。 -完成:`isa` 能编解码一条指令;头文件即规范,不再另写一份互相打架的文档。 +完成:`isa` 能编解码一条指令;实现头文件,不另写第三份规范。 --- diff --git a/isa/CMakeLists.txt b/isa/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/isa/README.md b/isa/README.md index f0166a5..15b1948 100644 --- a/isa/README.md +++ b/isa/README.md @@ -1,17 +1,7 @@ # isa -共享契约库。CMake 目标:`isa`(`STATIC` 或 `INTERFACE`),无依赖。 +指令集 + 映像 + 定宽类型。CMake 目标:`isa`(`STATIC`)。 -`compiler` 写映像、`vm` 读映像,两边只依赖本目录,彼此不链接。 +`compiler` 写映像、`vm` 读映像,两边只依赖本目录。 -## 职责 - -- 约 20 条寄存器指令的编码(`MOVE`、`LOADK`、跳转、`LOAD_I` / `STORE_Q`、`CALL` / `RET`、`CAL_TON` 等) -- 映像头:函数字节码、全局 / FB 数据槽、工程哈希 -- 定宽类型:第一版 `BOOL`、`INT`、`TIME` - -## 不放什么 - -解析器、符号表、扫描周期循环、I/O 采样。那些分别属于 `compiler`、`vm`、`host`。 - -指令表可作为内部 IR 的文本形态,不必当主语言。 +规范、特性与实现顺序见 [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md)。 From e4cdaf4717c77d91c1a93d5f3d3bc2098315e7c0 Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 17:18:40 +0800 Subject: [PATCH 05/69] =?UTF-8?q?=E6=8A=8A=E5=90=84=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E4=BB=8E=20README=20=E6=8B=86=E5=88=B0=20Doc?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E7=94=A8=E7=B4=A2=E5=BC=95=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=20Doc/README=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Doc/README.md | 9 --------- Doc/compiler/编译管线.md | 26 ++++++++++++++++++++++++++ Doc/host/宿主入口.md | 18 ++++++++++++++++++ Doc/vm/扫描周期.md | 22 ++++++++++++++++++++++ Doc/索引.md | 23 +++++++++++++++++++++++ README.md | 2 +- compiler/README.md | 25 +------------------------ host/README.md | 13 +------------ vm/README.md | 21 +-------------------- 9 files changed, 93 insertions(+), 66 deletions(-) delete mode 100644 Doc/README.md create mode 100644 Doc/compiler/编译管线.md create mode 100644 Doc/host/宿主入口.md create mode 100644 Doc/vm/扫描周期.md create mode 100644 Doc/索引.md diff --git a/Doc/README.md b/Doc/README.md deleted file mode 100644 index d833710..0000000 --- a/Doc/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Doc - -设计文档,不是编译目标。 - -| 文件 | 内容 | -|---|---| -| [`初步计划.md`](初步计划.md) | 定案:子集 ST、toml 工程、寄存器 VM、执行模型、ST 子集;第 12 节为分阶段实现顺序 | - -仓库根目录 [`README.md`](../README.md) 只说明目录和模块边界;语言规则、映像与扫描周期的细节以本目录为准。 diff --git a/Doc/compiler/编译管线.md b/Doc/compiler/编译管线.md new file mode 100644 index 0000000..cd0c370 --- /dev/null +++ b/Doc/compiler/编译管线.md @@ -0,0 +1,26 @@ +# 编译管线 + +compiler 模块:ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 [`指令与映像.md`](../isa/指令与映像.md)。 + +读齐工程后走直通 pass,产出单一映像,不上 LLVM。词法、语法、符号表、检查、codegen、链接都留在本库,不再拆 CMake 子库。 + +语言规则、toml 字段、链接与第 12 节阶段见 [`初步计划.md`](../初步计划.md)。指令和映像只认 [`指令与映像.md`](../isa/指令与映像.md)。 + +## 管线 + +```text +读 toml、收齐 .st + → 词法 / 递归下降语法 + → 收集导出符号 + → 解析 VAR_EXTERNAL、链接、定地址 + → 类型检查 + → 按 PROGRAM / FUNCTION / FB 编寄存器码 + → 回填跳转、写映像头与工程哈希 +``` + +## 边界 + +- toml **不声明变量**;`VAR_GLOBAL` 第一版只允许出现在 `gvl.file` +- `[[io.*]]` 的 `var` 必须已在 GVL 中;同名全局链接失败 +- 第三方 toml 解析放 `third_party/`,本库私有链接 +- 不写扫描周期,不链 `vm` diff --git a/Doc/host/宿主入口.md b/Doc/host/宿主入口.md new file mode 100644 index 0000000..4dea1f9 --- /dev/null +++ b/Doc/host/宿主入口.md @@ -0,0 +1,18 @@ +# 宿主入口 + +host 模块:可执行入口。CMake 目标:`host`(`EXECUTABLE`),链接 `compiler` 和 `vm`。 + +第一版一条命令:读工程 → 编译映像 → 按扫描周期跑。不拆独立的编译器可执行文件。总顺序见 [`初步计划.md`](../初步计划.md) 第 12.10 节。 + +## 职责 + +- 读 `project.toml` 和其中的 `.st`,交给 `compiler` +- 按 `[[io.input]]` / `[[io.output]]` 采样 I、写回 Q(不创造变量) +- 把 `dt_ms` 交给 VM,推进 `TON` / `TOF` / `CTU` +- 单步、看寄存器和映像、按录制的 `I` 回放 + +## 边界 + +- 指令编码见 [`指令与映像.md`](../isa/指令与映像.md) +- ST 解析与链接见 [`编译管线.md`](../compiler/编译管线.md) +- 寄存器执行循环见 [`扫描周期.md`](../vm/扫描周期.md) diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md new file mode 100644 index 0000000..16d789b --- /dev/null +++ b/Doc/vm/扫描周期.md @@ -0,0 +1,22 @@ +# 扫描周期 + +vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [`指令与映像.md`](../isa/指令与映像.md)。 + +运行时是 **一台 VM、一个入口、一份映像**。不链接 `compiler`。扫描周期与确定性见 [`初步计划.md`](../初步计划.md) 第 2、8 节。 + +## 扫描周期 + +```text +1. 按 toml 的 io 绑定,采样 → I / 输入全局 +2. 从 PROGRAM MAIN 的 pc=0 执行到 RET +3. 写回 Q / 输出全局 +4. 用本周期 Δt 推进 TON / TOF / CTU(不用 wall clock) +``` + +## 边界 + +- 只认 `isa` 映像;类型与溢出按 [`指令与映像.md`](../isa/指令与映像.md)(`INT` 饱和) +- 局部 / `VAR` = 固定寄存器;`nregs` 在函数头 +- `CALL` 目标是立即数 `fn_id`,有界调用栈 +- 无 GC、无堆、无线程;每周期受 `cycle_limit` 限制 +- I/O 从哪来、Δt 谁给、回放怎么喂,属于 `host` diff --git a/Doc/索引.md b/Doc/索引.md new file mode 100644 index 0000000..72ac6f6 --- /dev/null +++ b/Doc/索引.md @@ -0,0 +1,23 @@ +# 文档索引 + +设计文档,不是编译目标。全工程定案在根下;各 CMake 模块一个文件夹,文件名标明用途。 + +```text +doc/ + 索引.md + 初步计划.md + isa/指令与映像.md + compiler/编译管线.md + vm/扫描周期.md + host/宿主入口.md +``` + +| 路径 | 内容 | +|---|---| +| [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | +| [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | +| [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | +| [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | +| [`host/宿主入口.md`](host/宿主入口.md) | 可执行入口:编译 + 跑周期 | + +代码目录里的 README 只作入口;类型、指令、映像以 `isa/指令与映像.md` 为准。 diff --git a/README.md b/README.md index 9debe64..ccdb042 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 子集 ST + 扫描周期虚拟机。IEC 风格的子集 ST:用 `project.toml` 组织多个 `.st`,编译成单一映像,由寄存器虚拟机按扫描周期执行。 -方案见 [`Doc/初步计划.md`](Doc/初步计划.md)。 +方案见 [`Doc/初步计划.md`](Doc/初步计划.md),模块文档见 [`Doc/索引.md`](Doc/索引.md)。 ```text 多个 .st diff --git a/compiler/README.md b/compiler/README.md index 239d95a..eebf619 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -2,27 +2,4 @@ ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 `isa`。 -读齐工程后走直通 pass,产出单一映像,不上 LLVM。词法、语法、符号表、检查、codegen、链接都留在本库,不再拆 CMake 子库。 - -## 管线 - -```text -读 toml、收齐 .st - → 词法 / 递归下降语法 - → 收集导出符号 - → 解析 VAR_EXTERNAL、链接、定地址 - → 类型检查 - → 按 PROGRAM / FUNCTION / FB 编寄存器码 - → 回填跳转、写映像头与工程哈希 -``` - -## 工程文件 - -toml **不声明变量**,只做三件事:列出源文件、指定唯一 GVL、(可选)把已声明的全局接到硬件。 - -- 字段:`files` / `entry` / `cycle_limit` / `dt_ms` / `gvl.file` / 可选 `io.*` -- 第一版只有 `gvl.file` 允许出现 `VAR_GLOBAL` -- `[[io.*]]` 的 `var` 必须已在 GVL 中声明,否则编译错误 -- 同名 `VAR_GLOBAL` 链接失败,禁止覆盖 - -第三方 toml 解析放在 `third_party/`,本库私有链接。 +边界与管线见 [`doc/compiler/编译管线.md`](../doc/compiler/编译管线.md)。语言规则见 [`doc/初步计划.md`](../doc/初步计划.md)。 diff --git a/host/README.md b/host/README.md index 17e7c9c..8ffe590 100644 --- a/host/README.md +++ b/host/README.md @@ -2,15 +2,4 @@ 可执行入口。CMake 目标:`host`(`EXECUTABLE`),链接 `compiler` 和 `vm`。 -第一版一条命令:读工程 → 编译映像 → 按扫描周期跑。不拆独立的编译器可执行文件。 - -## 职责 - -- 读 `project.toml` 和其中的 `.st`,交给 `compiler` -- 按 `[[io.input]]` / `[[io.output]]` 采样 I、写回 Q -- 把 `dt_ms` 交给 VM,推进 `TON` / `TOF` / `CTU` -- 单步、看寄存器和映像、按录制的 `I` 回放 - -## 不放什么 - -指令编码(`isa`)、ST 解析与链接(`compiler`)、寄存器执行循环(`vm`)。 +职责与边界见 [`doc/host/宿主入口.md`](../doc/host/宿主入口.md)。 diff --git a/vm/README.md b/vm/README.md index feba254..ec97cc3 100644 --- a/vm/README.md +++ b/vm/README.md @@ -2,23 +2,4 @@ 寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 `isa`。 -运行时是 **一台 VM、一个入口、一份映像**。不链接 `compiler`。 - -## 扫描周期 - -```text -1. 按 toml 的 io 绑定,采样 → I / 输入全局 -2. 从 PROGRAM MAIN 的 pc=0 执行到 RET -3. 写回 Q / 输出全局 -4. 用本周期 Δt 推进 TON / TOF / CTU(不用 wall clock) -``` - -## 约束 - -- 局部 / `VAR` = 固定寄存器;`nregs` 写在函数头 -- `I` / `Q` / `M` 走映像指令,不是通用寄存器 -- FB 实例固定布局,字段偏移编译期算死 -- `CALL` 目标是立即数 `fn_id`,有界调用栈 -- 无 GC、无堆、无线程;每周期受 `cycle_limit` 限制 - -I/O 从哪来、Δt 谁给、回放怎么喂,属于 `host`。 +扫描周期与边界见 [`doc/vm/扫描周期.md`](../doc/vm/扫描周期.md)。 From 6db73042545b6aa61949a137e886f55b177e8bb1 Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 17:48:03 +0800 Subject: [PATCH 06/69] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20GCC=20CMake=20preset?= =?UTF-8?q?s=EF=BC=8C=E9=A1=B6=E5=B1=82=E5=85=88=E5=8F=AA=E7=BC=96=20isa?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compiler/vm/host 尚未成库,避免 configure 失败。 Co-authored-by: Cursor --- CMakeLists.txt | 6 +++--- CMakePresets.json | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 CMakePresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index b263b91..cfd9226 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,6 @@ project(Stator include("${CMAKE_SOURCE_DIR}/cmake/Standards.cmake") add_subdirectory(isa) -add_subdirectory(compiler) -add_subdirectory(vm) -add_subdirectory(host) \ No newline at end of file +# add_subdirectory(compiler) +# add_subdirectory(vm) +# add_subdirectory(host) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..e150da9 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,48 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 21, + "patch": 0 + }, + "configurePresets": [ + { + "name": "gcc", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "gcc-debug", + "displayName": "GCC Debug", + "description": "使用 PATH 中的 gcc / g++,Debug", + "inherits": "gcc", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "gcc-release", + "displayName": "GCC Release", + "description": "使用 PATH 中的 gcc / g++,Release", + "inherits": "gcc", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "gcc-debug", + "configurePreset": "gcc-debug" + }, + { + "name": "gcc-release", + "configurePreset": "gcc-release" + } + ] +} From ff244888c356b1386c862672a7f7bbb2e8700dc0 Mon Sep 17 00:00:00 2001 From: chentianya Date: Tue, 18 Aug 2026 17:48:09 +0800 Subject: [PATCH 07/69] =?UTF-8?q?=E8=90=BD=E5=9C=B0=20isa=20=E9=9D=99?= =?UTF-8?q?=E6=80=81=E5=BA=93=E9=AA=A8=E6=9E=B6=E4=B8=8E=20Types=20?= =?UTF-8?q?=E5=A4=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 先挂 Encode.cpp 和定宽类型别名,规范仍以 Doc 为准。 Co-authored-by: Cursor --- isa/CMakeLists.txt | 16 +++++ isa/README.md | 153 +++++++++++++++++++++++++++++++++++++++- isa/include/isa/Types.h | 18 +++++ isa/src/Encode.cpp | 0 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 isa/include/isa/Types.h create mode 100644 isa/src/Encode.cpp diff --git a/isa/CMakeLists.txt b/isa/CMakeLists.txt index e69de29..22cae7c 100644 --- a/isa/CMakeLists.txt +++ b/isa/CMakeLists.txt @@ -0,0 +1,16 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(ISA + VERSION 0.1 + DESCRIPTION "指令集架构") + +# C/C++ 标准版本 +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 11) + +include_directories(./include) + +add_library(isa STATIC + ./src/Encode.cpp) \ No newline at end of file diff --git a/isa/README.md b/isa/README.md index 15b1948..d06cf95 100644 --- a/isa/README.md +++ b/isa/README.md @@ -1,7 +1,154 @@ # isa -指令集 + 映像 + 定宽类型。CMake 目标:`isa`(`STATIC`)。 +指令集 + 映像 + 定宽类型。CMake 目标:`isa`(`STATIC`),无依赖。 -`compiler` 写映像、`vm` 读映像,两边只依赖本目录。 +`compiler` 写映像、`vm` 读映像,两边只依赖本目录,不互链。 -规范、特性与实现顺序见 [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md)。 +```text +host → compiler → isa +host → vm → isa +``` + +**类型、操作码、指令字、映像布局以** [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md) **为准。** 头文件实现该文,本 README 不另当规范。全工程阶段见 [`doc/初步计划.md`](../doc/初步计划.md) 12.0~12.1。 + +公开头:`isa/include/isa/`。 + +--- + +## 做 / 不做 + +**做:** 操作码与 32-bit 指令、映像头与段、`BOOL`/`INT`/`TIME` 宽度与饱和、编解码、工程哈希、一条指令的文本转储。 + +**不做:** toml、关键字、符号表、AST、VM `switch`、扫描周期、I/O 采样。`AND`/`OR` 只组合已算好的值;短路由 `compiler` 编成跳转。 + +--- + +## 实现步骤 + +原则:先冻合同,再头文件,再 `.cpp`,再最小测试。每步能编过再进下一步。改编码或映像字段时,先改规范文和本模块,再动 `compiler` / `vm`。 + +目标目录(与规范一致): + +```text +isa/ + CMakeLists.txt + README.md + include/isa/ + Types.hpp # 宽度、饱和、槽值 + Op.hpp # 操作码枚举 + 助记符声明 + Instr.hpp # 32-bit 打包/拆字段(inline) + Encode.hpp # encode / decode / disasm + Image.hpp # 头、段、FNV、读写缓冲 + src/ + Encode.cpp + Image.cpp +``` + +```cmake +add_library(isa STATIC src/Encode.cpp src/Image.cpp) +target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +``` + +`compiler` / `vm`:`target_link_libraries(... PUBLIC isa)`。 + +--- + +### 0. CMake 骨架 + +根 `CMakeLists.txt` 已 `add_subdirectory` 了 `compiler` / `vm` / `host`。这三个目录若没有 `CMakeLists.txt`,顶层 configure 失败,`isa` 也编不过。 + +本步只让工程能配置、能链,不写业务。 + +- 写本目录 `CMakeLists.txt`(上面那段即可) +- `compiler` / `vm`:空 `.cpp` + `PUBLIC` 链 `isa` +- `host`:`main` 打印 `Stator 0.1`,链 `compiler`、`vm` +- 根 CMake:`enable_testing()`(测试文件下一步再加) + +完成:`cmake --build` 过。 + +--- + +### 1. 冻合同(先改规范文) + +[`指令与映像.md`](../doc/isa/指令与映像.md) 已有助记符和 `[ op:8 | rd:8 | a:8 | b:8 ]`,还缺**数值和映像字节**。头文件不得自己发明一份表。 + +把下面这些写进规范文,再写代码(细则以该文最终落笔为准): + +- 指令字:小端 `uint32_t`;`op`/`rd`/`a`/`b` 各 8 bit;`imm16 = a | (b << 8)`;`off16` 为有符号、单位是指令条数 +- 操作码 0..28 连续赋值,以后只追加不插队 +- 各指令操作数字面(`LOADK`/`JMP`/`CALL`/`LOAD_*`/`STORE_*`/`CAL_*`/`RET`) +- `INT` 饱和、`INT_MIN / -1`、除零、`BOOL` 只允许 0/1 +- FNV-1a 64 的 basis/prime;路径只当排序键,对文件内容 update;空集合 = basis +- 映像魔数 `STSC`、版本 1、头字段偏移与长度、常量表/函数表/FB 表一行的字节 +- 第一版可写映像:空槽、空常量表/FB、`MAIN`(`fn_id=0`)、一条 `RET` + +完成:规范文能当唯一合同;本 README 仍只指向它。 + +--- + +### 2. `Types.hpp` + +`include/isa/Types.hpp`,inline,无对应 `.cpp`。命名空间 `isa`。C++11,不用 designated initializer。 + +- `Bool` = `uint8_t`,`Int` = `int16_t`,`Time` = `int32_t`(毫秒) +- `TypeTag`:`Bool` / `Int` / `Time` +- `sat_add` / `sat_sub` / `sat_mul` / `sat_div`(规则见规范文) +- `pack_bool` / `pack_int` / `pack_time` → 4 字节 cell;对应 unpack + +完成:单独包含该头能编译。 + +--- + +### 3. `Op.hpp` + `Instr.hpp` + +- `Op.hpp`:`enum Op : uint8_t`,取值与规范文一致;声明 `op_mnemonic(Op)`(表可放 `Encode.cpp`) +- `Instr.hpp`:`typedef uint32_t Instr`;`pack`、取 `op`/`rd`/`a`/`b`、`imm16`/`off16`;按形态 `enc_rr` / `enc_rrr` / `enc_imm` / `enc_jmp` / `enc_jc` + +完成:能打包/拆开一条 32-bit 指令,不依赖 `.cpp`。 + +--- + +### 4. `Encode.hpp` + `Encode.cpp` + +给 host 打印和测试往返: + +- `Decoded { Op op; uint8_t rd, a, b; }` +- `Instr encode(Decoded)` / `Decoded decode(Instr)` +- `disasm(Instr, char* out, size_t cap)` + +`disasm` 格式冻在规范文,例如:`RET`、`MOVE r1, r2`、`LOADK r0, 3`、`ADD r1, r2, r3`、`JMP +4`、`JT r0, -1`、`CALL 1`、`LOAD_I r0, 2`、`CAL_TON 0`。 + +完成:一条指令能编、能解、能打出一行文本。 + +--- + +### 5. `Image.hpp` + `Image.cpp` + +- `fnv1a64` / `fnv1a64_update` +- `ImageView`:只读视图,指向外部缓冲 +- `read_image`:校验魔数、版本、头大小、各段不越界、`entry_fn_id` 落在函数表内 +- `write_ret_main(cycle_limit, dt_ms, hash)` → `std::vector`:空槽 + `MAIN` + 一条 `RET` + +小端 `put_*` / `get_*` 只放 `.cpp`,不把 packed struct 当 ABI。本阶段不写完整 `ImageBuilder`(等 codegen)。 + +完成:能写出并读回「空映像 + 一条 `RET`」。 + +--- + +### 6. 最小测试 + +在 `tests/` 加一个只链 `isa` 的 CTest 可执行文件(不是第 12.2 节那 20 个用例): + +- `RET` / `ADD` / `LOADK` / `JMP -1`:encode 后再 decode,字段一致;`disasm` 非空 +- `write_ret_main` 再 `read_image`:魔数、version=1、`n_funcs=1`、入口是 `RET` +- `fnv1a64` 空输入等于 basis + +完成:`ctest` 过上述用例。 + +--- + +## 验收 + +- `cmake --build` 过;`ctest` 过 isa 往返 +- `compiler` / `vm` 只 `#include `,不互相包含 +- 操作码或映像有变:先改 [`指令与映像.md`](../doc/isa/指令与映像.md) + 本目录 + 本测试,再改两端 diff --git a/isa/include/isa/Types.h b/isa/include/isa/Types.h new file mode 100644 index 0000000..4ee3721 --- /dev/null +++ b/isa/include/isa/Types.h @@ -0,0 +1,18 @@ +/** + * @file Types.h + * @brief 类型定义 + * @author + * @date 2026-08-18 + */ + +#pragma once + +#include + +namespace isa { + namespace types { + using BOOL = uint8_t; + using INT = int16_t; + using TIME = uint64_t; + } +} diff --git a/isa/src/Encode.cpp b/isa/src/Encode.cpp new file mode 100644 index 0000000..e69de29 From 9a35a881d1e0f494d92197a943910f4db335deff Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 10:09:27 +0800 Subject: [PATCH 08/69] =?UTF-8?q?=E5=BB=BA=E7=AB=8B=E5=9B=9B=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=20CMake=20=E9=AA=A8=E6=9E=B6=E4=B8=8E=20line1=20?= =?UTF-8?q?=E6=A0=B7=E4=BE=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compiler / vm 空静态库,host 可执行打印版本 - 顶层解开 add_subdirectory,tests 接入 CTest(host_version 通过) - examples/line1 落地 project.toml 与三个 .st - Types.h 注释补饱和规则,与 Doc/isa 一致 - .gitignore 忽略 Doc/会话记录.md --- .gitignore | 3 ++- CMakeLists.txt | 10 +++++++--- compiler/CMakeLists.txt | 13 +++++++++++++ compiler/src/Lexer.cpp | 8 ++++++++ examples/line1/globals.st | 7 +++++++ examples/line1/main.st | 8 ++++++++ examples/line1/motor.st | 13 +++++++++++++ examples/line1/project.toml | 28 ++++++++++++++++++++++++++++ host/CMakeLists.txt | 10 ++++++++++ host/src/main.cpp | 14 ++++++++++++++ isa/include/isa/Types.h | 2 ++ tests/CMakeLists.txt | 3 +++ vm/CMakeLists.txt | 12 ++++++++++++ vm/src/Machine.cpp | 8 ++++++++ 14 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 compiler/CMakeLists.txt create mode 100644 compiler/src/Lexer.cpp create mode 100644 examples/line1/globals.st create mode 100644 examples/line1/main.st create mode 100644 examples/line1/motor.st create mode 100644 examples/line1/project.toml create mode 100644 host/CMakeLists.txt create mode 100644 host/src/main.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 vm/CMakeLists.txt create mode 100644 vm/src/Machine.cpp diff --git a/.gitignore b/.gitignore index c463767..a5a33aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/** -.vscode/** \ No newline at end of file +.vscode/** +Doc/会话记录.md \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index cfd9226..4d813b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,10 @@ project(Stator include("${CMAKE_SOURCE_DIR}/cmake/Standards.cmake") add_subdirectory(isa) -# add_subdirectory(compiler) -# add_subdirectory(vm) -# add_subdirectory(host) \ No newline at end of file +add_subdirectory(compiler) +add_subdirectory(vm) +add_subdirectory(host) + +# 自动化用例 +enable_testing() +add_subdirectory(tests) \ No newline at end of file diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt new file mode 100644 index 0000000..fb16c4f --- /dev/null +++ b/compiler/CMakeLists.txt @@ -0,0 +1,13 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(Compiler + VERSION 0.1 + DESCRIPTION "ST / toml 编译器") + +include_directories(./include) + +# 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 +add_library(compiler STATIC + ./src/Lexer.cpp) diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp new file mode 100644 index 0000000..f73d4d0 --- /dev/null +++ b/compiler/src/Lexer.cpp @@ -0,0 +1,8 @@ +/** + * @file Lexer.cpp + * @brief 词法分析(12.4 实现) + * @date 2026-08-19 + */ + +namespace compiler { +} diff --git a/examples/line1/globals.st b/examples/line1/globals.st new file mode 100644 index 0000000..6baeaa6 --- /dev/null +++ b/examples/line1/globals.st @@ -0,0 +1,7 @@ +(* globals.st *) +VAR_GLOBAL + EmergencyStop : BOOL := FALSE; + I0_0 : BOOL; + I0_1 : BOOL; + Q0_0 : BOOL; +END_VAR diff --git a/examples/line1/main.st b/examples/line1/main.st new file mode 100644 index 0000000..fef1b92 --- /dev/null +++ b/examples/line1/main.st @@ -0,0 +1,8 @@ +(* main.st *) +PROGRAM MAIN +VAR + starter : MotorStarter; +END_VAR + starter(start := I0_0, stop := I0_1); + Q0_0 := starter.Q; +END_PROGRAM diff --git a/examples/line1/motor.st b/examples/line1/motor.st new file mode 100644 index 0000000..69de7df --- /dev/null +++ b/examples/line1/motor.st @@ -0,0 +1,13 @@ +(* motor.st *) +FUNCTION_BLOCK MotorStarter +VAR_EXTERNAL + EmergencyStop : BOOL; +END_VAR +VAR_INPUT + start, stop : BOOL; +END_VAR +VAR_OUTPUT + Q : BOOL; +END_VAR + Q := start AND NOT stop AND NOT EmergencyStop; +END_FUNCTION_BLOCK diff --git a/examples/line1/project.toml b/examples/line1/project.toml new file mode 100644 index 0000000..3f6658f --- /dev/null +++ b/examples/line1/project.toml @@ -0,0 +1,28 @@ +[project] +name = "line1" +entry = "program MAIN" +cycle_limit = 100000 +dt_ms = 10 + +[files] +st = ["globals.st", "motor.st", "main.st"] + +# 全工程只允许这一处出现 VAR_GLOBAL(第一版:单个文件) +[gvl] +file = "globals.st" + +# 可选:I/O 绑定。不创造变量,var 必须已在 GVL 里声明 +[[io.input]] +var = "EmergencyStop" +channel = 0 +bit = 2 + +[[io.input]] +var = "I0_0" +channel = 0 +bit = 0 + +[[io.output]] +var = "Q0_0" +channel = 0 +bit = 0 diff --git a/host/CMakeLists.txt b/host/CMakeLists.txt new file mode 100644 index 0000000..c7f1fa0 --- /dev/null +++ b/host/CMakeLists.txt @@ -0,0 +1,10 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(Host + VERSION 0.1 + DESCRIPTION "可执行入口:编译并跑扫描周期") + +add_executable(host + ./src/main.cpp) diff --git a/host/src/main.cpp b/host/src/main.cpp new file mode 100644 index 0000000..5a1330c --- /dev/null +++ b/host/src/main.cpp @@ -0,0 +1,14 @@ +/** + * @file main.cpp + * @brief host 可执行入口 + * @date 2026-08-19 + */ + +#include + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + std::printf("Stator host 0.1\n"); + return 0; +} diff --git a/isa/include/isa/Types.h b/isa/include/isa/Types.h index 4ee3721..5a58f5a 100644 --- a/isa/include/isa/Types.h +++ b/isa/include/isa/Types.h @@ -11,6 +11,8 @@ namespace isa { namespace types { + // 定宽类型。整数溢出规则:饱和(夹到 INT 最小/最大), + // 由 compiler 与 vm 按同一规则实现,见 Doc/isa/指令与映像.md。 using BOOL = uint8_t; using INT = int16_t; using TIME = uint64_t; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8129641 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,3 @@ +# 自动化用例:先放一个必过的空测试,验证 CTest 接线 +add_test(NAME host_version + COMMAND host) diff --git a/vm/CMakeLists.txt b/vm/CMakeLists.txt new file mode 100644 index 0000000..9ffb15f --- /dev/null +++ b/vm/CMakeLists.txt @@ -0,0 +1,12 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(VM + VERSION 0.1 + DESCRIPTION "寄存器虚拟机") + +include_directories(./include) + +add_library(vm STATIC + ./src/Machine.cpp) diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp new file mode 100644 index 0000000..c337c8e --- /dev/null +++ b/vm/src/Machine.cpp @@ -0,0 +1,8 @@ +/** + * @file Machine.cpp + * @brief 寄存器虚拟机(12.9 实现) + * @date 2026-08-19 + */ + +namespace vm { +} From 94accd52c3215f909d6864488f3fcbc261a57920 Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 13:59:48 +0800 Subject: [PATCH 09/69] =?UTF-8?q?=E8=90=BD=E5=9C=B0=20isa=20=E5=90=88?= =?UTF-8?q?=E5=90=8C=EF=BC=9A=E9=A5=B1=E5=92=8C=E7=B1=BB=E5=9E=8B=E3=80=81?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=A0=81=E6=9E=9A=E4=B8=BE=E3=80=8132-bit=20?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E7=BC=96=E8=A7=A3=E7=A0=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Types.h:TIME 改 int64_t(毫秒),补 sat_add/sub/mul/div,除零与 INT_MIN/-1 规则 - Op.h:29 条操作码枚举 + mnemonic 助记符,数值即编码 - Instr.h:pack/拆字段 + 按形态 enc_rr/rrr/imm/jmp/jc/slot/call/ret --- isa/include/isa/Instr.h | 93 +++++++++++++++++++++++++++++++++++++++++ isa/include/isa/Op.h | 65 ++++++++++++++++++++++++++++ isa/include/isa/Types.h | 44 +++++++++++++++++-- 3 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 isa/include/isa/Instr.h create mode 100644 isa/include/isa/Op.h diff --git a/isa/include/isa/Instr.h b/isa/include/isa/Instr.h new file mode 100644 index 0000000..72afc27 --- /dev/null +++ b/isa/include/isa/Instr.h @@ -0,0 +1,93 @@ +/** + * @file Instr.h + * @brief 32-bit 指令打包 / 拆字段 + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include + +#include "isa/Op.h" + +namespace isa { + // 指令字:[ op:8 | rd:8 | a:8 | b:8 ],整体是一个小端 uint32_t。 + // 字段含义按操作码形态解释,见 Doc/isa/指令与映像.md。 + typedef uint32_t Instr; + + // 打包 / 拆字段 + inline Instr pack(Op op, uint8_t rd, uint8_t a, uint8_t b) { + return static_cast(static_cast(op)) + | (static_cast(rd) << 8) + | (static_cast(a) << 16) + | (static_cast(b) << 24); + } + + inline Op op(Instr w) { + return static_cast(w & 0xFFu); + } + + inline uint8_t rd(Instr w) { + return static_cast((w >> 8) & 0xFFu); + } + + inline uint8_t a(Instr w) { + return static_cast((w >> 16) & 0xFFu); + } + + inline uint8_t b(Instr w) { + return static_cast((w >> 24) & 0xFFu); + } + + // 组合视图:a|b 拼成 16 位(const_id / fn_id / slot) + inline uint16_t imm16(Instr w) { + return static_cast(a(w) | (static_cast(b(w)) << 8)); + } + + // 组合视图:a|b 为有符号相对偏移,单位是指令条数 + inline int16_t off16(Instr w) { + return static_cast(imm16(w)); + } + + // 按形态的便捷编码 + inline Instr enc_rr(Op op, uint8_t rd, uint8_t rs) { + return pack(op, rd, rs, 0); // MOVE / NOT:a = rs + } + + inline Instr enc_rrr(Op op, uint8_t rd, uint8_t ra, uint8_t rb) { + return pack(op, rd, ra, rb); // AND/OR/ADD/.../CMP_xx + } + + inline Instr enc_imm(Op op, uint8_t rd, uint16_t imm) { + return pack(op, rd, static_cast(imm & 0xFFu), + static_cast((imm >> 8) & 0xFFu)); // LOADK + } + + inline Instr enc_jmp(int16_t off) { + return pack(Op::JMP, 0, + static_cast(static_cast(off) & 0xFFu), + static_cast((static_cast(off) >> 8) & 0xFFu)); + } + + inline Instr enc_jc(Op op, uint8_t r, int16_t off) { + // JT / JF:条件寄存器在 rd,偏移在 a|b + return pack(op, r, + static_cast(static_cast(off) & 0xFFu), + static_cast((static_cast(off) >> 8) & 0xFFu)); + } + + inline Instr enc_slot(Op op, uint8_t rd, uint16_t slot) { + // LOAD_I / STORE_Q / LOAD_M / STORE_M / LOAD_GLOBAL / STORE_GLOBAL / CAL_* + // 槽号 / 实例号在 a|b + return enc_imm(op, rd, slot); + } + + inline Instr enc_call(uint16_t fn_id) { + return enc_imm(Op::CALL, 0, fn_id); + } + + inline Instr enc_ret() { + return pack(Op::RET, 0, 0, 0); + } +} diff --git a/isa/include/isa/Op.h b/isa/include/isa/Op.h new file mode 100644 index 0000000..42bc79e --- /dev/null +++ b/isa/include/isa/Op.h @@ -0,0 +1,65 @@ +/** + * @file Op.h + * @brief 操作码枚举 + 助记符 + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include + +namespace isa { + // 操作码一次列全。数值即编码:冻结后不得改顺序、不得插值, + // 增加指令只能追加到末尾,并同步 Doc/isa/指令与映像.md 与测试。 + enum class Op : uint8_t { + MOVE = 0, // MOVE rd, rs + LOADK = 1, // LOADK rd, const_id + NOT = 2, // NOT rd, rs + AND = 3, // AND rd, ra, rb + OR = 4, // OR rd, ra, rb + ADD = 5, // ADD rd, ra, rb + SUB = 6, // SUB rd, ra, rb + MUL = 7, // MUL rd, ra, rb + DIV = 8, // DIV rd, ra, rb + CMP_EQ = 9, // CMP_EQ rd, ra, rb + CMP_NE = 10, // CMP_NE rd, ra, rb + CMP_LT = 11, // CMP_LT rd, ra, rb + CMP_LE = 12, // CMP_LE rd, ra, rb + CMP_GT = 13, // CMP_GT rd, ra, rb + CMP_GE = 14, // CMP_GE rd, ra, rb + JMP = 15, // JMP offset + JT = 16, // JT r, offset + JF = 17, // JF r, offset + LOAD_I = 18, // LOAD_I rd, slot + STORE_Q = 19, // STORE_Q slot, rs + LOAD_M = 20, // LOAD_M rd, slot + STORE_M = 21, // STORE_M slot, rs + LOAD_GLOBAL = 22, // LOAD_GLOBAL rd, slot + STORE_GLOBAL = 23, // STORE_GLOBAL slot, rs + CAL_TON = 24, // CAL_TON instance_slot + CAL_TOF = 25, // CAL_TOF instance_slot + CAL_CTU = 26, // CAL_CTU instance_slot + CALL = 27, // CALL fn_id + RET = 28, // RET + }; + + // 操作码总数 + static const int kOpCount = 29; + + // 助记符:下标与 Op 数值一一对应 + inline const char* mnemonic(Op op) { + static const char* const kMnemonic[kOpCount] = { + "MOVE", "LOADK", "NOT", "AND", "OR", + "ADD", "SUB", "MUL", "DIV", + "CMP_EQ", "CMP_NE", "CMP_LT", "CMP_LE", "CMP_GT", "CMP_GE", + "JMP", "JT", "JF", + "LOAD_I", "STORE_Q", "LOAD_M", "STORE_M", + "LOAD_GLOBAL", "STORE_GLOBAL", + "CAL_TON", "CAL_TOF", "CAL_CTU", + "CALL", "RET", + }; + const int idx = static_cast(op); + return (idx >= 0 && idx < kOpCount) ? kMnemonic[idx] : "???"; + } +} diff --git a/isa/include/isa/Types.h b/isa/include/isa/Types.h index 5a58f5a..fd74a9f 100644 --- a/isa/include/isa/Types.h +++ b/isa/include/isa/Types.h @@ -8,13 +8,51 @@ #pragma once #include +#include namespace isa { namespace types { // 定宽类型。整数溢出规则:饱和(夹到 INT 最小/最大), // 由 compiler 与 vm 按同一规则实现,见 Doc/isa/指令与映像.md。 - using BOOL = uint8_t; - using INT = int16_t; - using TIME = uint64_t; + using BOOL = uint8_t; // 只允许 0/1 + using INT = int16_t; // IEC INT + // 毫秒,有符号。64 位理由见 Doc/isa/指令与映像.md: + // 差值可负、为 DATE_AND_TIME 与 64 位定时器预留。槽位按 8 字节对齐。 + using TIME = int64_t; + + // 饱和算术:夹到 INT 最小/最大,编译期与 VM 共用同一规则。 + + inline INT sat_add(INT a, INT b) { + const int32_t sum = static_cast(a) + static_cast(b); + return static_cast(sum < std::numeric_limits::min() ? std::numeric_limits::min() + : sum > std::numeric_limits::max() ? std::numeric_limits::max() + : sum); + } + + inline INT sat_sub(INT a, INT b) { + const int32_t diff = static_cast(a) - static_cast(b); + return static_cast(diff < std::numeric_limits::min() ? std::numeric_limits::min() + : diff > std::numeric_limits::max() ? std::numeric_limits::max() + : diff); + } + + inline INT sat_mul(INT a, INT b) { + const int32_t prod = static_cast(a) * static_cast(b); + return static_cast(prod < std::numeric_limits::min() ? std::numeric_limits::min() + : prod > std::numeric_limits::max() ? std::numeric_limits::max() + : prod); + } + + inline INT sat_div(INT a, INT b) { + // b == 0 视为除以 1,避免未定义行为;除零的语义由 compiler/VM 层报错 + if (b == 0) { + return a; + } + // INT_MIN / -1 溢出:饱和到 INT_MAX + if (a == std::numeric_limits::min() && b == -1) { + return std::numeric_limits::max(); + } + return static_cast(a / b); + } } } From 7c855b1618c4b647b3361365ed58999421388c22 Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 13:59:50 +0800 Subject: [PATCH 10/69] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=8F=8C=E5=8F=AF=E6=89=A7=E8=A1=8C=E6=9E=B6=E6=9E=84=EF=BC=9A?= =?UTF-8?q?STCompiler=20+=20BytecodeExecutor=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 12.10 重写为两条命令:STCompiler 编译出 .stb + sidecar,BytecodeExecutor 加载执行 - 12.0 删独立 stc.exe,12.13 依赖方向改为 STCompiler/BytecodeExecutor - 指令与映像.md 新增「映像文件(.stb)与 sidecar」节,I/O 绑定不进映像 - host/ 改名 executor/,宿主入口.md 重写为执行器入口.md - README / tests / examples / isa README 同步 --- Doc/compiler/编译管线.md | 5 +++-- Doc/executor/执行器入口.md | 18 +++++++++++++++ Doc/host/宿主入口.md | 18 --------------- Doc/isa/指令与映像.md | 45 +++++++++++++++++++++++++------------- Doc/vm/扫描周期.md | 4 ++-- Doc/初步计划.md | 36 +++++++++++++++++------------- Doc/索引.md | 4 ++-- README.md | 27 ++++++++++++----------- examples/README.md | 2 +- isa/README.md | 37 ++++++++++++++++--------------- tests/README.md | 2 +- 11 files changed, 111 insertions(+), 87 deletions(-) create mode 100644 Doc/executor/执行器入口.md delete mode 100644 Doc/host/宿主入口.md diff --git a/Doc/compiler/编译管线.md b/Doc/compiler/编译管线.md index cd0c370..1bcf34c 100644 --- a/Doc/compiler/编译管线.md +++ b/Doc/compiler/编译管线.md @@ -1,8 +1,8 @@ # 编译管线 -compiler 模块:ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 [`指令与映像.md`](../isa/指令与映像.md)。 +compiler 模块:ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 [`指令与映像.md`](../isa/指令与映像.md)。可执行入口 `STCompiler`(本模块 `src/main.cpp`,链 `compiler` + `isa`)。 -读齐工程后走直通 pass,产出单一映像,不上 LLVM。词法、语法、符号表、检查、codegen、链接都留在本库,不再拆 CMake 子库。 +读齐工程后走直通 pass,产出单一映像文件,不上 LLVM。词法、语法、符号表、检查、codegen、链接都留在本库,不再拆 CMake 子库。 语言规则、toml 字段、链接与第 12 节阶段见 [`初步计划.md`](../初步计划.md)。指令和映像只认 [`指令与映像.md`](../isa/指令与映像.md)。 @@ -16,6 +16,7 @@ compiler 模块:ST / toml 编译器。CMake 目标:`compiler`(`STATIC`) → 类型检查 → 按 PROGRAM / FUNCTION / FB 编寄存器码 → 回填跳转、写映像头与工程哈希 + → 写 .stb(映像)与 .runtime.toml(sidecar:io 绑定) ``` ## 边界 diff --git a/Doc/executor/执行器入口.md b/Doc/executor/执行器入口.md new file mode 100644 index 0000000..006e180 --- /dev/null +++ b/Doc/executor/执行器入口.md @@ -0,0 +1,18 @@ +# 执行器入口 + +BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`(`EXECUTABLE`),链接 `vm` 和 `isa`,**不链 `compiler`**。源码目录 `executor/`。 + +第一版两个可执行:`STCompiler` 编译出 `.stb` 与 `.runtime.toml`;`BytecodeExecutor` 加载映像按扫描周期跑。总顺序见 [`初步计划.md`](../初步计划.md) 第 12.10 节。 + +## 职责 + +- 加载 `.stb` 映像与 `.runtime.toml` sidecar(I/O 绑定 var → 槽号 → channel/bit) +- 按 sidecar 采样 I、写回 Q(不创造变量) +- `dt_ms` / `cycle_limit` 从映像头取,推进 `TON` / `TOF` / `CTU` +- 单步、看寄存器和映像、按录制的 `I` 回放 + +## 边界 + +- 指令与映像编码见 [`指令与映像.md`](../isa/指令与映像.md) +- ST 解析与链接见 [`编译管线.md`](../compiler/编译管线.md),本模块不碰 `.st` +- 寄存器执行循环见 [`扫描周期.md`](../vm/扫描周期.md) diff --git a/Doc/host/宿主入口.md b/Doc/host/宿主入口.md deleted file mode 100644 index 4dea1f9..0000000 --- a/Doc/host/宿主入口.md +++ /dev/null @@ -1,18 +0,0 @@ -# 宿主入口 - -host 模块:可执行入口。CMake 目标:`host`(`EXECUTABLE`),链接 `compiler` 和 `vm`。 - -第一版一条命令:读工程 → 编译映像 → 按扫描周期跑。不拆独立的编译器可执行文件。总顺序见 [`初步计划.md`](../初步计划.md) 第 12.10 节。 - -## 职责 - -- 读 `project.toml` 和其中的 `.st`,交给 `compiler` -- 按 `[[io.input]]` / `[[io.output]]` 采样 I、写回 Q(不创造变量) -- 把 `dt_ms` 交给 VM,推进 `TON` / `TOF` / `CTU` -- 单步、看寄存器和映像、按录制的 `I` 回放 - -## 边界 - -- 指令编码见 [`指令与映像.md`](../isa/指令与映像.md) -- ST 解析与链接见 [`编译管线.md`](../compiler/编译管线.md) -- 寄存器执行循环见 [`扫描周期.md`](../vm/扫描周期.md) diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index c4aed38..63ac85a 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -7,8 +7,8 @@ isa 模块规范:指令集 + 映像 + 定宽类型。不是编译器,不把 全工程定案见 [`初步计划.md`](../初步计划.md)。**类型、指令编码、映像布局以本文为准**,不要另写一份互相打架的表。头文件实现本文,不另当规范。 ```text -host → compiler → isa -host → vm → isa +STCompiler → compiler → isa +BytecodeExecutor → vm → isa ``` CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/`。 @@ -22,7 +22,7 @@ CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/` - 操作码与 32 位指令布局 - 映像头与段(函数字节码、全局 / FB 槽、工程哈希) - `BOOL` / `INT` / `TIME` 宽度与溢出规则 -- 编解码、工程哈希、一条指令的文本转储(给 host 打印) +- 编解码、工程哈希、一条指令的文本转储(反汇编,给 STCompiler `--disasm` 与测试打印) **不做** @@ -40,7 +40,7 @@ CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/` |---|---| | `BOOL` | `uint8_t`,只允许 0/1 | | `INT` | `int16_t`(IEC `INT`) | -| `TIME` | `int32_t` 毫秒;与 `INT` 不混用 | +| `TIME` | `int64_t` 毫秒(有符号);与 `INT` 不混用 | | 整数溢出 | **饱和**(夹到 `INT` 最小/最大) | | 字节序 | 小端 | | 指令宽度 | 32 bit,一种主布局 | @@ -49,8 +49,8 @@ CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/` | `CALL` | 立即数 `fn_id`(u16),无函数指针 | | 映像魔数 | `STSC`,版本 `1` | | 工程哈希 | 源文件路径排序后对内容做 FNV-1a 64(非密码学) | -| I/O 绑定 | **不进映像**;编译期把 `var` 收成槽号,channel/bit 留在 host 读 toml | -| 不做 | `REAL`、隐式宽化、独立 `stc.exe` | +| I/O 绑定 | **不进映像**;编译期把 `var` 收成槽号,channel/bit 由 STCompiler 写进 sidecar(`.runtime.toml`),BytecodeExecutor 读 sidecar 采样 | +| 不做 | `REAL`、隐式宽化 | --- @@ -60,10 +60,16 @@ C++11 定宽: - `BOOL`:`uint8_t`,0/1 - `INT`:`int16_t`;加减乘除溢出饱和 -- `TIME`:`int32_t` 毫秒 +- `TIME`:`int64_t` 毫秒,有符号 `compiler` 与 `vm` 都按同一规则,不再另选溢出策略。 +`TIME` 选 `int64_t` 的理由(冻结,不再反复问): + +- 有符号:`TOF` 剩余时间、两个时间点相减会出负值,免去无符号边界处理 +- 64 位:为将来 `DATE_AND_TIME`(毫秒时间戳)与 `TON` / `TOF` / `TP` 的 64 位定时器预留;毫秒量程约 2.9 亿年,扫描周期场景永不溢出 +- 代价:槽位 8 字节对齐。**槽位对齐规则**:`BOOL` 1 字节、`INT` 2 字节、`TIME` 8 字节;连续排列按类型对齐,需要时插 padding,偏移以本规则为准。 + --- ## 指令 @@ -123,6 +129,15 @@ FB 实例固定布局,字段偏移编译期算死。 --- +## 映像文件(`.stb`)与 sidecar + +编译产物是单文件 `.stb`:映像头 + 各段按本文布局连续排,魔数 `STSC`、版本 `1`、小端。`STCompiler` 写,`BytecodeExecutor` 读。 + +- `dt_ms` / `cycle_limit` 在映像头,执行器只认映像,不重复配置。 +- I/O 绑定不进映像:`STCompiler` 另写 sidecar `.runtime.toml`,内容是 `var`(已解析槽号)→ `channel` / `bit`;`BytecodeExecutor` 读 sidecar 做采样与写回,不创造变量。 + +--- + ## 目录与 CMake ```text @@ -130,11 +145,11 @@ isa/ CMakeLists.txt README.md # 代码目录入口,指向本文 include/isa/ - Types.hpp # 宽度、饱和、槽值表示 - Op.hpp # 操作码枚举 + 助记符 - Instr.hpp # 32-bit 打包/拆字段(inline) - Image.hpp # 头、段、函数表/常量表/FB 布局 - Encode.hpp # 编解码、哈希、反汇编一行 + Types.h # 宽度、饱和、槽值表示 + Op.h # 操作码枚举 + 助记符 + Instr.h # 32-bit 打包/拆字段(inline) + Image.h # 头、段、函数表/常量表/FB 布局 + Encode.h # 编解码、哈希、反汇编一行 src/ Encode.cpp Image.cpp @@ -151,10 +166,10 @@ target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) ## 实现顺序(对齐初步计划 12.0~12.1) -1. `Types.hpp`:三类型 + `sat_add` 等饱和函数(inline 即可) -2. `Op.hpp` + `Instr.hpp`:枚举与 32-bit 编解码 +1. `Types.h`:三类型 + `sat_add` 等饱和函数(inline 即可) +2. `Op.h` + `Instr.h`:枚举与 32-bit 编解码 3. `Encode.cpp`:`encode` / `decode` / `disasm` 一条指令 -4. `Image.hpp` + `Image.cpp`:头布局、FNV-1a、读写缓冲(先能写空映像 + 一条 `RET`) +4. `Image.h` + `Image.cpp`:头布局、FNV-1a、读写缓冲(先能写空映像 + 一条 `RET`) 5. 测试随后在 `tests/` 加编解码往返;本阶段以头文件和库能链为准 不在本模块做:VM `switch`、toml、关键字、符号表。 diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md index 16d789b..71dbbe0 100644 --- a/Doc/vm/扫描周期.md +++ b/Doc/vm/扫描周期.md @@ -7,7 +7,7 @@ vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [ ## 扫描周期 ```text -1. 按 toml 的 io 绑定,采样 → I / 输入全局 +1. 按 sidecar 的 io 绑定,采样 → I / 输入全局 2. 从 PROGRAM MAIN 的 pc=0 执行到 RET 3. 写回 Q / 输出全局 4. 用本周期 Δt 推进 TON / TOF / CTU(不用 wall clock) @@ -19,4 +19,4 @@ vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [ - 局部 / `VAR` = 固定寄存器;`nregs` 在函数头 - `CALL` 目标是立即数 `fn_id`,有界调用栈 - 无 GC、无堆、无线程;每周期受 `cycle_limit` 限制 -- I/O 从哪来、Δt 谁给、回放怎么喂,属于 `host` +- I/O 从哪来、Δt 谁给、回放怎么喂,属于 `BytecodeExecutor`(读 `.stb` + sidecar) diff --git a/Doc/初步计划.md b/Doc/初步计划.md index eb099b0..edaa3a3 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -271,13 +271,13 @@ RET ## 10. 目录 -仓库布局见根目录 `README.md`。实现时按四个 CMake 目标拆,样例不放仓库根: +仓库布局见根目录 `README.md`。实现时按模块拆 CMake 目标(三个库 + 两个可执行),样例不放仓库根: ```text isa/ # 指令编码、映像头、定宽类型(compiler 与 vm 的合同) -compiler/ # lexer parser symbols checker codegen linker -vm/ # 扫描周期、寄存器、映像、FB -host/ # 喂 I、收 Q、Δt、回放 +compiler/ # lexer parser symbols checker codegen linker;STCompiler 入口在 src/main.cpp +vm/ # 扫描周期、寄存器、映像、FB(库) +executor/ # BytecodeExecutor:加载 .stb + sidecar,喂 I、收 Q、Δt、回放 examples/line1/ # 第 11 节最小样例(给人看) tests/ # 冻规则用的正例 / 负例 ``` @@ -327,13 +327,13 @@ END_PROGRAM 原则:**先冻合同,再解析,再链接,最后写 VM**。`compiler` 与 `vm` 不互链,合同只进 `isa`。每一阶段先有可运行的测试,再进入下一阶段。一人约 4~8 周做到第 11 节 `line1`。 -第一版不做:图染色、SSA、WHILE 有界证明、异常表、闭包、优化、`REAL`、独立 `stc.exe`。 +第一版不做:图染色、SSA、WHILE 有界证明、异常表、闭包、优化、`REAL`。 --- ### 12.0 骨架(约 1 天) -CMake 四个目标能空编译:`isa`、`compiler`、`vm`、`host`。`host` 先打印版本即可。 +CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCompiler`、`BytecodeExecutor` 先打印版本即可。 - 落下 `examples/line1/`:第 3 节 toml + 第 11 节三个 `.st`。 - `tests/` 接上 CTest,先放一个必过的空测试。 @@ -524,16 +524,22 @@ CMake 四个目标能空编译:`isa`、`compiler`、`vm`、`host`。`host` 先 --- -### 12.10 host(约 2~3 天) +### 12.10 双可执行:STCompiler 与 BytecodeExecutor(约 2~3 天) -一条命令:读 toml → `compiler` → `vm` 跑 N 个周期。不拆第二个可执行文件。 +编译与执行拆成两个可执行,映像 `.stb` 是中间产物: -- 按 `[[io.*]]` 从宿主缓冲采样 / 写回,不创造变量。 -- 把 `dt_ms` 每周期传给 VM。 -- 最小可观测:打印本周期 I/Q,或把寄存器/映像 dump 出来。 +```text +STCompiler -o .stb # 编译:读 toml + .st → 映像 + sidecar +BytecodeExecutor .stb [--cycles N] # 执行:加载 .stb + sidecar,跑 N 周期 +``` + +- `STCompiler` 链 `compiler` + `isa`;`BytecodeExecutor` 链 `vm` + `isa`,**不链 `compiler`**。 +- 产物两个文件:`.stb`(映像,魔数 `STSC`)+ `.runtime.toml`(sidecar:I/O 绑定 var → 槽号 → channel/bit)。`[[io.*]]` 不进映像,sidecar 由 STCompiler 生成,不创造变量。 +- `dt_ms` / `cycle_limit` 从映像头取,sidecar 不重复配置。 +- 最小可观测:BytecodeExecutor 打印本周期 I/Q,或把寄存器/映像 dump 出来。 - 回放:读预先录制的 I 序列(文本即可),不接真实硬件。 -完成:`host examples/line1/project.toml` 能编译并跑若干周期。 +完成:`STCompiler examples/line1/project.toml -o line1.stb` 产出两个文件;`BytecodeExecutor line1.stb` 能跑若干周期。 --- @@ -556,7 +562,7 @@ CMake 四个目标能空编译:`isa`、`compiler`、`vm`、`host`。`host` 先 | 2 | 12.5:AST,能 parse `line1`,拒绝多余特性 | | 3 | 12.6~12.7:链接、定址、类型、io 校验 | | 4 | 12.8 前半 + 12.9 前半:赋值/短路/IF 出映像,VM 能跑 | -| 5 | 12.8 后半 + 12.10:CALL、FB、host 跑起来 | +| 5 | 12.8 后半 + 12.10:CALL、FB、两个可执行跑起来 | | 6 | 定时器/计数器、io、Δt、`line1` 语义 | | 7~8 | 20 用例、回放、单步、补漏 | @@ -567,8 +573,8 @@ CMake 四个目标能空编译:`isa`、`compiler`、`vm`、`host`。`host` 先 ### 12.13 依赖方向(全程遵守) ```text -host → compiler → isa -host → vm → isa +STCompiler → compiler → isa +BytecodeExecutor → vm → isa ``` - 指令或映像布局有变:先改 `isa` 和编解码测试,再改两端。 diff --git a/Doc/索引.md b/Doc/索引.md index 72ac6f6..1e278c4 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -9,7 +9,7 @@ doc/ isa/指令与映像.md compiler/编译管线.md vm/扫描周期.md - host/宿主入口.md + executor/执行器入口.md ``` | 路径 | 内容 | @@ -18,6 +18,6 @@ doc/ | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | -| [`host/宿主入口.md`](host/宿主入口.md) | 可执行入口:编译 + 跑周期 | +| [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | 代码目录里的 README 只作入口;类型、指令、映像以 `isa/指令与映像.md` 为准。 diff --git a/README.md b/README.md index ccdb042..2728cc2 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,23 @@ 多个 .st ↑ project.toml 列出 ↓ -compiler(词法 / 语法 → 符号表 → 类型检查 → 寄存器码 → 链接) +STCompiler(compiler 库:词法 / 语法 → 符号表 → 类型检查 → 寄存器码 → 链接) ↓ -单一映像(函数字节码 + 全局 / FB 数据槽 + 工程哈希) +.stb 映像 + .runtime.toml sidecar ↓ -vm 按扫描周期执行;host 喂 I、收 Q、给 Δt、回放 +BytecodeExecutor(vm 库按扫描周期执行;喂 I、收 Q、给 Δt、回放) ``` ## 目录 ```text . -├── CMakeLists.txt # 顶层工程:加入四个目标,并启用测试 +├── CMakeLists.txt # 顶层工程:加入各目标,并启用测试 ├── cmake/ # CMake 辅助(警告、C++ 标准等),不是业务模块 ├── isa/ # 共享契约:指令、映像、定宽类型 -├── compiler/ # ST / toml 编译器 -├── vm/ # 寄存器虚拟机 -├── host/ # 可执行入口:编译并跑扫描周期 +├── compiler/ # ST / toml 编译器(库)+ STCompiler 可执行入口 +├── vm/ # 寄存器虚拟机(库) +├── executor/ # BytecodeExecutor:加载 .stb + sidecar 并执行 ├── examples/ # 样例工程(给人看、跑通最小闭环) │ └── line1/ # MAIN + Motor FB + I/O │ ├── project.toml @@ -40,14 +40,15 @@ vm 按扫描周期执行;host 喂 I、收 Q、给 Δt、回放 ## 模块与 CMake 目标 -四个链接目标,与目录一一对应。`compiler` 与 `vm` 不互相链接,合同放在 `isa`。 +三个静态库与两个可执行,与目录一一对应。`compiler` 与 `vm` 不互相链接,合同放在 `isa`。依赖方向:`STCompiler → compiler → isa`、`BytecodeExecutor → vm → isa`。 | 目录 | 目标 | 类型 | 依赖 | 职责 | |---|---|---|---|---| -| `isa/` | `isa` | `STATIC` 或 `INTERFACE` | 无 | 约 20 条指令编码、映像头、工程哈希、`BOOL` / `INT` / `TIME` 宽度 | +| `isa/` | `isa` | `STATIC` | 无 | 约 20 条指令编码、映像头、工程哈希、`BOOL` / `INT` / `TIME` 宽度 | | `compiler/` | `compiler` | `STATIC` | `isa` | 读 toml、收齐 `.st`、解析、链接、定址、类型检查、编寄存器码、写映像 | | `vm/` | `vm` | `STATIC` | `isa` | 一台 VM、一个入口、一份映像;扫描周期、寄存器、FB、有界 `CALL` / `RET` | -| `host/` | `host` | `EXECUTABLE` | `compiler`、`vm` | 读工程、编译、按 toml 的 io 采样 I、执行 `PROGRAM MAIN`、写回 Q、用 Δt 推进定时器 / 计数器、回放 | +| `compiler/` | `STCompiler` | `EXECUTABLE` | `compiler`、`isa` | `STCompiler -o .stb`:编译并写映像 + sidecar;`--disasm` 可反汇编 | +| `executor/` | `BytecodeExecutor` | `EXECUTABLE` | `vm`、`isa` | `BytecodeExecutor .stb`:加载映像 + sidecar,采样 I、执行 `PROGRAM MAIN`、写回 Q、用 Δt 推进定时器 / 计数器、回放 | 词法、语法、符号表、检查、codegen、链接都留在 `compiler` 一个库里,不再拆 CMake 子库。 @@ -63,9 +64,9 @@ toml **不声明变量**,只列出源文件、指定唯一 GVL、可选地把 每个扫描周期:采样 I → 从 `PROGRAM MAIN` 的 `pc=0` 执行到 `RET` → 写回 Q → 用本周期 Δt 推进 `TON` / `TOF` / `CTU`。无 GC、无堆、无线程;变量编译期分配。 -### `host` +### `executor`(BytecodeExecutor) -第一版做成一条命令:编译 + 跑周期。不必再拆独立的编译器可执行文件。 +第一版两个可执行:`STCompiler` 编译出 `.stb`(映像)+ `.runtime.toml`(sidecar:I/O 绑定);`BytecodeExecutor` 加载后按扫描周期跑。不再有"一条命令编译 + 跑"。`dt_ms` / `cycle_limit` 从映像头取。 ### `examples/` 与 `tests/` @@ -74,4 +75,4 @@ toml **不声明变量**,只列出源文件、指定唯一 GVL、可选地把 | `examples/line1/` | 计划中的最小样例:全局、`MotorStarter` FB、`PROGRAM MAIN` | | `tests/` | 冻关键字、指令、符号表、链接错误(重复 `VAR_GLOBAL`、`io.var` 未声明等) | -`tests/` 通过 `add_test` 调用 `host` 或直接链接 `compiler` + `vm`。 +`tests/` 通过 `add_test` 调 `STCompiler` / `BytecodeExecutor`,或直接链接 `compiler` + `vm`。样例工程数据按「一份 toml + 若干 `.st`」分子目录存放,不做成 library。 diff --git a/examples/README.md b/examples/README.md index 1568291..d50c168 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,7 +2,7 @@ 给人看的样例工程,**不是** CMake 库。每个子目录是一份完整工程:`project.toml` + 若干 `.st`。 -供 `host` 直接打开,用来跑通最小闭环。自动化断言放 `tests/`。 +供 `STCompiler` 编译、`BytecodeExecutor` 执行,用来跑通最小闭环。自动化断言放 `tests/`。 ## line1 diff --git a/isa/README.md b/isa/README.md index d06cf95..5265c12 100644 --- a/isa/README.md +++ b/isa/README.md @@ -5,8 +5,8 @@ `compiler` 写映像、`vm` 读映像,两边只依赖本目录,不互链。 ```text -host → compiler → isa -host → vm → isa +STCompiler → compiler → isa +BytecodeExecutor → vm → isa ``` **类型、操作码、指令字、映像布局以** [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md) **为准。** 头文件实现该文,本 README 不另当规范。全工程阶段见 [`doc/初步计划.md`](../doc/初步计划.md) 12.0~12.1。 @@ -34,11 +34,11 @@ isa/ CMakeLists.txt README.md include/isa/ - Types.hpp # 宽度、饱和、槽值 - Op.hpp # 操作码枚举 + 助记符声明 - Instr.hpp # 32-bit 打包/拆字段(inline) - Encode.hpp # encode / decode / disasm - Image.hpp # 头、段、FNV、读写缓冲 + Types.h # 宽度、饱和、槽值 + Op.h # 操作码枚举 + 助记符 + Instr.h # 32-bit 打包/拆字段(inline) + Encode.h # encode / decode / disasm + Image.h # 头、段、FNV、读写缓冲 src/ Encode.cpp Image.cpp @@ -55,13 +55,13 @@ target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) ### 0. CMake 骨架 -根 `CMakeLists.txt` 已 `add_subdirectory` 了 `compiler` / `vm` / `host`。这三个目录若没有 `CMakeLists.txt`,顶层 configure 失败,`isa` 也编不过。 +根 `CMakeLists.txt` 已 `add_subdirectory` 了 `compiler` / `vm` / `executor`。这三个目录若没有 `CMakeLists.txt`,顶层 configure 失败,`isa` 也编不过。 本步只让工程能配置、能链,不写业务。 - 写本目录 `CMakeLists.txt`(上面那段即可) - `compiler` / `vm`:空 `.cpp` + `PUBLIC` 链 `isa` -- `host`:`main` 打印 `Stator 0.1`,链 `compiler`、`vm` +- `executor`:`BytecodeExecutor`,`main` 打印版本,链 `vm`、`isa`;`STCompiler` 入口在 `compiler/src/main.cpp`,链 `compiler`、`isa` - 根 CMake:`enable_testing()`(测试文件下一步再加) 完成:`cmake --build` 过。 @@ -86,11 +86,11 @@ target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) --- -### 2. `Types.hpp` +### 2. `Types.h` -`include/isa/Types.hpp`,inline,无对应 `.cpp`。命名空间 `isa`。C++11,不用 designated initializer。 +`include/isa/Types.h`,inline,无对应 `.cpp`。命名空间 `isa`。C++11,不用 designated initializer。 -- `Bool` = `uint8_t`,`Int` = `int16_t`,`Time` = `int32_t`(毫秒) +- `Bool` = `uint8_t`,`Int` = `int16_t`,`Time` = `int64_t`(毫秒;理由见规范文) - `TypeTag`:`Bool` / `Int` / `Time` - `sat_add` / `sat_sub` / `sat_mul` / `sat_div`(规则见规范文) - `pack_bool` / `pack_int` / `pack_time` → 4 字节 cell;对应 unpack @@ -99,18 +99,18 @@ target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) --- -### 3. `Op.hpp` + `Instr.hpp` +### 3. `Op.h` + `Instr.h` -- `Op.hpp`:`enum Op : uint8_t`,取值与规范文一致;声明 `op_mnemonic(Op)`(表可放 `Encode.cpp`) -- `Instr.hpp`:`typedef uint32_t Instr`;`pack`、取 `op`/`rd`/`a`/`b`、`imm16`/`off16`;按形态 `enc_rr` / `enc_rrr` / `enc_imm` / `enc_jmp` / `enc_jc` +- `Op.h`:`enum Op : uint8_t`,取值与规范文一致;`mnemonic(Op)` 助记符(已在头内落地) +- `Instr.h`:`typedef uint32_t Instr`;`pack`、取 `op`/`rd`/`a`/`b`、`imm16`/`off16`;按形态 `enc_rr` / `enc_rrr` / `enc_imm` / `enc_jmp` / `enc_jc` 完成:能打包/拆开一条 32-bit 指令,不依赖 `.cpp`。 --- -### 4. `Encode.hpp` + `Encode.cpp` +### 4. `Encode.h` + `Encode.cpp` -给 host 打印和测试往返: +给 `STCompiler --disasm` 和测试往返打印: - `Decoded { Op op; uint8_t rd, a, b; }` - `Instr encode(Decoded)` / `Decoded decode(Instr)` @@ -122,12 +122,13 @@ target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) --- -### 5. `Image.hpp` + `Image.cpp` +### 5. `Image.h` + `Image.cpp` - `fnv1a64` / `fnv1a64_update` - `ImageView`:只读视图,指向外部缓冲 - `read_image`:校验魔数、版本、头大小、各段不越界、`entry_fn_id` 落在函数表内 - `write_ret_main(cycle_limit, dt_ms, hash)` → `std::vector`:空槽 + `MAIN` + 一条 `RET` +- `.stb` 文件读写与 `.runtime.toml` sidecar(I/O 绑定 var → 槽号 → channel/bit) 小端 `put_*` / `get_*` 只放 `.cpp`,不把 packed struct 当 ABI。本阶段不写完整 `ImageBuilder`(等 codegen)。 diff --git a/tests/README.md b/tests/README.md index 8c563d4..8246209 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,7 +2,7 @@ 自动化用例,由顶层 `enable_testing()` 接入 CTest。 -可以 `add_test` 调用 `host`,或直接链接 `compiler` + `vm`。样例工程数据按「一份 toml + 若干 `.st`」分子目录存放,不要做成 library。 +可以 `add_test` 调 `STCompiler` / `BytecodeExecutor`(编译→执行管线),或直接链接 `compiler` + `vm` 做单元断言。样例工程数据按「一份 toml + 若干 `.st`」分子目录存放,不要做成 library。 ## 第一版要冻的 From eba48f852e7526f1b848c69d77c5001e95b30eb6 Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 14:06:20 +0800 Subject: [PATCH 11/69] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E7=BC=96=E8=A7=A3=E7=A0=81=E4=B8=8E=E5=8F=8D=E6=B1=87=E7=BC=96?= =?UTF-8?q?=EF=BC=8C=E5=86=BB=E7=BB=93=20disasm=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Encode.h/Encode.cpp:encode/decode 往返 + disasm 一行文本 - 指令与映像.md 新增反汇编格式表(9 种形态 + 未知操作码) - Op.h STORE_* 注释对齐 disasm(rs, slot 寄存器在前) --- Doc/isa/指令与映像.md | 19 ++++++++ isa/include/isa/Encode.h | 31 +++++++++++++ isa/include/isa/Op.h | 6 +-- isa/src/Encode.cpp | 96 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 isa/include/isa/Encode.h diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 63ac85a..311ea1b 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -107,6 +107,25 @@ RET `I`/`Q`/`M` 走映像指令,不是通用寄存器。 FB 实例固定布局,字段偏移编译期算死。 +### 反汇编格式(disasm) + +`disasm` 输出一条指令的一行文本,格式冻结: + +| 形态 | 格式 | 示例 | +|---|---|---| +| 二寄存器 | `MOVE rd, rs` / `NOT rd, rs` | `MOVE r1, r2` | +| 三寄存器 | `OP rd, ra, rb`(逻辑 / 算术 / 比较) | `ADD r1, r2, r3` | +| 常量 | `LOADK rd, imm` | `LOADK r0, 3` | +| 无条件跳转 | `JMP off` | `JMP +4` | +| 条件跳转 | `JT rd, off` / `JF rd, off` | `JT r0, -1` | +| I/Q/M/全局 | `LOAD_I rd, slot` 等;STORE 同形 `STORE_Q rs, slot` | `LOAD_I r0, 2` | +| FB 内置 | `CAL_TON instance` 等 | `CAL_TON 0` | +| 调用 | `CALL fn_id` | `CALL 1` | +| 返回 | `RET` | `RET` | +| 未知操作码 | `??? 0x` | — | + +跳转偏移带符号、恒显正负号(`%+d`);寄存器一律 `r`;常量/槽号/`fn_id` 无符号十进制。 + --- ## 映像 diff --git a/isa/include/isa/Encode.h b/isa/include/isa/Encode.h new file mode 100644 index 0000000..ea4d8ad --- /dev/null +++ b/isa/include/isa/Encode.h @@ -0,0 +1,31 @@ +/** + * @file Encode.h + * @brief 指令编解码与反汇编 + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include +#include + +#include "isa/Instr.h" +#include "isa/Op.h" + +namespace isa { + // 解码后的字段视图 + struct Decoded { + Op op; + uint8_t rd; + uint8_t a; + uint8_t b; + }; + + // 编解码往返:encode(decode(w)) == w + Instr encode(Decoded d); + Decoded decode(Instr w); + + // 反汇编一行文本,写入 out,至多 cap 字节(含 '\0')。格式见 Doc/isa/指令与映像.md。 + void disasm(Instr w, char* out, size_t cap); +} diff --git a/isa/include/isa/Op.h b/isa/include/isa/Op.h index 42bc79e..e74b33e 100644 --- a/isa/include/isa/Op.h +++ b/isa/include/isa/Op.h @@ -32,11 +32,11 @@ namespace isa { JT = 16, // JT r, offset JF = 17, // JF r, offset LOAD_I = 18, // LOAD_I rd, slot - STORE_Q = 19, // STORE_Q slot, rs + STORE_Q = 19, // STORE_Q rs, slot LOAD_M = 20, // LOAD_M rd, slot - STORE_M = 21, // STORE_M slot, rs + STORE_M = 21, // STORE_M rs, slot LOAD_GLOBAL = 22, // LOAD_GLOBAL rd, slot - STORE_GLOBAL = 23, // STORE_GLOBAL slot, rs + STORE_GLOBAL = 23, // STORE_GLOBAL rs, slot CAL_TON = 24, // CAL_TON instance_slot CAL_TOF = 25, // CAL_TOF instance_slot CAL_CTU = 26, // CAL_CTU instance_slot diff --git a/isa/src/Encode.cpp b/isa/src/Encode.cpp index e69de29..f6f1391 100644 --- a/isa/src/Encode.cpp +++ b/isa/src/Encode.cpp @@ -0,0 +1,96 @@ +/** + * @file Encode.cpp + * @brief 指令编解码与反汇编 + * @author + * @date 2026-08-19 + */ + +#include "isa/Encode.h" + +#include + +namespace isa { + +Instr encode(Decoded d) { + return pack(d.op, d.rd, d.a, d.b); +} + +Decoded decode(Instr w) { + Decoded d; + d.op = op(w); + d.rd = rd(w); + d.a = a(w); + d.b = b(w); + return d; +} + +void disasm(Instr w, char* out, size_t cap) { + if (cap == 0) { + return; + } + out[0] = '\0'; + + const Op o = op(w); + const int idx = static_cast(o); + if (idx < 0 || idx >= kOpCount) { + snprintf(out, cap, "??? 0x%08x", static_cast(w)); + return; + } + const char* m = mnemonic(o); + + switch (o) { + case Op::MOVE: + case Op::NOT: + snprintf(out, cap, "%s r%u, r%u", m, + static_cast(rd(w)), static_cast(a(w))); + break; + case Op::AND: + case Op::OR: + case Op::ADD: + case Op::SUB: + case Op::MUL: + case Op::DIV: + case Op::CMP_EQ: + case Op::CMP_NE: + case Op::CMP_LT: + case Op::CMP_LE: + case Op::CMP_GT: + case Op::CMP_GE: + snprintf(out, cap, "%s r%u, r%u, r%u", m, + static_cast(rd(w)), static_cast(a(w)), + static_cast(b(w))); + break; + case Op::LOADK: + snprintf(out, cap, "%s r%u, %u", m, + static_cast(rd(w)), static_cast(imm16(w))); + break; + case Op::JMP: + snprintf(out, cap, "%s %+d", m, static_cast(off16(w))); + break; + case Op::JT: + case Op::JF: + snprintf(out, cap, "%s r%u, %+d", m, + static_cast(rd(w)), static_cast(off16(w))); + break; + case Op::LOAD_I: + case Op::STORE_Q: + case Op::LOAD_M: + case Op::STORE_M: + case Op::LOAD_GLOBAL: + case Op::STORE_GLOBAL: + snprintf(out, cap, "%s r%u, %u", m, + static_cast(rd(w)), static_cast(imm16(w))); + break; + case Op::CAL_TON: + case Op::CAL_TOF: + case Op::CAL_CTU: + case Op::CALL: + snprintf(out, cap, "%s %u", m, static_cast(imm16(w))); + break; + case Op::RET: + snprintf(out, cap, "%s", m); + break; + } +} + +} // namespace isa From c378411637532167ab0f4ad67bf8872a733fb9bf Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 14:40:38 +0800 Subject: [PATCH 12/69] =?UTF-8?q?=E8=90=BD=E5=9C=B0=E6=98=A0=E5=83=8F?= =?UTF-8?q?=E8=AF=BB=E5=86=99=EF=BC=9A72=20=E5=AD=97=E8=8A=82=E5=A4=B4?= =?UTF-8?q?=E3=80=81=E6=AE=B5=E5=B8=83=E5=B1=80=E3=80=81FNV-1a=E3=80=81.st?= =?UTF-8?q?b=20=E4=B8=8E=20sidecar=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 指令与映像.md 冻结:头字段表、常量表/函数表行、段序、数据段对齐、sidecar 格式 - Types.h 补 TypeTag;Image.h/cpp:ImageView 校验、write_ret_main、文件与 sidecar - isa CMake 改 target_include_directories PUBLIC --- Doc/isa/指令与映像.md | 64 +++++- {host => executor}/CMakeLists.txt | 0 {host => executor}/README.md | 0 {host => executor}/src/main.cpp | 0 isa/CMakeLists.txt | 7 +- isa/include/isa/Image.h | 123 ++++++++++++ isa/include/isa/Types.h | 7 + isa/src/Image.cpp | 321 ++++++++++++++++++++++++++++++ 8 files changed, 509 insertions(+), 13 deletions(-) rename {host => executor}/CMakeLists.txt (100%) rename {host => executor}/README.md (100%) rename {host => executor}/src/main.cpp (100%) create mode 100644 isa/include/isa/Image.h create mode 100644 isa/src/Image.cpp diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 311ea1b..a36fb46 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -130,19 +130,51 @@ FB 实例固定布局,字段偏移编译期算死。 ## 映像 -魔数 `STSC`,版本 `1`。 +魔数 `STSC`,版本 `1`。全部整数小端。文件 = 头 + 五个段,段序:常量表 → 函数表 → 字节码 → FB 布局 → 数据。偏移记在头里,段必须连续、偏移单调不减,末段终点不超过文件长度。 -头: +### 头(72 字节) -- `cycle_limit`、`dt_ms` -- 工程哈希 -- 全局槽数、I/Q/M 槽数 -- 常量表 / 函数表 / 字节码 / FB 布局的偏移 -- 入口 `MAIN` 的 `fn_id` +| 偏移 | 宽 | 字段 | +|---|---|---| +| 0 | 4 | 魔数 `STSC` | +| 4 | 4 | 版本 `1` | +| 8 | 4 | `cycle_limit` | +| 12 | 4 | `dt_ms` | +| 16 | 8 | 工程哈希(FNV-1a 64) | +| 24 | 4 | 入口 `entry_fn_id` | +| 28 | 4 | 全局槽数 `n_globals` | +| 32 | 4 | I 槽数 `n_i` | +| 36 | 4 | Q 槽数 `n_q` | +| 40 | 4 | M 槽数 `n_m` | +| 44 | 4 | 常量数 `n_consts` | +| 48 | 4 | 函数数 `n_funcs` | +| 52 | 4 | `offset_const` | +| 56 | 4 | `offset_funcs` | +| 60 | 4 | `offset_code` | +| 64 | 4 | `offset_fb` | +| 68 | 4 | `offset_data` | -函数表一行:`fn_id`、`nregs`、字节码偏移与长度。 +### 常量表一行(12 字节) -工程哈希:收录源文件路径排序后对内容做 **FNV-1a 64**,只保证回放槽位一致,不是密码学哈希。 +`[tag:u32][value:u64]`;`tag`:0=BOOL、1=INT、2=TIME。`value` 按 tag 解释,未用高位为 0。 + +### 函数表一行(12 字节) + +`[nregs:u32][code_offset:u32][code_len:u32]`;行下标即 `fn_id`,`code_offset` 相对字节码段起点,`code_len` 为指令条数。 + +### 字节码段 + +指令按 `u32` 连续排;`CALL` 的 `fn_id` 是函数表行下标。 + +### FB 布局段 + +第一版为空;每类型一行 `[field_count:u32][field_count × (tag:u32, offset:u32)]`,offset 相对实例基址,12.8 落地 codegen 时冻精确表。 + +### 数据段 + +槽初值按序排:全局(`n_globals`)→ I → Q → M。槽宽与对齐:`BOOL` 1 字节 / `INT` 2 字节 / `TIME` 8 字节,每个槽起点对齐到自身宽度(需要时插 padding)。 + +工程哈希:收录源文件路径排序后对内容做 **FNV-1a 64**,只保证回放槽位一致,不是密码学哈希。FNV-1a 64:basis `0xcbf29ce484222325`,prime `0x100000001b3`。 `[[io.input]]` / `[[io.output]]` 不进映像。 @@ -153,7 +185,19 @@ FB 实例固定布局,字段偏移编译期算死。 编译产物是单文件 `.stb`:映像头 + 各段按本文布局连续排,魔数 `STSC`、版本 `1`、小端。`STCompiler` 写,`BytecodeExecutor` 读。 - `dt_ms` / `cycle_limit` 在映像头,执行器只认映像,不重复配置。 -- I/O 绑定不进映像:`STCompiler` 另写 sidecar `.runtime.toml`,内容是 `var`(已解析槽号)→ `channel` / `bit`;`BytecodeExecutor` 读 sidecar 做采样与写回,不创造变量。 +- I/O 绑定不进映像:`STCompiler` 另写 sidecar `.runtime.toml`,`BytecodeExecutor` 读 sidecar 做采样与写回,不创造变量。 + +sidecar 格式(TOML 子集,冻结): + +```toml +[[io.input]] # 或 [[io.output]] +var = "I0_0" # 已在 GVL 声明的名字 +slot = 1 # 全局槽号(编译期已解析) +channel = 0 +bit = 0 +``` + +每行一个绑定;`var` 顺序不要求与映像槽序一致(`slot` 为准)。 --- diff --git a/host/CMakeLists.txt b/executor/CMakeLists.txt similarity index 100% rename from host/CMakeLists.txt rename to executor/CMakeLists.txt diff --git a/host/README.md b/executor/README.md similarity index 100% rename from host/README.md rename to executor/README.md diff --git a/host/src/main.cpp b/executor/src/main.cpp similarity index 100% rename from host/src/main.cpp rename to executor/src/main.cpp diff --git a/isa/CMakeLists.txt b/isa/CMakeLists.txt index 22cae7c..2f42c49 100644 --- a/isa/CMakeLists.txt +++ b/isa/CMakeLists.txt @@ -10,7 +10,8 @@ project(ISA set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 11) -include_directories(./include) - add_library(isa STATIC - ./src/Encode.cpp) \ No newline at end of file + ./src/Encode.cpp + ./src/Image.cpp) + +target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) \ No newline at end of file diff --git a/isa/include/isa/Image.h b/isa/include/isa/Image.h new file mode 100644 index 0000000..d1f1289 --- /dev/null +++ b/isa/include/isa/Image.h @@ -0,0 +1,123 @@ +/** + * @file Image.h + * @brief 映像头、段、FNV-1a 哈希、.stb 文件与 sidecar + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include +#include +#include +#include + +#include "isa/Types.h" + +namespace isa { + + // 魔数 "STSC"(小端 u32)、版本 + static const uint32_t kMagic = 0x43545353u; + static const uint32_t kVersion = 1; + + // 头与表行宽(见 Doc/isa/指令与映像.md) + static const size_t kHeaderSize = 72; + static const size_t kConstEntrySize = 12; + static const size_t kFuncRowSize = 12; + + // FNV-1a 64:basis / prime(见 Doc/isa/指令与映像.md) + static const uint64_t kFnvBasis = 0xcbf29ce484222325ull; + static const uint64_t kFnvPrime = 0x100000001b3ull; + + // FNV-1a 64 增量与一次性 + uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len); + uint64_t fnv1a64(const uint8_t* data, size_t len); + + // 映像头(字节布局见 Doc/isa/指令与映像.md) + struct ImageHeader { + uint32_t cycle_limit; + uint32_t dt_ms; + uint64_t project_hash; + uint32_t entry_fn_id; + uint32_t n_globals; + uint32_t n_i; + uint32_t n_q; + uint32_t n_m; + uint32_t n_consts; + uint32_t n_funcs; + uint32_t offset_const; + uint32_t offset_funcs; + uint32_t offset_code; + uint32_t offset_fb; + uint32_t offset_data; + }; + + // 常量表一行 + struct ConstEntry { + types::TypeTag tag; + uint64_t value; + }; + + // 函数表一行 + struct FuncRow { + uint32_t nregs; + uint32_t code_offset; // 相对字节码段起点 + uint32_t code_len; // 指令条数 + }; + + // I/O 绑定(sidecar 一行) + struct IoBinding { + std::string var; + uint32_t slot; + uint32_t channel; + uint32_t bit; + bool is_input; // true = [[io.input]],false = [[io.output]] + }; + + // 只读视图:校验过的映像 + class ImageView { + public: + static ImageView from(const uint8_t* buf, size_t len); + static ImageView from(const std::vector& buf); + + bool ok() const; + const std::string& error() const; + const ImageHeader& header() const; + + const uint8_t* raw() const; + size_t raw_len() const; + + ConstEntry const_entry(size_t i) const; + FuncRow func_row(size_t i) const; + + const uint8_t* code_bytes() const; // 字节码段起点 + size_t code_len() const; // 字节数 + const uint8_t* data_bytes() const; // 数据段起点 + size_t data_len() const; // 字节数 + + private: + ImageView(); + + const uint8_t* buf_; + size_t len_; + bool ok_; + std::string err_; + ImageHeader hdr_; + }; + + // 写最小映像:空槽 + MAIN(fn_id=0)+ 一条 RET + std::vector write_ret_main(uint32_t cycle_limit, uint32_t dt_ms, + uint64_t project_hash); + + // .stb 文件读写(纯字节,校验交给 read_image / ImageView) + bool write_stb_file(const char* path, const std::vector& image, + std::string* err); + bool read_stb_file(const char* path, std::vector* image, + std::string* err); + + // sidecar 生成与写文件(格式见 Doc/isa/指令与映像.md) + std::string make_sidecar(const std::vector& bindings); + bool write_sidecar_file(const char* path, + const std::vector& bindings, + std::string* err); +} diff --git a/isa/include/isa/Types.h b/isa/include/isa/Types.h index fd74a9f..2fa1efb 100644 --- a/isa/include/isa/Types.h +++ b/isa/include/isa/Types.h @@ -20,6 +20,13 @@ namespace isa { // 差值可负、为 DATE_AND_TIME 与 64 位定时器预留。槽位按 8 字节对齐。 using TIME = int64_t; + // 槽 / 常量表类型标记,数值冻结:0=BOOL、1=INT、2=TIME(见映像规范) + enum TypeTag : uint8_t { + Bool = 0, + Int = 1, + Time = 2, + }; + // 饱和算术:夹到 INT 最小/最大,编译期与 VM 共用同一规则。 inline INT sat_add(INT a, INT b) { diff --git a/isa/src/Image.cpp b/isa/src/Image.cpp new file mode 100644 index 0000000..1d2fd7b --- /dev/null +++ b/isa/src/Image.cpp @@ -0,0 +1,321 @@ +/** + * @file Image.cpp + * @brief 映像头、段、FNV-1a 哈希、.stb 文件与 sidecar + * @author + * @date 2026-08-19 + */ + +#include "isa/Image.h" + +#include +#include + +#include "isa/Instr.h" + +namespace isa { + +// ---- 小端读写(只放 .cpp,不把 packed struct 当 ABI)---- + +static void put_le32(std::vector& b, size_t off, uint32_t v) { + b[off + 0] = static_cast(v & 0xFFu); + b[off + 1] = static_cast((v >> 8) & 0xFFu); + b[off + 2] = static_cast((v >> 16) & 0xFFu); + b[off + 3] = static_cast((v >> 24) & 0xFFu); +} + +static uint32_t get_le32(const uint8_t* p) { + return static_cast(p[0]) + | (static_cast(p[1]) << 8) + | (static_cast(p[2]) << 16) + | (static_cast(p[3]) << 24); +} + +static void put_le64(std::vector& b, size_t off, uint64_t v) { + for (int i = 0; i < 8; ++i) { + b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); + } +} + +static uint64_t get_le64(const uint8_t* p) { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) { + v |= static_cast(p[i]) << (8 * i); + } + return v; +} + +// ---- FNV-1a 64 ---- + +uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + h ^= data[i]; + h *= kFnvPrime; + } + return h; +} + +uint64_t fnv1a64(const uint8_t* data, size_t len) { + return fnv1a64_update(kFnvBasis, data, len); +} + +// ---- 最小映像:空槽 + MAIN + 一条 RET ---- + +std::vector write_ret_main(uint32_t cycle_limit, uint32_t dt_ms, + uint64_t project_hash) { + std::vector b(kHeaderSize + kFuncRowSize + 4, 0); + + put_le32(b, 0, kMagic); + put_le32(b, 4, kVersion); + put_le32(b, 8, cycle_limit); + put_le32(b, 12, dt_ms); + put_le64(b, 16, project_hash); + put_le32(b, 24, 0); // entry_fn_id = MAIN = 0 + // n_globals / n_i / n_q / n_m = 0 + // n_consts = 0 + put_le32(b, 48, 1); // n_funcs = 1 + + const size_t off_funcs = kHeaderSize; // 72 + const size_t off_code = off_funcs + kFuncRowSize; // 84 + const size_t off_end = off_code + 4; // 88 + + put_le32(b, 52, static_cast(off_funcs)); // offset_const + put_le32(b, 56, static_cast(off_funcs)); // offset_funcs + put_le32(b, 60, static_cast(off_code)); // offset_code + put_le32(b, 64, static_cast(off_end)); // offset_fb + put_le32(b, 68, static_cast(off_end)); // offset_data + + // 函数表一行:MAIN,无寄存器,代码 0 起 1 条 + put_le32(b, off_funcs + 0, 0); // nregs + put_le32(b, off_funcs + 4, 0); // code_offset + put_le32(b, off_funcs + 8, 1); // code_len + + // 字节码:RET + put_le32(b, off_code, pack(Op::RET, 0, 0, 0)); + + return b; +} + +// ---- 只读视图 ---- + +ImageView::ImageView() + : buf_(0), len_(0), ok_(false), err_("uninitialized"), hdr_() {} + +ImageView ImageView::from(const uint8_t* buf, size_t len) { + ImageView v; + v.buf_ = buf; + v.len_ = len; + + if (buf == 0) { + v.err_ = "null buffer"; + return v; + } + if (len < kHeaderSize) { + v.err_ = "image too short"; + return v; + } + if (get_le32(buf + 0) != kMagic) { + v.err_ = "bad magic"; + return v; + } + if (get_le32(buf + 4) != kVersion) { + v.err_ = "bad version"; + return v; + } + + ImageHeader& h = v.hdr_; + h.cycle_limit = get_le32(buf + 8); + h.dt_ms = get_le32(buf + 12); + h.project_hash = get_le64(buf + 16); + h.entry_fn_id = get_le32(buf + 24); + h.n_globals = get_le32(buf + 28); + h.n_i = get_le32(buf + 32); + h.n_q = get_le32(buf + 36); + h.n_m = get_le32(buf + 40); + h.n_consts = get_le32(buf + 44); + h.n_funcs = get_le32(buf + 48); + h.offset_const = get_le32(buf + 52); + h.offset_funcs = get_le32(buf + 56); + h.offset_code = get_le32(buf + 60); + h.offset_fb = get_le32(buf + 64); + h.offset_data = get_le32(buf + 68); + + // 段校验:连续、单调不减、末段终点不越界 + const uint64_t offs[5] = { + h.offset_const, h.offset_funcs, h.offset_code, h.offset_fb, h.offset_data, + }; + for (int i = 0; i < 5; ++i) { + if (offs[i] < kHeaderSize || offs[i] > len) { + v.err_ = "segment offset out of range"; + return v; + } + if (i > 0 && offs[i] < offs[i - 1]) { + v.err_ = "segment offsets not monotonic"; + return v; + } + } + + const uint64_t const_len = h.offset_funcs - h.offset_const; + const uint64_t funcs_len = h.offset_code - h.offset_funcs; + if (const_len != static_cast(h.n_consts) * kConstEntrySize) { + v.err_ = "const table size mismatch"; + return v; + } + if (funcs_len != static_cast(h.n_funcs) * kFuncRowSize) { + v.err_ = "function table size mismatch"; + return v; + } + if ((h.offset_fb - h.offset_code) % 4 != 0) { + v.err_ = "code segment not 4-byte aligned"; + return v; + } + if (h.entry_fn_id >= h.n_funcs && h.n_funcs != 0) { + v.err_ = "entry fn_id out of range"; + return v; + } + + v.ok_ = true; + v.err_.clear(); + return v; +} + +ImageView ImageView::from(const std::vector& buf) { + return from(buf.data(), buf.size()); +} + +bool ImageView::ok() const { return ok_; } +const std::string& ImageView::error() const { return err_; } +const ImageHeader& ImageView::header() const { return hdr_; } +const uint8_t* ImageView::raw() const { return buf_; } +size_t ImageView::raw_len() const { return len_; } + +ConstEntry ImageView::const_entry(size_t i) const { + ConstEntry e; + e.tag = types::Bool; + e.value = 0; + if (ok_ && i < hdr_.n_consts) { + const uint8_t* p = buf_ + hdr_.offset_const + i * kConstEntrySize; + const uint32_t tag = get_le32(p); + if (tag <= static_cast(types::Time)) { + e.tag = static_cast(tag); + } + e.value = get_le64(p + 4); + } + return e; +} + +FuncRow ImageView::func_row(size_t i) const { + FuncRow r = {0, 0, 0}; + if (ok_ && i < hdr_.n_funcs) { + const uint8_t* p = buf_ + hdr_.offset_funcs + i * kFuncRowSize; + r.nregs = get_le32(p + 0); + r.code_offset = get_le32(p + 4); + r.code_len = get_le32(p + 8); + } + return r; +} + +const uint8_t* ImageView::code_bytes() const { + return ok_ ? buf_ + hdr_.offset_code : 0; +} + +size_t ImageView::code_len() const { + return ok_ ? hdr_.offset_fb - hdr_.offset_code : 0; +} + +const uint8_t* ImageView::data_bytes() const { + return ok_ ? buf_ + hdr_.offset_data : 0; +} + +size_t ImageView::data_len() const { + return ok_ ? len_ - hdr_.offset_data : 0; +} + +// ---- .stb 文件读写 ---- + +bool write_stb_file(const char* path, const std::vector& image, + std::string* err) { + FILE* f = std::fopen(path, "wb"); + if (f == 0) { + if (err) { + *err = std::string("cannot open for write: ") + path; + } + return false; + } + const bool ok = image.empty() || std::fwrite(&image[0], 1, image.size(), f) == image.size(); + std::fclose(f); + if (!ok && err) { + *err = std::string("write failed: ") + path; + } + return ok; +} + +bool read_stb_file(const char* path, std::vector* image, + std::string* err) { + FILE* f = std::fopen(path, "rb"); + if (f == 0) { + if (err) { + *err = std::string("cannot open for read: ") + path; + } + return false; + } + std::fseek(f, 0, SEEK_END); + const long size = std::ftell(f); + std::fseek(f, 0, SEEK_SET); + if (size < 0) { + std::fclose(f); + if (err) { + *err = "tell failed"; + } + return false; + } + image->resize(static_cast(size)); + const bool ok = size == 0 || std::fread(&(*image)[0], 1, static_cast(size), f) == static_cast(size); + std::fclose(f); + if (!ok && err) { + *err = std::string("read failed: ") + path; + } + return ok; +} + +// ---- sidecar ---- + +std::string make_sidecar(const std::vector& bindings) { + std::string out; + for (size_t i = 0; i < bindings.size(); ++i) { + const IoBinding& b = bindings[i]; + char buf[64]; + out += b.is_input ? "[[io.input]]\n" : "[[io.output]]\n"; + out += "var = \""; + out += b.var; + out += "\"\n"; + std::snprintf(buf, sizeof buf, "slot = %u\n", static_cast(b.slot)); + out += buf; + std::snprintf(buf, sizeof buf, "channel = %u\n", static_cast(b.channel)); + out += buf; + std::snprintf(buf, sizeof buf, "bit = %u\n", static_cast(b.bit)); + out += buf; + out += "\n"; + } + return out; +} + +bool write_sidecar_file(const char* path, + const std::vector& bindings, + std::string* err) { + FILE* f = std::fopen(path, "wb"); + if (f == 0) { + if (err) { + *err = std::string("cannot open for write: ") + path; + } + return false; + } + const std::string s = make_sidecar(bindings); + const bool ok = s.empty() || std::fwrite(s.data(), 1, s.size(), f) == s.size(); + std::fclose(f); + if (!ok && err) { + *err = std::string("write failed: ") + path; + } + return ok; +} + +} // namespace isa From dd7f7bcba7a31269abe79638e98acf3a262aa61a Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 14:40:44 +0800 Subject: [PATCH 13/69] =?UTF-8?q?=E5=8F=8C=E5=8F=AF=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=EF=BC=9ASTCompiler=20=E4=B8=8E=20BytecodeExe?= =?UTF-8?q?cutor=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STCompiler 入口在 compiler/src/main.cpp,链 compiler+isa - host/ 改名 executor/,BytecodeExecutor 链 vm+isa 不链 compiler - compiler/vm 补 PUBLIC 链 isa;顶层加入 executor - CTest:版本冒烟 2 例 + isa_roundtrip(89 断言),3/3 通过 --- CMakeLists.txt | 2 +- compiler/CMakeLists.txt | 11 +- compiler/src/main.cpp | 15 +++ executor/CMakeLists.txt | 9 +- executor/README.md | 6 +- executor/src/main.cpp | 5 +- tests/CMakeLists.txt | 17 ++- tests/src/isa_test.cpp | 236 ++++++++++++++++++++++++++++++++++++++++ vm/CMakeLists.txt | 5 +- 9 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 compiler/src/main.cpp create mode 100644 tests/src/isa_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d813b6..ff69001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ include("${CMAKE_SOURCE_DIR}/cmake/Standards.cmake") add_subdirectory(isa) add_subdirectory(compiler) add_subdirectory(vm) -add_subdirectory(host) +add_subdirectory(executor) # 自动化用例 enable_testing() diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index fb16c4f..c09970c 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -6,8 +6,15 @@ project(Compiler VERSION 0.1 DESCRIPTION "ST / toml 编译器") -include_directories(./include) - # 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 add_library(compiler STATIC ./src/Lexer.cpp) + +target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_link_libraries(compiler PUBLIC isa) + +# 可执行入口:STCompiler -o .stb +add_executable(STCompiler + ./src/main.cpp) + +target_link_libraries(STCompiler PRIVATE compiler) diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp new file mode 100644 index 0000000..222bc03 --- /dev/null +++ b/compiler/src/main.cpp @@ -0,0 +1,15 @@ +/** + * @file main.cpp + * @brief STCompiler 可执行入口 + * @author + * @date 2026-08-19 + */ + +#include + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + std::printf("STCompiler 0.1\n"); + return 0; +} diff --git a/executor/CMakeLists.txt b/executor/CMakeLists.txt index c7f1fa0..0395530 100644 --- a/executor/CMakeLists.txt +++ b/executor/CMakeLists.txt @@ -2,9 +2,12 @@ cmake_minimum_required(VERSION 3.21) # 工程名、版本号、工程描述 -project(Host +project(Executor VERSION 0.1 - DESCRIPTION "可执行入口:编译并跑扫描周期") + DESCRIPTION "字节码执行器:加载 .stb + sidecar 跑扫描周期") -add_executable(host +add_executable(BytecodeExecutor ./src/main.cpp) + +# 只链 vm + isa,不链 compiler +target_link_libraries(BytecodeExecutor PRIVATE vm) diff --git a/executor/README.md b/executor/README.md index 8ffe590..ca9eb17 100644 --- a/executor/README.md +++ b/executor/README.md @@ -1,5 +1,5 @@ -# host +# executor -可执行入口。CMake 目标:`host`(`EXECUTABLE`),链接 `compiler` 和 `vm`。 +BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`(`EXECUTABLE`),链接 `vm` 和 `isa`,**不链 `compiler`**。 -职责与边界见 [`doc/host/宿主入口.md`](../doc/host/宿主入口.md)。 +职责与边界见 [`doc/executor/执行器入口.md`](../doc/executor/执行器入口.md)。 diff --git a/executor/src/main.cpp b/executor/src/main.cpp index 5a1330c..b1a1b6d 100644 --- a/executor/src/main.cpp +++ b/executor/src/main.cpp @@ -1,6 +1,7 @@ /** * @file main.cpp - * @brief host 可执行入口 + * @brief BytecodeExecutor 可执行入口 + * @author * @date 2026-08-19 */ @@ -9,6 +10,6 @@ int main(int argc, char** argv) { (void)argc; (void)argv; - std::printf("Stator host 0.1\n"); + std::printf("BytecodeExecutor 0.1\n"); return 0; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8129641..44ab0de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,14 @@ -# 自动化用例:先放一个必过的空测试,验证 CTest 接线 -add_test(NAME host_version - COMMAND host) +# 自动化用例 +add_test(NAME stcompiler_version + COMMAND STCompiler) +add_test(NAME bytecode_executor_version + COMMAND BytecodeExecutor) + +# isa 合同测试:链接 isa 库,断言覆盖饱和/编解码/disasm/哈希/映像/sidecar +add_executable(isa_test + ./src/isa_test.cpp) + +target_link_libraries(isa_test PRIVATE isa) + +add_test(NAME isa_roundtrip + COMMAND isa_test) diff --git a/tests/src/isa_test.cpp b/tests/src/isa_test.cpp new file mode 100644 index 0000000..ce95a3b --- /dev/null +++ b/tests/src/isa_test.cpp @@ -0,0 +1,236 @@ +/** + * @file isa_test.cpp + * @brief isa 合同测试:饱和、编解码、disasm、哈希、映像、sidecar + * @author + * @date 2026-08-19 + */ + +#include +#include +#include +#include +#include + +#include "isa/Encode.h" +#include "isa/Image.h" +#include "isa/Instr.h" +#include "isa/Op.h" +#include "isa/Types.h" + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 1. 饱和算术 ---- + +static bool test_sat() { + using namespace isa::types; + CHECK(sat_add(30000, 30000) == 32767); + CHECK(sat_add(-30000, -30000) == -32768); + CHECK(sat_add(10, 20) == 30); + CHECK(sat_sub(-30000, 30000) == -32768); + CHECK(sat_sub(5, 3) == 2); + CHECK(sat_mul(300, 200) == 32767); + CHECK(sat_mul(-300, 200) == -32768); + CHECK(sat_mul(6, 7) == 42); + CHECK(sat_div(10, 3) == 3); + CHECK(sat_div(-32768, -1) == 32767); // INT_MIN / -1 饱和 + CHECK(sat_div(7, 0) == 7); // 除零返回被除数 + CHECK(static_cast(TypeTag::Bool) == 0); + CHECK(static_cast(TypeTag::Int) == 1); + CHECK(static_cast(TypeTag::Time) == 2); + return true; +} + +// ---- 2. 操作码与助记符 ---- + +static bool test_op() { + using namespace isa; + CHECK(static_cast(Op::MOVE) == 0); + CHECK(static_cast(Op::RET) == 28); + CHECK(kOpCount == 29); + CHECK(std::strcmp(mnemonic(Op::CAL_CTU), "CAL_CTU") == 0); + CHECK(std::strcmp(mnemonic(static_cast(255)), "???") == 0); + return true; +} + +// ---- 3. 指令打包 / 拆字段 ---- + +static bool test_instr() { + using namespace isa; + const Instr w = pack(Op::ADD, 5, 3, 4); + CHECK(op(w) == Op::ADD); + CHECK(rd(w) == 5); + CHECK(a(w) == 3); + CHECK(b(w) == 4); + CHECK(imm16(enc_imm(Op::LOADK, 0, 0x1234)) == 0x1234); + CHECK(off16(enc_jmp(-1)) == -1); + CHECK(off16(enc_jmp(4)) == 4); + CHECK(rd(enc_jc(Op::JT, 2, -3)) == 2); + CHECK(off16(enc_jc(Op::JT, 2, -3)) == -3); + CHECK(imm16(enc_call(0xABCD)) == 0xABCD); + CHECK(imm16(enc_slot(Op::LOAD_I, 7, 300)) == 300); + CHECK(rd(enc_slot(Op::LOAD_I, 7, 300)) == 7); + CHECK(rd(enc_rr(Op::MOVE, 1, 2)) == 1); + CHECK(a(enc_rr(Op::MOVE, 1, 2)) == 2); + CHECK(op(enc_ret()) == Op::RET); + return true; +} + +// ---- 4. encode / decode / disasm ---- + +static bool test_encode() { + using namespace isa; + const Instr w = enc_rrr(Op::ADD, 5, 3, 4); + CHECK(encode(decode(w)) == w); + CHECK(decode(w).op == Op::ADD); + CHECK(decode(w).rd == 5); + CHECK(decode(w).a == 3); + CHECK(decode(w).b == 4); + + char buf[64]; + disasm(w, buf, sizeof buf); + CHECK(std::strcmp(buf, "ADD r5, r3, r4") == 0); + disasm(enc_rr(Op::MOVE, 1, 2), buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r1, r2") == 0); + disasm(enc_imm(Op::LOADK, 0, 3), buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r0, 3") == 0); + disasm(enc_jmp(4), buf, sizeof buf); + CHECK(std::strcmp(buf, "JMP +4") == 0); + disasm(enc_jmp(-1), buf, sizeof buf); + CHECK(std::strcmp(buf, "JMP -1") == 0); + disasm(enc_jc(Op::JT, 0, -1), buf, sizeof buf); + CHECK(std::strcmp(buf, "JT r0, -1") == 0); + disasm(enc_call(1), buf, sizeof buf); + CHECK(std::strcmp(buf, "CALL 1") == 0); + disasm(enc_slot(Op::LOAD_I, 0, 2), buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_I r0, 2") == 0); + disasm(enc_slot(Op::STORE_Q, 4, 0), buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_Q r4, 0") == 0); + disasm(enc_ret(), buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + disasm(enc_imm(Op::CAL_TON, 0, 0), buf, sizeof buf); + CHECK(std::strcmp(buf, "CAL_TON 0") == 0); + disasm(0xFFu, buf, sizeof buf); // 未知操作码 + CHECK(std::strcmp(buf, "??? 0x000000ff") == 0); + return true; +} + +// ---- 5. FNV-1a 64 ---- + +static bool test_fnv() { + using namespace isa; + CHECK(fnv1a64(0, 0) == kFnvBasis); + const uint8_t a1[] = {'a'}; + CHECK(fnv1a64(a1, 1) == 0xaf63dc4c8601ec8cull); + return true; +} + +// ---- 6. 映像:write_ret_main → ImageView ---- + +static bool test_image() { + using namespace isa; + const uint64_t hash = 0x123456789abcdef0ull; + std::vector img = write_ret_main(100000, 10, hash); + + ImageView v = ImageView::from(img); + CHECK(v.ok()); + CHECK(v.error().empty()); + CHECK(v.header().cycle_limit == 100000); + CHECK(v.header().dt_ms == 10); + CHECK(v.header().project_hash == hash); + CHECK(v.header().entry_fn_id == 0); + CHECK(v.header().n_funcs == 1); + CHECK(v.header().n_globals == 0); + CHECK(v.header().n_i == 0); + CHECK(v.header().n_q == 0); + CHECK(v.header().n_m == 0); + CHECK(v.header().offset_const == kHeaderSize); + CHECK(v.header().offset_data == kHeaderSize + kFuncRowSize + 4); + CHECK(v.code_len() == 4); + CHECK(v.data_len() == 0); + + const FuncRow r = v.func_row(0); + CHECK(r.nregs == 0); + CHECK(r.code_offset == 0); + CHECK(r.code_len == 1); + + char buf[32]; + disasm(v.code_bytes()[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + + // 损坏映像必须拒绝 + std::vector bad = img; + bad[0] = 'X'; + CHECK(!ImageView::from(bad).ok()); + std::vector bad2 = img; + bad2[60] = 255; // offset_code 越界 + CHECK(!ImageView::from(bad2).ok()); + std::vector bad3 = img; + bad3[48] = 2; // n_funcs=2 但表只有 1 行 + CHECK(!ImageView::from(bad3).ok()); + std::vector bad4 = img; + bad4[24] = 1; // entry_fn_id 越界 + CHECK(!ImageView::from(bad4).ok()); + std::vector short_img = img; + short_img.resize(kHeaderSize - 1); + CHECK(!ImageView::from(short_img).ok()); + return true; +} + +// ---- 7. .stb 文件与 sidecar ---- + +static bool test_files() { + using namespace isa; + std::vector img = write_ret_main(100000, 10, 0); + + const char* stb_path = "isa_test_tmp.stb"; + std::string err; + CHECK(write_stb_file(stb_path, img, &err)); + std::vector back; + CHECK(read_stb_file(stb_path, &back, &err)); + CHECK(back == img); + CHECK(ImageView::from(back).ok()); + std::remove(stb_path); + + std::vector bs; + IoBinding b1; + b1.var = "I0_0"; b1.slot = 1; b1.channel = 0; b1.bit = 0; b1.is_input = true; + IoBinding b2; + b2.var = "Q0_0"; b2.slot = 3; b2.channel = 0; b2.bit = 0; b2.is_input = false; + bs.push_back(b1); + bs.push_back(b2); + + const std::string sc = make_sidecar(bs); + CHECK(sc.find("[[io.input]]") == 0); + CHECK(sc.find("var = \"I0_0\"") != std::string::npos); + CHECK(sc.find("slot = 1") != std::string::npos); + CHECK(sc.find("channel = 0") != std::string::npos); + CHECK(sc.find("[[io.output]]") != std::string::npos); + CHECK(sc.find("var = \"Q0_0\"") != std::string::npos); + CHECK(sc.find("slot = 3") != std::string::npos); + + const char* sc_path = "isa_test_tmp.runtime.toml"; + CHECK(write_sidecar_file(sc_path, bs, &err)); + std::remove(sc_path); + return true; +} + +int main() { + if (!test_sat()) return 1; + if (!test_op()) return 1; + if (!test_instr()) return 1; + if (!test_encode()) return 1; + if (!test_fnv()) return 1; + if (!test_image()) return 1; + if (!test_files()) return 1; + std::printf("isa_test: %d checks passed\n", g_checks); + return 0; +} diff --git a/vm/CMakeLists.txt b/vm/CMakeLists.txt index 9ffb15f..6f39576 100644 --- a/vm/CMakeLists.txt +++ b/vm/CMakeLists.txt @@ -6,7 +6,8 @@ project(VM VERSION 0.1 DESCRIPTION "寄存器虚拟机") -include_directories(./include) - add_library(vm STATIC ./src/Machine.cpp) + +target_include_directories(vm PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_link_libraries(vm PUBLIC isa) From ef94ab1987db8511a98661a4dd58eac6a7d960fd Mon Sep 17 00:00:00 2001 From: chentianya Date: Wed, 19 Aug 2026 14:43:47 +0800 Subject: [PATCH 14/69] =?UTF-8?q?=E8=90=BD=E5=9C=B0=2012.2=20=E4=BA=8C?= =?UTF-8?q?=E5=8D=81=E4=B8=AA=E7=94=A8=E4=BE=8B=E7=9B=AE=E5=BD=95=EF=BC=9A?= =?UTF-8?q?toml=20+=20.st=20+=20=E6=9C=9F=E6=9C=9B=E8=A1=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/cases/ 01~20:13 正例 / 6 负例 / 1 运行时故障 - 负例冻结报错类别:非 GVL 写全局、同名全局、io.var 未声明、FUNCTION 写全局、未声明实例、循环调用 - tests/README 补充用例清单与点亮节奏 --- tests/README.md | 10 +++++++ tests/cases/01_empty_main/EXPECTED.md | 4 +++ tests/cases/01_empty_main/main.st | 2 ++ tests/cases/01_empty_main/project.toml | 8 ++++++ tests/cases/02_bool_assign/EXPECTED.md | 4 +++ tests/cases/02_bool_assign/main.st | 8 ++++++ tests/cases/02_bool_assign/project.toml | 8 ++++++ tests/cases/03_short_circuit/EXPECTED.md | 4 +++ tests/cases/03_short_circuit/main.st | 9 ++++++ tests/cases/03_short_circuit/project.toml | 8 ++++++ tests/cases/04_if_elsif_else/EXPECTED.md | 4 +++ tests/cases/04_if_elsif_else/main.st | 13 +++++++++ tests/cases/04_if_elsif_else/project.toml | 8 ++++++ tests/cases/05_while_normal/EXPECTED.md | 3 ++ tests/cases/05_while_normal/main.st | 9 ++++++ tests/cases/05_while_normal/project.toml | 8 ++++++ tests/cases/06_while_cycle_limit/EXPECTED.md | 4 +++ tests/cases/06_while_cycle_limit/main.st | 4 +++ tests/cases/06_while_cycle_limit/project.toml | 8 ++++++ tests/cases/07_int_arith/EXPECTED.md | 4 +++ tests/cases/07_int_arith/main.st | 10 +++++++ tests/cases/07_int_arith/project.toml | 8 ++++++ tests/cases/08_time_literal/EXPECTED.md | 4 +++ tests/cases/08_time_literal/main.st | 7 +++++ tests/cases/08_time_literal/project.toml | 8 ++++++ tests/cases/09_gvl_external/EXPECTED.md | 4 +++ tests/cases/09_gvl_external/globals.st | 3 ++ tests/cases/09_gvl_external/main.st | 9 ++++++ tests/cases/09_gvl_external/project.toml | 11 ++++++++ tests/cases/10_gvl_wrong_file/EXPECTED.md | 4 +++ tests/cases/10_gvl_wrong_file/globals.st | 3 ++ tests/cases/10_gvl_wrong_file/other.st | 5 ++++ tests/cases/10_gvl_wrong_file/project.toml | 11 ++++++++ tests/cases/11_duplicate_global/EXPECTED.md | 3 ++ tests/cases/11_duplicate_global/globals.st | 7 +++++ tests/cases/11_duplicate_global/main.st | 2 ++ tests/cases/11_duplicate_global/project.toml | 11 ++++++++ tests/cases/12_io_unknown_var/EXPECTED.md | 4 +++ tests/cases/12_io_unknown_var/globals.st | 3 ++ tests/cases/12_io_unknown_var/main.st | 2 ++ tests/cases/12_io_unknown_var/project.toml | 16 +++++++++++ tests/cases/13_function_call/EXPECTED.md | 4 +++ tests/cases/13_function_call/main.st | 13 +++++++++ tests/cases/13_function_call/project.toml | 8 ++++++ .../14_function_write_global/EXPECTED.md | 4 +++ .../cases/14_function_write_global/globals.st | 3 ++ tests/cases/14_function_write_global/main.st | 9 ++++++ .../14_function_write_global/project.toml | 11 ++++++++ tests/cases/15_fb_instance/EXPECTED.md | 4 +++ tests/cases/15_fb_instance/main.st | 8 ++++++ tests/cases/15_fb_instance/motor.st | 9 ++++++ tests/cases/15_fb_instance/project.toml | 8 ++++++ tests/cases/16_fb_undeclared/EXPECTED.md | 4 +++ tests/cases/16_fb_undeclared/main.st | 7 +++++ tests/cases/16_fb_undeclared/motor.st | 9 ++++++ tests/cases/16_fb_undeclared/project.toml | 8 ++++++ tests/cases/17_ton/EXPECTED.md | 4 +++ tests/cases/17_ton/main.st | 8 ++++++ tests/cases/17_ton/project.toml | 8 ++++++ tests/cases/18_tof_ctu/EXPECTED.md | 5 ++++ tests/cases/18_tof_ctu/main.st | 13 +++++++++ tests/cases/18_tof_ctu/project.toml | 8 ++++++ tests/cases/19_recursive_call/EXPECTED.md | 4 +++ tests/cases/19_recursive_call/main.st | 14 ++++++++++ tests/cases/19_recursive_call/project.toml | 8 ++++++ tests/cases/20_line1/EXPECTED.md | 11 ++++++++ tests/cases/20_line1/globals.st | 7 +++++ tests/cases/20_line1/main.st | 8 ++++++ tests/cases/20_line1/motor.st | 13 +++++++++ tests/cases/20_line1/project.toml | 28 +++++++++++++++++++ 70 files changed, 513 insertions(+) create mode 100644 tests/cases/01_empty_main/EXPECTED.md create mode 100644 tests/cases/01_empty_main/main.st create mode 100644 tests/cases/01_empty_main/project.toml create mode 100644 tests/cases/02_bool_assign/EXPECTED.md create mode 100644 tests/cases/02_bool_assign/main.st create mode 100644 tests/cases/02_bool_assign/project.toml create mode 100644 tests/cases/03_short_circuit/EXPECTED.md create mode 100644 tests/cases/03_short_circuit/main.st create mode 100644 tests/cases/03_short_circuit/project.toml create mode 100644 tests/cases/04_if_elsif_else/EXPECTED.md create mode 100644 tests/cases/04_if_elsif_else/main.st create mode 100644 tests/cases/04_if_elsif_else/project.toml create mode 100644 tests/cases/05_while_normal/EXPECTED.md create mode 100644 tests/cases/05_while_normal/main.st create mode 100644 tests/cases/05_while_normal/project.toml create mode 100644 tests/cases/06_while_cycle_limit/EXPECTED.md create mode 100644 tests/cases/06_while_cycle_limit/main.st create mode 100644 tests/cases/06_while_cycle_limit/project.toml create mode 100644 tests/cases/07_int_arith/EXPECTED.md create mode 100644 tests/cases/07_int_arith/main.st create mode 100644 tests/cases/07_int_arith/project.toml create mode 100644 tests/cases/08_time_literal/EXPECTED.md create mode 100644 tests/cases/08_time_literal/main.st create mode 100644 tests/cases/08_time_literal/project.toml create mode 100644 tests/cases/09_gvl_external/EXPECTED.md create mode 100644 tests/cases/09_gvl_external/globals.st create mode 100644 tests/cases/09_gvl_external/main.st create mode 100644 tests/cases/09_gvl_external/project.toml create mode 100644 tests/cases/10_gvl_wrong_file/EXPECTED.md create mode 100644 tests/cases/10_gvl_wrong_file/globals.st create mode 100644 tests/cases/10_gvl_wrong_file/other.st create mode 100644 tests/cases/10_gvl_wrong_file/project.toml create mode 100644 tests/cases/11_duplicate_global/EXPECTED.md create mode 100644 tests/cases/11_duplicate_global/globals.st create mode 100644 tests/cases/11_duplicate_global/main.st create mode 100644 tests/cases/11_duplicate_global/project.toml create mode 100644 tests/cases/12_io_unknown_var/EXPECTED.md create mode 100644 tests/cases/12_io_unknown_var/globals.st create mode 100644 tests/cases/12_io_unknown_var/main.st create mode 100644 tests/cases/12_io_unknown_var/project.toml create mode 100644 tests/cases/13_function_call/EXPECTED.md create mode 100644 tests/cases/13_function_call/main.st create mode 100644 tests/cases/13_function_call/project.toml create mode 100644 tests/cases/14_function_write_global/EXPECTED.md create mode 100644 tests/cases/14_function_write_global/globals.st create mode 100644 tests/cases/14_function_write_global/main.st create mode 100644 tests/cases/14_function_write_global/project.toml create mode 100644 tests/cases/15_fb_instance/EXPECTED.md create mode 100644 tests/cases/15_fb_instance/main.st create mode 100644 tests/cases/15_fb_instance/motor.st create mode 100644 tests/cases/15_fb_instance/project.toml create mode 100644 tests/cases/16_fb_undeclared/EXPECTED.md create mode 100644 tests/cases/16_fb_undeclared/main.st create mode 100644 tests/cases/16_fb_undeclared/motor.st create mode 100644 tests/cases/16_fb_undeclared/project.toml create mode 100644 tests/cases/17_ton/EXPECTED.md create mode 100644 tests/cases/17_ton/main.st create mode 100644 tests/cases/17_ton/project.toml create mode 100644 tests/cases/18_tof_ctu/EXPECTED.md create mode 100644 tests/cases/18_tof_ctu/main.st create mode 100644 tests/cases/18_tof_ctu/project.toml create mode 100644 tests/cases/19_recursive_call/EXPECTED.md create mode 100644 tests/cases/19_recursive_call/main.st create mode 100644 tests/cases/19_recursive_call/project.toml create mode 100644 tests/cases/20_line1/EXPECTED.md create mode 100644 tests/cases/20_line1/globals.st create mode 100644 tests/cases/20_line1/main.st create mode 100644 tests/cases/20_line1/motor.st create mode 100644 tests/cases/20_line1/project.toml diff --git a/tests/README.md b/tests/README.md index 8246209..630b834 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,16 @@ 可以 `add_test` 调 `STCompiler` / `BytecodeExecutor`(编译→执行管线),或直接链接 `compiler` + `vm` 做单元断言。样例工程数据按「一份 toml + 若干 `.st`」分子目录存放,不要做成 library。 +## 用例目录(`cases/`) + +每个用例一个子目录:`project.toml` + `.st` + `EXPECTED.md`(期望:通过 / 报哪类错)。清单与编号见 [`Doc/初步计划.md`](../Doc/初步计划.md) 12.2 节,共 20 个: + +- 正例:01 空 MAIN、02 BOOL、03 短路、04 IF、05 WHILE、07 算术、08 TIME、09 GVL+EXTERNAL、13 FUNCTION、15 FB、17 TON、18 TOF/CTU、20 line1 +- 负例(编译错误/链接失败):10 非 GVL 写全局、11 同名全局、12 io.var 未声明、14 FUNCTION 写全局、16 未声明实例、19 循环调用 +- 运行时故障:06 cycle_limit 打满 + +12.2 完成标准:目录和期望表齐,此时大部分还编不过,属正常。实现阶段逐个点亮。 + ## 第一版要冻的 - 关键字表与约 20 个样例程序 diff --git a/tests/cases/01_empty_main/EXPECTED.md b/tests/cases/01_empty_main/EXPECTED.md new file mode 100644 index 0000000..f89b6a0 --- /dev/null +++ b/tests/cases/01_empty_main/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- 映像:MAIN(fn_id=0)字节码为一条 `RET`,`nregs=0` +- 运行任意周期数无故障 diff --git a/tests/cases/01_empty_main/main.st b/tests/cases/01_empty_main/main.st new file mode 100644 index 0000000..abb17ed --- /dev/null +++ b/tests/cases/01_empty_main/main.st @@ -0,0 +1,2 @@ +PROGRAM MAIN +END_PROGRAM diff --git a/tests/cases/01_empty_main/project.toml b/tests/cases/01_empty_main/project.toml new file mode 100644 index 0000000..46b4e3b --- /dev/null +++ b/tests/cases/01_empty_main/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case01" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/02_bool_assign/EXPECTED.md b/tests/cases/02_bool_assign/EXPECTED.md new file mode 100644 index 0000000..775f938 --- /dev/null +++ b/tests/cases/02_bool_assign/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- 执行后 `a` = TRUE、`b` = FALSE +- `TRUE` / `FALSE` 是关键字,不是标识符 diff --git a/tests/cases/02_bool_assign/main.st b/tests/cases/02_bool_assign/main.st new file mode 100644 index 0000000..570f321 --- /dev/null +++ b/tests/cases/02_bool_assign/main.st @@ -0,0 +1,8 @@ +PROGRAM MAIN +VAR + a : BOOL; + b : BOOL; +END_VAR + a := TRUE; + b := FALSE; +END_PROGRAM diff --git a/tests/cases/02_bool_assign/project.toml b/tests/cases/02_bool_assign/project.toml new file mode 100644 index 0000000..96dbd78 --- /dev/null +++ b/tests/cases/02_bool_assign/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case02" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/03_short_circuit/EXPECTED.md b/tests/cases/03_short_circuit/EXPECTED.md new file mode 100644 index 0000000..553b8e5 --- /dev/null +++ b/tests/cases/03_short_circuit/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- 反汇编中 `AND` / `OR` 必须编成条件跳转(`JT` / `JF`),不得先算两侧再组合 +- v1 表达式无调用、无副作用;「右侧有副作用时不执行」以编译产物含条件跳转为验收标准 diff --git a/tests/cases/03_short_circuit/main.st b/tests/cases/03_short_circuit/main.st new file mode 100644 index 0000000..7af35e7 --- /dev/null +++ b/tests/cases/03_short_circuit/main.st @@ -0,0 +1,9 @@ +PROGRAM MAIN +VAR + a : BOOL; + b : BOOL; + x : BOOL; +END_VAR + x := a AND b; + x := a OR b; +END_PROGRAM diff --git a/tests/cases/03_short_circuit/project.toml b/tests/cases/03_short_circuit/project.toml new file mode 100644 index 0000000..6c28ffc --- /dev/null +++ b/tests/cases/03_short_circuit/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case03" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/04_if_elsif_else/EXPECTED.md b/tests/cases/04_if_elsif_else/EXPECTED.md new file mode 100644 index 0000000..d7abe9d --- /dev/null +++ b/tests/cases/04_if_elsif_else/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- `sel` = 0 → `out` = 10;`sel` = 1 → `out` = 20;其他 → `out` = 30 +- 必须编成比较 + 条件跳转,不逐分支执行 diff --git a/tests/cases/04_if_elsif_else/main.st b/tests/cases/04_if_elsif_else/main.st new file mode 100644 index 0000000..ddf2cd3 --- /dev/null +++ b/tests/cases/04_if_elsif_else/main.st @@ -0,0 +1,13 @@ +PROGRAM MAIN +VAR + sel : INT; + out : INT; +END_VAR + IF sel = 0 THEN + out := 10; + ELSIF sel = 1 THEN + out := 20; + ELSE + out := 30; + END_IF +END_PROGRAM diff --git a/tests/cases/04_if_elsif_else/project.toml b/tests/cases/04_if_elsif_else/project.toml new file mode 100644 index 0000000..39d6878 --- /dev/null +++ b/tests/cases/04_if_elsif_else/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case04" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/05_while_normal/EXPECTED.md b/tests/cases/05_while_normal/EXPECTED.md new file mode 100644 index 0000000..a73f4f9 --- /dev/null +++ b/tests/cases/05_while_normal/EXPECTED.md @@ -0,0 +1,3 @@ +# 期望:编译通过 + +- 循环结束后 `n` = 10(正常终止,不触发 cycle_limit) diff --git a/tests/cases/05_while_normal/main.st b/tests/cases/05_while_normal/main.st new file mode 100644 index 0000000..90ea81c --- /dev/null +++ b/tests/cases/05_while_normal/main.st @@ -0,0 +1,9 @@ +PROGRAM MAIN +VAR + n : INT; +END_VAR + n := 0; + WHILE n < 10 DO + n := n + 1; + END_WHILE +END_PROGRAM diff --git a/tests/cases/05_while_normal/project.toml b/tests/cases/05_while_normal/project.toml new file mode 100644 index 0000000..2af69b3 --- /dev/null +++ b/tests/cases/05_while_normal/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case05" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/06_while_cycle_limit/EXPECTED.md b/tests/cases/06_while_cycle_limit/EXPECTED.md new file mode 100644 index 0000000..e73793a --- /dev/null +++ b/tests/cases/06_while_cycle_limit/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:运行时故障 + +- 单周期指令数超 `cycle_limit = 10`,VM 报故障并停止 +- 编译本身通过;WHILE 不做终止证明 diff --git a/tests/cases/06_while_cycle_limit/main.st b/tests/cases/06_while_cycle_limit/main.st new file mode 100644 index 0000000..7c3f716 --- /dev/null +++ b/tests/cases/06_while_cycle_limit/main.st @@ -0,0 +1,4 @@ +PROGRAM MAIN + WHILE TRUE DO + END_WHILE +END_PROGRAM diff --git a/tests/cases/06_while_cycle_limit/project.toml b/tests/cases/06_while_cycle_limit/project.toml new file mode 100644 index 0000000..6e5d23a --- /dev/null +++ b/tests/cases/06_while_cycle_limit/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case06" +entry = "program MAIN" +cycle_limit = 10 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/07_int_arith/EXPECTED.md b/tests/cases/07_int_arith/EXPECTED.md new file mode 100644 index 0000000..8f832e9 --- /dev/null +++ b/tests/cases/07_int_arith/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- `a` = 42、`b` = 10、`c` = 32、`eq` = TRUE +- 溢出饱和:`30000 + 30000` = 32767、`-30000 - 30000` = -32768(由 isa 饱和函数保证) diff --git a/tests/cases/07_int_arith/main.st b/tests/cases/07_int_arith/main.st new file mode 100644 index 0000000..0b140c9 --- /dev/null +++ b/tests/cases/07_int_arith/main.st @@ -0,0 +1,10 @@ +PROGRAM MAIN +VAR + a, b, c : INT; + eq : BOOL; +END_VAR + a := 7 * 6; + b := a / 4; + c := a - b; + eq := c > 30; +END_PROGRAM diff --git a/tests/cases/07_int_arith/project.toml b/tests/cases/07_int_arith/project.toml new file mode 100644 index 0000000..c3a2005 --- /dev/null +++ b/tests/cases/07_int_arith/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case07" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/08_time_literal/EXPECTED.md b/tests/cases/08_time_literal/EXPECTED.md new file mode 100644 index 0000000..3316c3d --- /dev/null +++ b/tests/cases/08_time_literal/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- `t1` = 10 毫秒、`t2` = 1250 毫秒(内部一律毫秒) +- `TIME` 与 `INT` 不混用;不支持 `T#` 以外的 TIME 语法 diff --git a/tests/cases/08_time_literal/main.st b/tests/cases/08_time_literal/main.st new file mode 100644 index 0000000..48f1d9e --- /dev/null +++ b/tests/cases/08_time_literal/main.st @@ -0,0 +1,7 @@ +PROGRAM MAIN +VAR + t1, t2 : TIME; +END_VAR + t1 := T#10ms; + t2 := T#1s250ms; +END_PROGRAM diff --git a/tests/cases/08_time_literal/project.toml b/tests/cases/08_time_literal/project.toml new file mode 100644 index 0000000..85aff37 --- /dev/null +++ b/tests/cases/08_time_literal/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case08" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/09_gvl_external/EXPECTED.md b/tests/cases/09_gvl_external/EXPECTED.md new file mode 100644 index 0000000..540026d --- /dev/null +++ b/tests/cases/09_gvl_external/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- 符号表里 `G1` 的 global 与 external 指向**同一槽**,不是新变量 +- 执行后 `x` = 5(初值来自 GVL) diff --git a/tests/cases/09_gvl_external/globals.st b/tests/cases/09_gvl_external/globals.st new file mode 100644 index 0000000..8b63cbf --- /dev/null +++ b/tests/cases/09_gvl_external/globals.st @@ -0,0 +1,3 @@ +VAR_GLOBAL + G1 : INT := 5; +END_VAR diff --git a/tests/cases/09_gvl_external/main.st b/tests/cases/09_gvl_external/main.st new file mode 100644 index 0000000..e16974d --- /dev/null +++ b/tests/cases/09_gvl_external/main.st @@ -0,0 +1,9 @@ +PROGRAM MAIN +VAR_EXTERNAL + G1 : INT; +END_VAR +VAR + x : INT; +END_VAR + x := G1; +END_PROGRAM diff --git a/tests/cases/09_gvl_external/project.toml b/tests/cases/09_gvl_external/project.toml new file mode 100644 index 0000000..f7d30d1 --- /dev/null +++ b/tests/cases/09_gvl_external/project.toml @@ -0,0 +1,11 @@ +[project] +name = "case09" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["globals.st", "main.st"] + +[gvl] +file = "globals.st" diff --git a/tests/cases/10_gvl_wrong_file/EXPECTED.md b/tests/cases/10_gvl_wrong_file/EXPECTED.md new file mode 100644 index 0000000..e778bd8 --- /dev/null +++ b/tests/cases/10_gvl_wrong_file/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译错误 + +- `other.st` 出现 `VAR_GLOBAL`,但 `gvl.file = globals.st` +- 报错类别:VAR_GLOBAL 只允许出现在 gvl.file diff --git a/tests/cases/10_gvl_wrong_file/globals.st b/tests/cases/10_gvl_wrong_file/globals.st new file mode 100644 index 0000000..0fd7899 --- /dev/null +++ b/tests/cases/10_gvl_wrong_file/globals.st @@ -0,0 +1,3 @@ +VAR_GLOBAL + G1 : BOOL; +END_VAR diff --git a/tests/cases/10_gvl_wrong_file/other.st b/tests/cases/10_gvl_wrong_file/other.st new file mode 100644 index 0000000..8ed700a --- /dev/null +++ b/tests/cases/10_gvl_wrong_file/other.st @@ -0,0 +1,5 @@ +PROGRAM MAIN +VAR_GLOBAL + G2 : BOOL; +END_VAR +END_PROGRAM diff --git a/tests/cases/10_gvl_wrong_file/project.toml b/tests/cases/10_gvl_wrong_file/project.toml new file mode 100644 index 0000000..8a0108f --- /dev/null +++ b/tests/cases/10_gvl_wrong_file/project.toml @@ -0,0 +1,11 @@ +[project] +name = "case10" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["globals.st", "other.st"] + +[gvl] +file = "globals.st" diff --git a/tests/cases/11_duplicate_global/EXPECTED.md b/tests/cases/11_duplicate_global/EXPECTED.md new file mode 100644 index 0000000..c87a3b0 --- /dev/null +++ b/tests/cases/11_duplicate_global/EXPECTED.md @@ -0,0 +1,3 @@ +# 期望:链接失败 + +- 同名 `VAR_GLOBAL`(G1)出现两次,禁止覆盖 diff --git a/tests/cases/11_duplicate_global/globals.st b/tests/cases/11_duplicate_global/globals.st new file mode 100644 index 0000000..f89fdf4 --- /dev/null +++ b/tests/cases/11_duplicate_global/globals.st @@ -0,0 +1,7 @@ +VAR_GLOBAL + G1 : INT; +END_VAR + +VAR_GLOBAL + G1 : INT; +END_VAR diff --git a/tests/cases/11_duplicate_global/main.st b/tests/cases/11_duplicate_global/main.st new file mode 100644 index 0000000..abb17ed --- /dev/null +++ b/tests/cases/11_duplicate_global/main.st @@ -0,0 +1,2 @@ +PROGRAM MAIN +END_PROGRAM diff --git a/tests/cases/11_duplicate_global/project.toml b/tests/cases/11_duplicate_global/project.toml new file mode 100644 index 0000000..ed7fcbb --- /dev/null +++ b/tests/cases/11_duplicate_global/project.toml @@ -0,0 +1,11 @@ +[project] +name = "case11" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["globals.st", "main.st"] + +[gvl] +file = "globals.st" diff --git a/tests/cases/12_io_unknown_var/EXPECTED.md b/tests/cases/12_io_unknown_var/EXPECTED.md new file mode 100644 index 0000000..6553fcb --- /dev/null +++ b/tests/cases/12_io_unknown_var/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译错误 + +- `[[io.input]]` 的 `var = "Nope"` 未在 GVL 声明 +- 报错类别:io.var 未在 GVL(io 不创造变量) diff --git a/tests/cases/12_io_unknown_var/globals.st b/tests/cases/12_io_unknown_var/globals.st new file mode 100644 index 0000000..0fd7899 --- /dev/null +++ b/tests/cases/12_io_unknown_var/globals.st @@ -0,0 +1,3 @@ +VAR_GLOBAL + G1 : BOOL; +END_VAR diff --git a/tests/cases/12_io_unknown_var/main.st b/tests/cases/12_io_unknown_var/main.st new file mode 100644 index 0000000..abb17ed --- /dev/null +++ b/tests/cases/12_io_unknown_var/main.st @@ -0,0 +1,2 @@ +PROGRAM MAIN +END_PROGRAM diff --git a/tests/cases/12_io_unknown_var/project.toml b/tests/cases/12_io_unknown_var/project.toml new file mode 100644 index 0000000..87db796 --- /dev/null +++ b/tests/cases/12_io_unknown_var/project.toml @@ -0,0 +1,16 @@ +[project] +name = "case12" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["globals.st", "main.st"] + +[gvl] +file = "globals.st" + +[[io.input]] +var = "Nope" +channel = 0 +bit = 0 diff --git a/tests/cases/13_function_call/EXPECTED.md b/tests/cases/13_function_call/EXPECTED.md new file mode 100644 index 0000000..0525049 --- /dev/null +++ b/tests/cases/13_function_call/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- `CALL fn_id` + 有界调用帧;函数无内部状态 +- 执行后 `x` = 7;结果值 = 函数名赋值(`Add := ...`) diff --git a/tests/cases/13_function_call/main.st b/tests/cases/13_function_call/main.st new file mode 100644 index 0000000..32597f9 --- /dev/null +++ b/tests/cases/13_function_call/main.st @@ -0,0 +1,13 @@ +FUNCTION Add : INT +VAR_INPUT + a, b : INT; +END_VAR + Add := a + b; +END_FUNCTION + +PROGRAM MAIN +VAR + x : INT; +END_VAR + x := Add(3, 4); +END_PROGRAM diff --git a/tests/cases/13_function_call/project.toml b/tests/cases/13_function_call/project.toml new file mode 100644 index 0000000..a3a31a3 --- /dev/null +++ b/tests/cases/13_function_call/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case13" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/14_function_write_global/EXPECTED.md b/tests/cases/14_function_write_global/EXPECTED.md new file mode 100644 index 0000000..c7b5913 --- /dev/null +++ b/tests/cases/14_function_write_global/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译错误 + +- `FUNCTION` 内写全局(此处经 `VAR_EXTERNAL` 写 `G1`) +- 报错类别:FUNCTION 禁止写全局 diff --git a/tests/cases/14_function_write_global/globals.st b/tests/cases/14_function_write_global/globals.st new file mode 100644 index 0000000..ab85e9b --- /dev/null +++ b/tests/cases/14_function_write_global/globals.st @@ -0,0 +1,3 @@ +VAR_GLOBAL + G1 : INT; +END_VAR diff --git a/tests/cases/14_function_write_global/main.st b/tests/cases/14_function_write_global/main.st new file mode 100644 index 0000000..3612310 --- /dev/null +++ b/tests/cases/14_function_write_global/main.st @@ -0,0 +1,9 @@ +FUNCTION Bad : INT +VAR_EXTERNAL + G1 : INT; +END_VAR + G1 := 1; +END_FUNCTION + +PROGRAM MAIN +END_PROGRAM diff --git a/tests/cases/14_function_write_global/project.toml b/tests/cases/14_function_write_global/project.toml new file mode 100644 index 0000000..6089ead --- /dev/null +++ b/tests/cases/14_function_write_global/project.toml @@ -0,0 +1,11 @@ +[project] +name = "case14" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["globals.st", "main.st"] + +[gvl] +file = "globals.st" diff --git a/tests/cases/15_fb_instance/EXPECTED.md b/tests/cases/15_fb_instance/EXPECTED.md new file mode 100644 index 0000000..5478127 --- /dev/null +++ b/tests/cases/15_fb_instance/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过 + +- FB 实例固定布局,`starter.Q` 字段偏移编译期算死 +- 命名输入绑定 `start` / `stop`;执行后 `q` = TRUE diff --git a/tests/cases/15_fb_instance/main.st b/tests/cases/15_fb_instance/main.st new file mode 100644 index 0000000..087c1b1 --- /dev/null +++ b/tests/cases/15_fb_instance/main.st @@ -0,0 +1,8 @@ +PROGRAM MAIN +VAR + starter : MotorStarter; + q : BOOL; +END_VAR + starter(start := TRUE, stop := FALSE); + q := starter.Q; +END_PROGRAM diff --git a/tests/cases/15_fb_instance/motor.st b/tests/cases/15_fb_instance/motor.st new file mode 100644 index 0000000..db037f9 --- /dev/null +++ b/tests/cases/15_fb_instance/motor.st @@ -0,0 +1,9 @@ +FUNCTION_BLOCK MotorStarter +VAR_INPUT + start, stop : BOOL; +END_VAR +VAR_OUTPUT + Q : BOOL; +END_VAR + Q := start AND NOT stop; +END_FUNCTION_BLOCK diff --git a/tests/cases/15_fb_instance/project.toml b/tests/cases/15_fb_instance/project.toml new file mode 100644 index 0000000..e33b8b6 --- /dev/null +++ b/tests/cases/15_fb_instance/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case15" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["motor.st", "main.st"] diff --git a/tests/cases/16_fb_undeclared/EXPECTED.md b/tests/cases/16_fb_undeclared/EXPECTED.md new file mode 100644 index 0000000..76a94bc --- /dev/null +++ b/tests/cases/16_fb_undeclared/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译错误 + +- 调用 `starter(...)` 但未声明实例 +- 报错类别:FB 必须先声明实例才能调用 diff --git a/tests/cases/16_fb_undeclared/main.st b/tests/cases/16_fb_undeclared/main.st new file mode 100644 index 0000000..f42d0cb --- /dev/null +++ b/tests/cases/16_fb_undeclared/main.st @@ -0,0 +1,7 @@ +PROGRAM MAIN +VAR + q : BOOL; +END_VAR + starter(start := TRUE, stop := FALSE); + q := starter.Q; +END_PROGRAM diff --git a/tests/cases/16_fb_undeclared/motor.st b/tests/cases/16_fb_undeclared/motor.st new file mode 100644 index 0000000..db037f9 --- /dev/null +++ b/tests/cases/16_fb_undeclared/motor.st @@ -0,0 +1,9 @@ +FUNCTION_BLOCK MotorStarter +VAR_INPUT + start, stop : BOOL; +END_VAR +VAR_OUTPUT + Q : BOOL; +END_VAR + Q := start AND NOT stop; +END_FUNCTION_BLOCK diff --git a/tests/cases/16_fb_undeclared/project.toml b/tests/cases/16_fb_undeclared/project.toml new file mode 100644 index 0000000..19123fd --- /dev/null +++ b/tests/cases/16_fb_undeclared/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case16" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["motor.st", "main.st"] diff --git a/tests/cases/17_ton/EXPECTED.md b/tests/cases/17_ton/EXPECTED.md new file mode 100644 index 0000000..b05ed8a --- /dev/null +++ b/tests/cases/17_ton/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译通过;运行行为与 Δt 一致 + +- `dt_ms = 10`、`PT = 30ms`:前 2 周期 `Q` = FALSE,第 3 周期起 `Q` = TRUE +- 累计用映像里的 Δt,不用 wall clock diff --git a/tests/cases/17_ton/main.st b/tests/cases/17_ton/main.st new file mode 100644 index 0000000..4d7447b --- /dev/null +++ b/tests/cases/17_ton/main.st @@ -0,0 +1,8 @@ +PROGRAM MAIN +VAR + t : TON; + q : BOOL; +END_VAR + t(in := TRUE, pt := T#30ms); + q := t.Q; +END_PROGRAM diff --git a/tests/cases/17_ton/project.toml b/tests/cases/17_ton/project.toml new file mode 100644 index 0000000..2726f46 --- /dev/null +++ b/tests/cases/17_ton/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case17" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/18_tof_ctu/EXPECTED.md b/tests/cases/18_tof_ctu/EXPECTED.md new file mode 100644 index 0000000..486bcb0 --- /dev/null +++ b/tests/cases/18_tof_ctu/EXPECTED.md @@ -0,0 +1,5 @@ +# 期望:编译通过;运行行为与 Δt 一致 + +- TOF:`in` 掉电后 `Q` 保持 `PT` 时长(20ms)再复位 +- CTU:`cu` 上升沿计数 +1;`r` 复位;`CV` 到 `PV`(3)时 `Q` = TRUE +- 内部累计用映像 Δt,不用 wall clock diff --git a/tests/cases/18_tof_ctu/main.st b/tests/cases/18_tof_ctu/main.st new file mode 100644 index 0000000..b3ab44a --- /dev/null +++ b/tests/cases/18_tof_ctu/main.st @@ -0,0 +1,13 @@ +PROGRAM MAIN +VAR + tf : TOF; + c : CTU; + q1, q2 : BOOL; + cnt : INT; +END_VAR + tf(in := FALSE, pt := T#20ms); + q1 := tf.Q; + c(cu := TRUE, r := FALSE, pv := 3); + q2 := c.Q; + cnt := c.CV; +END_PROGRAM diff --git a/tests/cases/18_tof_ctu/project.toml b/tests/cases/18_tof_ctu/project.toml new file mode 100644 index 0000000..37b21e6 --- /dev/null +++ b/tests/cases/18_tof_ctu/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case18" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/19_recursive_call/EXPECTED.md b/tests/cases/19_recursive_call/EXPECTED.md new file mode 100644 index 0000000..608fa62 --- /dev/null +++ b/tests/cases/19_recursive_call/EXPECTED.md @@ -0,0 +1,4 @@ +# 期望:编译错误 + +- `A` ↔ `B` 函数体循环调用(无界递归) +- 报错类别:有界 CALL 检查拒绝循环调用 diff --git a/tests/cases/19_recursive_call/main.st b/tests/cases/19_recursive_call/main.st new file mode 100644 index 0000000..8cf5455 --- /dev/null +++ b/tests/cases/19_recursive_call/main.st @@ -0,0 +1,14 @@ +FUNCTION A : INT + A := B(); +END_FUNCTION + +FUNCTION B : INT + B := A(); +END_FUNCTION + +PROGRAM MAIN +VAR + x : INT; +END_VAR + x := A(); +END_PROGRAM diff --git a/tests/cases/19_recursive_call/project.toml b/tests/cases/19_recursive_call/project.toml new file mode 100644 index 0000000..36e8e6c --- /dev/null +++ b/tests/cases/19_recursive_call/project.toml @@ -0,0 +1,8 @@ +[project] +name = "case19" +entry = "program MAIN" +cycle_limit = 1000 +dt_ms = 10 + +[files] +st = ["main.st"] diff --git a/tests/cases/20_line1/EXPECTED.md b/tests/cases/20_line1/EXPECTED.md new file mode 100644 index 0000000..dee5181 --- /dev/null +++ b/tests/cases/20_line1/EXPECTED.md @@ -0,0 +1,11 @@ +# 期望:编译通过;I 组合 → `Q0_0` 与急停语义一致 + +| I0_0 | I0_1 | EmergencyStop | Q0_0 | +|---|---|---|---| +| TRUE | FALSE | FALSE | TRUE | +| TRUE | TRUE | FALSE | FALSE | +| FALSE | FALSE | FALSE | FALSE | +| TRUE | FALSE | TRUE | FALSE | + +- `Q0_0 = I0_0 AND NOT I0_1 AND NOT EmergencyStop` +- 固定 I 序列跑两遍,槽位与工程哈希一致(回放确定性) diff --git a/tests/cases/20_line1/globals.st b/tests/cases/20_line1/globals.st new file mode 100644 index 0000000..6baeaa6 --- /dev/null +++ b/tests/cases/20_line1/globals.st @@ -0,0 +1,7 @@ +(* globals.st *) +VAR_GLOBAL + EmergencyStop : BOOL := FALSE; + I0_0 : BOOL; + I0_1 : BOOL; + Q0_0 : BOOL; +END_VAR diff --git a/tests/cases/20_line1/main.st b/tests/cases/20_line1/main.st new file mode 100644 index 0000000..fef1b92 --- /dev/null +++ b/tests/cases/20_line1/main.st @@ -0,0 +1,8 @@ +(* main.st *) +PROGRAM MAIN +VAR + starter : MotorStarter; +END_VAR + starter(start := I0_0, stop := I0_1); + Q0_0 := starter.Q; +END_PROGRAM diff --git a/tests/cases/20_line1/motor.st b/tests/cases/20_line1/motor.st new file mode 100644 index 0000000..69de7df --- /dev/null +++ b/tests/cases/20_line1/motor.st @@ -0,0 +1,13 @@ +(* motor.st *) +FUNCTION_BLOCK MotorStarter +VAR_EXTERNAL + EmergencyStop : BOOL; +END_VAR +VAR_INPUT + start, stop : BOOL; +END_VAR +VAR_OUTPUT + Q : BOOL; +END_VAR + Q := start AND NOT stop AND NOT EmergencyStop; +END_FUNCTION_BLOCK diff --git a/tests/cases/20_line1/project.toml b/tests/cases/20_line1/project.toml new file mode 100644 index 0000000..3f6658f --- /dev/null +++ b/tests/cases/20_line1/project.toml @@ -0,0 +1,28 @@ +[project] +name = "line1" +entry = "program MAIN" +cycle_limit = 100000 +dt_ms = 10 + +[files] +st = ["globals.st", "motor.st", "main.st"] + +# 全工程只允许这一处出现 VAR_GLOBAL(第一版:单个文件) +[gvl] +file = "globals.st" + +# 可选:I/O 绑定。不创造变量,var 必须已在 GVL 里声明 +[[io.input]] +var = "EmergencyStop" +channel = 0 +bit = 2 + +[[io.input]] +var = "I0_0" +channel = 0 +bit = 0 + +[[io.output]] +var = "Q0_0" +channel = 0 +bit = 0 From a82be632f6ec3c3f2e4b675e871d1eb864f11355 Mon Sep 17 00:00:00 2001 From: chentianya Date: Thu, 20 Aug 2026 15:25:09 +0800 Subject: [PATCH 15/69] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0toml++=E5=AD=90?= =?UTF-8?q?=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 3 +++ third_party/tomlplusplus | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 third_party/tomlplusplus diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6c69223 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/tomlplusplus"] + path = third_party/tomlplusplus + url = https://github.com/marzer/tomlplusplus.git diff --git a/third_party/tomlplusplus b/third_party/tomlplusplus new file mode 160000 index 0000000..3017243 --- /dev/null +++ b/third_party/tomlplusplus @@ -0,0 +1 @@ +Subproject commit 30172438cee64926dc41fdd9c11fb3ba5b2ba9de From f1d53de2d1e802479b91c7444c95330f741a63c4 Mon Sep 17 00:00:00 2001 From: chentianya Date: Thu, 20 Aug 2026 21:36:01 +0800 Subject: [PATCH 16/69] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E6=A0=87=E5=87=86=E8=87=B3=20C17=20+=20C++20=EF=BC=8C=E5=85=B3?= =?UTF-8?q?=E9=97=AD=20GNU=20=E6=89=A9=E5=B1=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/README.md | 2 +- cmake/Standards.cmake | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/README.md b/cmake/README.md index a9e6efd..b68d32a 100644 --- a/cmake/README.md +++ b/cmake/README.md @@ -4,6 +4,6 @@ | 文件 | 内容 | |---|---| -| `Standards.cmake` | C11、C++11;`STANDARD_REQUIRED`;关闭 GNU 扩展 | +| `Standards.cmake` | C17、C++20;`STANDARD_REQUIRED`;关闭 GNU 扩展 | 不要按词法、VM、host 再拆成业务 CMake module。改语言标准只改本目录,不要在各子目录再 `set(CMAKE_CXX_STANDARD …)`。 diff --git a/cmake/Standards.cmake b/cmake/Standards.cmake index e4dcdd9..f34e2ab 100644 --- a/cmake/Standards.cmake +++ b/cmake/Standards.cmake @@ -1,8 +1,8 @@ # C / C++ 语言标准。在 project() 之后 include,对后续目标生效。 -set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) From 6df3465b37b4f1ccba269215c4999d917a6e624d Mon Sep 17 00:00:00 2001 From: chentianya Date: Thu, 20 Aug 2026 22:51:05 +0800 Subject: [PATCH 17/69] =?UTF-8?q?=E7=99=BB=E8=AE=B0=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E5=AD=90=E6=A8=A1=E5=9D=97=E5=B9=B6=E9=A2=84=E7=95=99?= =?UTF-8?q?=20TUI=20=E8=A7=82=E5=AF=9F=E6=8E=A5=E5=8F=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - third_party:tomlplusplus(v3.4.0)+ FTXUI(main@9221a3b)git 子模块 - vm/扫描周期.md:可观察性接口(v1 预留)——step/snapshot、只读轮询、快照 POD 化 - executor/执行器入口.md:TUI 观察界面(最后做)——TUI 属执行器,输出层可替换 - 初步计划 12.11 补 TUI 归属;C++11 残留引用同步为 C++20 --- .gitmodules | 3 +++ Doc/executor/执行器入口.md | 11 +++++++++++ Doc/isa/指令与映像.md | 2 +- Doc/vm/扫描周期.md | 28 ++++++++++++++++++++++++++++ Doc/初步计划.md | 1 + isa/README.md | 2 +- third_party/FTXUI | 1 + third_party/README.md | 14 +++++++++++++- 8 files changed, 59 insertions(+), 3 deletions(-) create mode 160000 third_party/FTXUI diff --git a/.gitmodules b/.gitmodules index 6c69223..154ee6c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "third_party/tomlplusplus"] path = third_party/tomlplusplus url = https://github.com/marzer/tomlplusplus.git +[submodule "third_party/FTXUI"] + path = third_party/FTXUI + url = https://github.com/ArthurSonzogni/FTXUI.git diff --git a/Doc/executor/执行器入口.md b/Doc/executor/执行器入口.md index 006e180..be0b218 100644 --- a/Doc/executor/执行器入口.md +++ b/Doc/executor/执行器入口.md @@ -16,3 +16,14 @@ BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`( - 指令与映像编码见 [`指令与映像.md`](../isa/指令与映像.md) - ST 解析与链接见 [`编译管线.md`](../compiler/编译管线.md),本模块不碰 `.st` - 寄存器执行循环见 [`扫描周期.md`](../vm/扫描周期.md) + +--- + +## TUI 观察界面(最后做,v1 只预留) + +TUI 属于本模块(`BytecodeExecutor` 的界面),用途是**观察 VM 运行状态**(寄存器、I/Q/M、全局、FB 字段、pc、cycle 计数)。v1 不做 TUI,只预留两点: + +1. **可替换输出层**:所有人类可读输出走可注入 sink(默认文本模式)。将来 TUI 独占终端时,文本输出换成 TUI 渲染器,互不污染。 +2. **展示层与 vm 观察 API 分离**:TUI 只轮询 vm 的只读快照(见 [`扫描周期.md`](../vm/扫描周期.md) 可观察性接口),不写 VM 状态。 + +候选库:`third_party/FTXUI/`(占位,vendor 在 TUI 阶段进行,见 [`third_party/README.md`](../third_party/README.md))。 diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index a36fb46..7d2c0fb 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -56,7 +56,7 @@ CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/` ## 类型 -C++11 定宽: +C++20 定宽: - `BOOL`:`uint8_t`,0/1 - `INT`:`int16_t`;加减乘除溢出饱和 diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md index 71dbbe0..f0d93d3 100644 --- a/Doc/vm/扫描周期.md +++ b/Doc/vm/扫描周期.md @@ -20,3 +20,31 @@ vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [ - `CALL` 目标是立即数 `fn_id`,有界调用栈 - 无 GC、无堆、无线程;每周期受 `cycle_limit` 限制 - I/O 从哪来、Δt 谁给、回放怎么喂,属于 `BytecodeExecutor`(读 `.stb` + sidecar) + +--- + +## 可观察性接口(v1 预留) + +TUI 是执行器(`BytecodeExecutor`)的界面,用途是方便观察 VM 运行状态。**v1 不做 TUI**,只预留接口,避免后续改 VM 的观察面。原则: + +- **数据与展示分离**:`vm` 只提供只读观察 API(数据);TUI 渲染与文本输出属于 `executor`(展示)。vm 不画任何界面。 +- **只读轮询**:TUI 通过快照轮询,绝不写 VM 状态;"继续 / 单步"是 executor 侧命令,不改变确定性。 +- **快照 POD 化**:快照保持简单数据(将来跨进程 TUI 可序列化)。 + +建议的 API 形态(12.9 实现时冻结签名,本节只定方向): + +```cpp +struct Snapshot { + uint32_t pc; // 当前指令下标(相对函数字节码段) + uint32_t fn_id; // 当前函数 + uint32_t cycle_count; // 本周期已执行指令数 + uint32_t call_depth; // 调用栈深度 + // 寄存器 / I / Q / M / 全局 / FB 字段 的只读视图 +}; + +bool step(); // 精确执行一条指令,停在指令边界 +const Snapshot& snapshot() const; +``` + +- 寄存器 ≤256、映像很小,快照拷贝成本可忽略。 +- 单步、回放、dump 都建立在同一套观察 API 上。 diff --git a/Doc/初步计划.md b/Doc/初步计划.md index edaa3a3..4b3163f 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -549,6 +549,7 @@ BytecodeExecutor .stb [--cycles N] # 执行:加载 .stb + sidecar - `line1` 固定输入表:急停、start/stop 组合 → 期望 `Q0_0`。 - 回放:同一 I 序列跑两遍,槽位与哈希一致。 - 单步:能停在指令边界,看到寄存器和 I/Q/M(可先做打印,不必做 UI)。 +- TUI 界面**最后做**,v1 只预留接口:执行器(`BytecodeExecutor`)的观察界面,数据走 vm 观察 API(见 `Doc/vm/扫描周期.md`),展示与输出层分离(见 `Doc/executor/执行器入口.md`)。 完成标准(第一版可交付):**`MAIN` + 一个 Motor FB + I/O + 全局**,扫描周期可重复。 diff --git a/isa/README.md b/isa/README.md index 5265c12..f17b372 100644 --- a/isa/README.md +++ b/isa/README.md @@ -88,7 +88,7 @@ target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) ### 2. `Types.h` -`include/isa/Types.h`,inline,无对应 `.cpp`。命名空间 `isa`。C++11,不用 designated initializer。 +`include/isa/Types.h`,inline,无对应 `.cpp`。命名空间 `isa`。C++20;本模块保持简单结构体,不依赖花哨特性。 - `Bool` = `uint8_t`,`Int` = `int16_t`,`Time` = `int64_t`(毫秒;理由见规范文) - `TypeTag`:`Bool` / `Int` / `Time` diff --git a/third_party/FTXUI b/third_party/FTXUI new file mode 160000 index 0000000..9221a3b --- /dev/null +++ b/third_party/FTXUI @@ -0,0 +1 @@ +Subproject commit 9221a3bf199e5a06057d6b99d9d3797a78523c5d diff --git a/third_party/README.md b/third_party/README.md index 120fdd8..a338a56 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -2,6 +2,18 @@ 第三方源码或单头库,本仓库不把它们做成业务 CMake 目标。 -预期由 `compiler` **私有链接**(例如 toml 解析)。不要从 `isa`、`vm`、`host` 直接依赖本目录。 +预期由 `compiler` **私有链接**(例如 toml 解析)。不要从 `isa`、`vm`、`executor` 直接依赖本目录。 新增依赖时在此注明来源、许可和所用版本。第一版不上 LLVM、不做 CPython / JIT。 + +## 已登记 + +| 目录 | 来源 | 许可 | 版本 | 形态 | 用途 | +|---|---|---|---|---|---| +| `tomlplusplus/` | marzer/tomlplusplus | MIT | v3.4.0 | **git 子模块**(需 `git submodule update --init` 检出) | compiler 解析 `project.toml` | +| `FTXUI/` | ArthurSonzogni/FTXUI | MIT | main@9221a3b | **git 子模块** | 执行器 TUI 候选(最后做,见 `Doc/executor/执行器入口.md`) | + +## 说明 + +- 两者都是子模块:拉取后先 `git submodule update --init --recursive` 才能编。 +- 首次检出可用 `--depth 1` 浅克隆加快(本仓库不依赖子模块历史)。 From c076c253683d4b6d90ed341e2e886fb060a5a615 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 00:32:53 +0800 Subject: [PATCH 18/69] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.3=20toml=20?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=EF=BC=9Aschema=20=E6=A0=A1=E9=AA=8C=E3=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E9=9B=86=E5=90=88=E3=80=81=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E5=93=88=E5=B8=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Project.h/cpp:toml++ 解析 + 字段白名单(未知键带行号报错)、name/entry/cycle_limit/dt_ms/files.st 必填、io 条目完整性、entry 必须 program MAIN - compile_files:files.st ∪ gvl.file 去重;compute_project_hash:路径排序后 FNV-1a 64 增量,缺文件报 file missing - STCompiler :打印文件列表与哈希,失败退出码 1,--help - compiler 私有链接 toml++ v3.4.0;测试固化 compiler_toml(35 断言,line1 哈希冻结 0xc3fe4ae74ad45900) --- compiler/CMakeLists.txt | 4 +- compiler/include/compiler/Project.h | 42 ++++ compiler/src/Project.cpp | 366 ++++++++++++++++++++++++++++ compiler/src/main.cpp | 47 +++- tests/CMakeLists.txt | 11 + tests/src/compiler_test.cpp | 217 +++++++++++++++++ 6 files changed, 682 insertions(+), 5 deletions(-) create mode 100644 compiler/include/compiler/Project.h create mode 100644 compiler/src/Project.cpp create mode 100644 tests/src/compiler_test.cpp diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index c09970c..eba0270 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -8,9 +8,11 @@ project(Compiler # 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 add_library(compiler STATIC - ./src/Lexer.cpp) + ./src/Lexer.cpp + ./src/Project.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) target_link_libraries(compiler PUBLIC isa) # 可执行入口:STCompiler -o .stb diff --git a/compiler/include/compiler/Project.h b/compiler/include/compiler/Project.h new file mode 100644 index 0000000..eb9e24f --- /dev/null +++ b/compiler/include/compiler/Project.h @@ -0,0 +1,42 @@ +/** + * @file Project.h + * @brief 工程定义与 project.toml 解析(schema 校验) + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include + +#include "isa/Image.h" + +namespace compiler { + + // 与 Doc/初步计划.md 12.1 的 toml 字段表一一对应。 + // 字段白名单 / 必填 / io 完整性在 parse_project 内校验。 + struct Project { + std::string name; // [project] 必填 + std::string entry; // 必填,第一版必须 "program MAIN" + uint32_t cycle_limit = 0; // 必填 > 0 + uint32_t dt_ms = 0; // 必填 > 0 + std::vector files_st; // [files] 必填非空 + std::string gvl_file; // [gvl] 可选;无则空串 + std::vector io; // [[io.*]] 可选;slot 12.6 才解析,此处填 0 + std::string base_dir; // toml 所在目录(解析相对路径用) + }; + + // 解析并校验 project.toml。成功返回 true;失败返回 false 并写 err, + // err 带稳定类别前缀(unknown key / missing field / invalid value / bad entry)+ 行号。 + bool parse_project(const std::string& toml_path, Project* out, std::string* err); + + // 编译文件集合:files.st ∪ gvl.file,去重(gvl 已在 files.st 则跳过)。 + // 保持 files.st 顺序,gvl 追加在后;相对路径以 base_dir 为基准解析。 + std::vector compile_files(const Project& p); + + // 校验全部文件存在并计算工程哈希:集合按路径排序,对内容做 FNV-1a 64 增量; + // 空集合 = basis(isa::kFnvBasis)。缺文件报错前缀 "file missing"。 + bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err); +} diff --git a/compiler/src/Project.cpp b/compiler/src/Project.cpp new file mode 100644 index 0000000..c809ee5 --- /dev/null +++ b/compiler/src/Project.cpp @@ -0,0 +1,366 @@ +/** + * @file Project.cpp + * @brief 工程定义与 project.toml 解析(schema 校验) + * @author + * @date 2026-08-21 + */ + +#include "compiler/Project.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "isa/Image.h" + +namespace compiler { +namespace { + + // ---- 错误收集:稳定类别前缀 + 行号 ---- + + void fail(std::string* err, const std::string& msg) { + if (err) { + *err = msg; + } + } + + std::string at(const toml::node& n) { + char buf[32]; + std::snprintf(buf, sizeof buf, " (line %u)", n.source().begin.line); + return buf; + } + + // parse_error 的位置可能是空的(如文件打不开):只在有位置时带行号 + std::string at_pos(const toml::source_position& pos) { + char buf[32]; + std::snprintf(buf, sizeof buf, " (line %u)", pos.line); + return static_cast(pos) ? buf : std::string(); + } + + // 取字符串,必填校验 + bool req_string(const toml::table& tbl, const char* sec, const char* key, + std::string* out, std::string* err) { + if (const auto nv = tbl[key]) { + if (!nv.is_string()) { + fail(err, std::string("invalid value for '") + key + "' in [" + sec + + "] (expect string)" + at(*nv.node())); + return false; + } + *out = nv.value_or(std::string()); + return true; + } + fail(err, std::string("missing field '") + key + "' in [" + sec + "]"); + return false; + } + + // 取正整数,必填校验 + bool req_pos_int(const toml::table& tbl, const char* sec, const char* key, + uint32_t* out, std::string* err) { + if (const auto nv = tbl[key]) { + if (!nv.is_integer()) { + fail(err, std::string("invalid value for '") + key + "' in [" + sec + + "] (expect integer)" + at(*nv.node())); + return false; + } + const int64_t v = nv.value_or(0); + if (v <= 0) { + fail(err, std::string("invalid value for '") + key + "' in [" + sec + + "] (expect > 0)" + at(*nv.node())); + return false; + } + *out = static_cast(v); + return true; + } + fail(err, std::string("missing field '") + key + "' in [" + sec + "]"); + return false; + } + + // 取非负整数,必填校验(channel / bit 允许 0) + bool req_uint(const toml::table& tbl, const char* sec, const char* key, + uint32_t* out, std::string* err) { + if (const auto nv = tbl[key]) { + if (!nv.is_integer()) { + fail(err, std::string("invalid value for '") + key + "' in [" + sec + + "] (expect integer)" + at(*nv.node())); + return false; + } + const int64_t v = nv.value_or(0); + if (v < 0) { + fail(err, std::string("invalid value for '") + key + "' in [" + sec + + "] (expect >= 0)" + at(*nv.node())); + return false; + } + *out = static_cast(v); + return true; + } + fail(err, std::string("missing field '") + key + "' in [" + sec + "]"); + return false; + } + + // 表内键白名单;返回 false 并报首个未知键 + bool check_keys(const toml::table& tbl, const char* sec, + const char* const* allowed, size_t n_allowed, + std::string* err) { + for (const auto& [key, node] : tbl) { + bool ok = false; + for (size_t i = 0; i < n_allowed; ++i) { + if (key == allowed[i]) { + ok = true; + break; + } + } + if (!ok) { + fail(err, std::string("unknown key '") + std::string(key) + "' in [" + + sec + "]" + at(node)); + return false; + } + } + return true; + } + + // ---- [project] ---- + + bool parse_project_section(const toml::table& root, Project* out, std::string* err) { + static const char* const allowed[] = {"name", "entry", "cycle_limit", "dt_ms"}; + + if (const auto nv = root["project"]) { + if (!nv.is_table()) { + fail(err, "invalid value for 'project' (expect table)" + at(*nv.node())); + return false; + } + const toml::table& sec = *nv.as_table(); + if (!check_keys(sec, "project", allowed, 4, err)) { + return false; + } + if (!req_string(sec, "project", "name", &out->name, err)) { + return false; + } + if (!req_string(sec, "project", "entry", &out->entry, err)) { + return false; + } + if (out->entry != "program MAIN") { + fail(err, "invalid value for 'entry' in [project] (expect \"program MAIN\")" + + at(*sec["entry"].node())); + return false; + } + if (!req_pos_int(sec, "project", "cycle_limit", &out->cycle_limit, err)) { + return false; + } + if (!req_pos_int(sec, "project", "dt_ms", &out->dt_ms, err)) { + return false; + } + return true; + } + fail(err, "missing field 'project' in "); + return false; + } + + // ---- [files] ---- + + bool parse_files_section(const toml::table& root, Project* out, std::string* err) { + static const char* const allowed[] = {"st"}; + + if (const auto nv = root["files"]) { + if (!nv.is_table()) { + fail(err, "invalid value for 'files' (expect table)" + at(*nv.node())); + return false; + } + const toml::table& sec = *nv.as_table(); + if (!check_keys(sec, "files", allowed, 1, err)) { + return false; + } + if (const auto st = sec["st"]) { + if (!st.is_array()) { + fail(err, "invalid value for 'st' in [files] (expect array)" + at(*st.node())); + return false; + } + const toml::array& arr = *st.as_array(); + if (arr.empty()) { + fail(err, "invalid value for 'st' in [files] (expect non-empty array)"); + return false; + } + for (const auto& el : arr) { + if (!el.is_string()) { + fail(err, "invalid value in 'st' in [files] (expect array of strings)" + + at(el)); + return false; + } + out->files_st.push_back(el.value_or(std::string())); + } + return true; + } + fail(err, "missing field 'st' in [files]"); + return false; + } + fail(err, "missing field 'files' in "); + return false; + } + + // ---- [gvl](可选)---- + + bool parse_gvl_section(const toml::table& root, Project* out, std::string* err) { + static const char* const allowed[] = {"file"}; + + if (const auto nv = root["gvl"]) { + if (!nv.is_table()) { + fail(err, "invalid value for 'gvl' (expect table)" + at(*nv.node())); + return false; + } + const toml::table& sec = *nv.as_table(); + if (!check_keys(sec, "gvl", allowed, 1, err)) { + return false; + } + return req_string(sec, "gvl", "file", &out->gvl_file, err); + } + return true; // gvl 可选 + } + + // ---- [[io.input]] / [[io.output]](可选)---- + + bool parse_io_entry(const toml::node& el, bool is_input, Project* out, std::string* err) { + static const char* const allowed[] = {"var", "channel", "bit"}; + + if (!el.is_table()) { + fail(err, std::string("bad entry in [[io.") + (is_input ? "input" : "output") + + "]] (expect table)" + at(el)); + return false; + } + const toml::table& sec = *el.as_table(); + if (!check_keys(sec, is_input ? "io.input" : "io.output", allowed, 3, err)) { + return false; + } + isa::IoBinding b; + b.is_input = is_input; + b.slot = 0; // 12.6 链接阶段再解析 + if (!req_string(sec, is_input ? "io.input" : "io.output", "var", &b.var, err)) { + return false; + } + if (!req_uint(sec, is_input ? "io.input" : "io.output", "channel", &b.channel, err)) { + return false; + } + if (!req_uint(sec, is_input ? "io.input" : "io.output", "bit", &b.bit, err)) { + return false; + } + out->io.push_back(b); + return true; + } + + bool parse_io_section(const toml::table& root, Project* out, std::string* err) { + static const char* const allowed[] = {"input", "output"}; + + if (const auto nv = root["io"]) { + if (!nv.is_table()) { + fail(err, "invalid value for 'io' (expect table)" + at(*nv.node())); + return false; + } + const toml::table& sec = *nv.as_table(); + if (!check_keys(sec, "io", allowed, 2, err)) { + return false; + } + for (const char* which : {"input", "output"}) { + if (const auto arr = sec[which]) { + if (!arr.is_array()) { + fail(err, std::string("invalid value for '") + which + + "' in [io] (expect array of tables)" + at(*arr.node())); + return false; + } + const bool is_input = (which[0] == 'i'); + for (const auto& el : *arr.as_array()) { + if (!parse_io_entry(el, is_input, out, err)) { + return false; + } + } + } + } + return true; + } + return true; // io 可选 + } + + // ---- 顶层 ---- + + bool parse_root(const toml::table& root, Project* out, std::string* err) { + static const char* const allowed[] = {"project", "files", "gvl", "io"}; + if (!check_keys(root, "root", allowed, 4, err)) { + return false; + } + if (!parse_project_section(root, out, err)) { + return false; + } + if (!parse_files_section(root, out, err)) { + return false; + } + if (!parse_gvl_section(root, out, err)) { + return false; + } + return parse_io_section(root, out, err); + } + + std::string dir_of(const std::string& path) { + const size_t slash = path.find_last_of("/\\"); + return (slash == std::string::npos) ? "." : path.substr(0, slash); + } + +} // namespace + +bool parse_project(const std::string& toml_path, Project* out, std::string* err) { + out->base_dir = dir_of(toml_path); + + // toml++ 默认 TOML_EXCEPTIONS=1:parse_file 失败直接抛 parse_error + toml::table tbl; + try { + tbl = toml::parse_file(toml_path); + } catch (const toml::parse_error& e) { + fail(err, std::string("parse error: ") + std::string(e.description()) + + at_pos(e.source().begin)); + return false; + } + return parse_root(tbl, out, err); +} + +// ---- 文件集合与工程哈希 ---- + +std::vector compile_files(const Project& p) { + std::vector out = p.files_st; + if (!p.gvl_file.empty() && + std::find(out.begin(), out.end(), p.gvl_file) == out.end()) { + out.push_back(p.gvl_file); + } + return out; +} + +bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err) { + std::vector files = compile_files(p); + + // 路径排序(规格:路径只当排序键) + std::vector sorted = files; + std::sort(sorted.begin(), sorted.end()); + + std::filesystem::path base(p.base_dir); + uint64_t h = isa::kFnvBasis; + for (const std::string& f : sorted) { + std::ifstream in(base / f, std::ios::binary); + if (!in) { + fail(err, "file missing: " + (base / f).string()); + return false; + } + char buf[4096]; + while (in) { + in.read(buf, sizeof buf); + const std::streamsize n = in.gcount(); + if (n > 0) { + h = isa::fnv1a64_update(h, reinterpret_cast(buf), + static_cast(n)); + } + } + } + *hash = h; + return true; +} + +} // namespace compiler diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index 222bc03..a45da05 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -2,14 +2,53 @@ * @file main.cpp * @brief STCompiler 可执行入口 * @author - * @date 2026-08-19 + * @date 2026-08-21 */ #include +#include +#include +#include + +#include "compiler/Project.h" + +namespace { + void usage() { + std::printf("usage: STCompiler \n" + " 解析工程并打印文件集合与工程哈希(12.3 阶段)\n" + " --help 打印本帮助\n"); + } +} int main(int argc, char** argv) { - (void)argc; - (void)argv; - std::printf("STCompiler 0.1\n"); + if (argc < 2) { + std::printf("STCompiler 0.1\n"); + usage(); + return 0; + } + if (std::strcmp(argv[1], "--help") == 0) { + usage(); + return 0; + } + + compiler::Project proj; + std::string err; + if (!compiler::parse_project(argv[1], &proj, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + const std::vector files = compiler::compile_files(proj); + uint64_t hash = 0; + if (!compiler::compute_project_hash(proj, &hash, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + + std::printf("project: %s\n", proj.name.c_str()); + std::printf("files:\n"); + for (const std::string& f : files) { + std::printf(" %s\n", f.c_str()); + } + std::printf("hash: 0x%016llx\n", static_cast(hash)); return 0; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 44ab0de..7765d03 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,3 +12,14 @@ target_link_libraries(isa_test PRIVATE isa) add_test(NAME isa_roundtrip COMMAND isa_test) + +# compiler 库测试:toml schema、文件集合、工程哈希(REPO_ROOT 注入源目录绝对路径) +add_executable(compiler_test + ./src/compiler_test.cpp) + +target_link_libraries(compiler_test PRIVATE compiler) +target_compile_definitions(compiler_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME compiler_toml + COMMAND compiler_test) diff --git a/tests/src/compiler_test.cpp b/tests/src/compiler_test.cpp new file mode 100644 index 0000000..d77fcb0 --- /dev/null +++ b/tests/src/compiler_test.cpp @@ -0,0 +1,217 @@ +/** + * @file compiler_test.cpp + * @brief compiler 库测试:toml schema、文件集合、工程哈希 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include +#include + +#include "compiler/Project.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 写临时 toml(负例用),返回路径 ---- + +static std::string write_tmp(const char* name, const char* content) { + const std::filesystem::path dir = std::filesystem::temp_directory_path(); + const std::filesystem::path path = dir / name; + std::FILE* f = std::fopen(path.string().c_str(), "w"); + std::fputs(content, f); + std::fclose(f); + return path.string(); +} + +static void rm_tmp(const std::string& path) { + std::remove(path.c_str()); +} + +// ---- 1. line1 正例 ---- + +static bool test_line1() { + using namespace compiler; + Project p; + std::string err; + CHECK(parse_project(std::string(REPO_ROOT) + "/examples/line1/project.toml", &p, &err)); + CHECK(p.name == "line1"); + CHECK(p.entry == "program MAIN"); + CHECK(p.cycle_limit == 100000); + CHECK(p.dt_ms == 10); + CHECK(p.files_st.size() == 3); + CHECK(p.files_st[0] == "globals.st"); + CHECK(p.gvl_file == "globals.st"); + CHECK(p.io.size() == 3); + CHECK(p.io[0].var == "EmergencyStop" && p.io[0].is_input && p.io[0].bit == 2); + CHECK(p.io[2].var == "Q0_0" && !p.io[2].is_input); + CHECK(p.base_dir == std::string(REPO_ROOT) + "/examples/line1"); + + const std::vector files = compile_files(p); + CHECK(files.size() == 3); // gvl 已在 files.st,不重复 + + uint64_t h = 0; + CHECK(compute_project_hash(p, &h, &err)); + CHECK(h == 0xc3fe4ae74ad45900ull); // 冻结值:确定性回放 + return true; +} + +// ---- 2. 无 io / 无 gvl 的用例 ---- + +static bool test_minimal() { + using namespace compiler; + Project p; + std::string err; + CHECK(parse_project(std::string(REPO_ROOT) + "/tests/cases/01_empty_main/project.toml", &p, &err)); + CHECK(p.gvl_file.empty()); + CHECK(p.io.empty()); + CHECK(compile_files(p).size() == 1); + CHECK(parse_project(std::string(REPO_ROOT) + "/tests/cases/15_fb_instance/project.toml", &p, &err)); + CHECK(p.gvl_file.empty()); + return true; +} + +// ---- 3. gvl.file 不在 files.st 时追加 ---- + +static bool test_gvl_append() { + using namespace compiler; + const std::string path = write_tmp( + "stator_gvl_append.toml", + "[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n" + "[files]\nst = [\"main.st\"]\n[gvl]\nfile = \"globals.st\"\n"); + Project p; + std::string err; + CHECK(parse_project(path, &p, &err)); + const std::vector files = compile_files(p); + CHECK(files.size() == 2); + CHECK(files[0] == "main.st"); + CHECK(files[1] == "globals.st"); + rm_tmp(path); + return true; +} + +// ---- 4. 负例:schema 校验类别 ---- + +static bool expect_err(const std::string& path, const char* prefix) { + compiler::Project p; + std::string err; + if (compiler::parse_project(path, &p, &err)) { + std::printf("FAIL %s: parsed ok\n", path.c_str()); + return false; + } + if (err.find(prefix) != 0) { + std::printf("FAIL %s: want '%s', got '%s'\n", path.c_str(), prefix, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +static bool test_negative() { + using namespace compiler; + + const std::string b1 = write_tmp( + "stator_bad1.toml", + "[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n" + "[files]\nst = [\"a.st\"]\n[bogus]\nx = 1\n"); + if (!expect_err(b1, "unknown key")) return false; + + const std::string b2 = write_tmp( + "stator_bad2.toml", + "[project]\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n" + "[files]\nst = [\"a.st\"]\n"); + if (!expect_err(b2, "missing field")) return false; + + const std::string b3 = write_tmp( + "stator_bad3.toml", + "[project]\nname = \"x\"\nentry = \"program foo\"\ncycle_limit = 1\ndt_ms = 1\n" + "[files]\nst = [\"a.st\"]\n"); + if (!expect_err(b3, "invalid value")) return false; + + const std::string b4 = write_tmp( + "stator_bad4.toml", + "[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n" + "[files]\nst = [\"a.st\"]\n[[io.input]]\nvar = \"I0_0\"\nchannel = 0\n"); + if (!expect_err(b4, "missing field")) return false; + + const std::string b5 = write_tmp("stator_bad5.toml", "[project\nname = \"x\"\n"); + if (!expect_err(b5, "parse error")) return false; + + // 缺文件(compute_project_hash 层) + const std::string b6 = write_tmp( + "stator_bad6.toml", + "[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n" + "[files]\nst = [\"nope.st\"]\n"); + Project p; + std::string err; + CHECK(parse_project(b6, &p, &err)); + uint64_t h = 0; + CHECK(!compute_project_hash(p, &h, &err)); + CHECK(err.find("file missing") == 0); + + // toml 文件本身不存在 + if (!expect_err(std::filesystem::temp_directory_path().string() + "/stator_nope.toml", + "parse error")) { + return false; + } + + rm_tmp(b1); + rm_tmp(b2); + rm_tmp(b3); + rm_tmp(b4); + rm_tmp(b5); + rm_tmp(b6); + return true; +} + +// ---- 5. 20 个用例全部可解析 + 可哈希 ---- + +static bool test_all_cases() { + using namespace compiler; + int n = 0; + for (const auto& dir : std::filesystem::directory_iterator( + std::string(REPO_ROOT) + "/tests/cases")) { + if (!dir.is_directory()) { + continue; + } + const std::string toml = dir.path().string() + "/project.toml"; + Project p; + std::string err; + if (!parse_project(toml, &p, &err)) { + std::printf("FAIL parse %s: %s\n", toml.c_str(), err.c_str()); + return false; + } + uint64_t h = 0; + if (!compute_project_hash(p, &h, &err)) { + std::printf("FAIL hash %s: %s\n", toml.c_str(), err.c_str()); + return false; + } + ++n; + } + CHECK(n == 20); + return true; +} + +int main() { + if (!test_line1()) return 1; + if (!test_minimal()) return 1; + if (!test_gvl_append()) return 1; + if (!test_negative()) return 1; + if (!test_all_cases()) return 1; + std::printf("compiler_test: %d checks passed\n", g_checks); + return 0; +} From 9a07f938f97f79924a734f9ced612727bb87a28b Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 10:41:39 +0800 Subject: [PATCH 19/69] =?UTF-8?q?=E6=B8=85=E7=90=86=EF=BC=9Aisa=20?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E5=B1=80=E9=83=A8=20C++11=20=E8=A6=86?= =?UTF-8?q?=E7=9B=96=EF=BC=8CProject.cpp=20=E8=A1=A5=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=8C=E5=8A=A0=20.clangd=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isa/CMakeLists.txt 删除 set(CMAKE_CXX_STANDARD 11):会覆盖全局 C++20 决策,改由 Standards.cmake 统一 - Project.cpp 各段解析补说明注释 - .clangd:clangd 编译数据库指向 build/gcc-debug --- .clangd | 2 ++ compiler/src/Project.cpp | 11 +++++++++++ isa/CMakeLists.txt | 4 ---- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..829c7b0 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: build/gcc-debug diff --git a/compiler/src/Project.cpp b/compiler/src/Project.cpp index c809ee5..a7f748a 100644 --- a/compiler/src/Project.cpp +++ b/compiler/src/Project.cpp @@ -23,12 +23,14 @@ namespace { // ---- 错误收集:稳定类别前缀 + 行号 ---- + // 写错误消息;err 为 nullptr 时静默忽略(调用方可不关心原因) void fail(std::string* err, const std::string& msg) { if (err) { *err = msg; } } + // 给 toml 节点附加行号后缀 " (line N)",用于定位非法值位置 std::string at(const toml::node& n) { char buf[32]; std::snprintf(buf, sizeof buf, " (line %u)", n.source().begin.line); @@ -125,6 +127,8 @@ namespace { // ---- [project] ---- + // [project] 段:name/entry/cycle_limit/dt_ms 全必填; + // 第一版 entry 只接受 "program MAIN" bool parse_project_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"name", "entry", "cycle_limit", "dt_ms"}; @@ -162,6 +166,7 @@ namespace { // ---- [files] ---- + // [files] 段:st 必填、非空数组,元素必须全为字符串,按声明顺序收集 bool parse_files_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"st"}; @@ -203,6 +208,7 @@ namespace { // ---- [gvl](可选)---- + // [gvl] 段:可选;存在时 file 必填字符串,缺失整段不报错 bool parse_gvl_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"file"}; @@ -222,6 +228,7 @@ namespace { // ---- [[io.input]] / [[io.output]](可选)---- + // 单个 I/O 条目:var/channel/bit 全必填、全非负整数;slot 留待 12.6 链接阶段 bool parse_io_entry(const toml::node& el, bool is_input, Project* out, std::string* err) { static const char* const allowed[] = {"var", "channel", "bit"}; @@ -250,6 +257,7 @@ namespace { return true; } + // [io] 段:可选;input/output 子表为数组时逐条解析,两数组都可缺省 bool parse_io_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"input", "output"}; @@ -284,6 +292,8 @@ namespace { // ---- 顶层 ---- + // 顶层:root 键白名单 + 按 project → files → gvl → io 顺序解析, + // 任一段失败立即返回 false(首个错误优先,不做恢复) bool parse_root(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"project", "files", "gvl", "io"}; if (!check_keys(root, "root", allowed, 4, err)) { @@ -301,6 +311,7 @@ namespace { return parse_io_section(root, out, err); } + // 取路径的目录部分:最后一个分隔符之前;无分隔符返回 "."(相对当前目录) std::string dir_of(const std::string& path) { const size_t slash = path.find_last_of("/\\"); return (slash == std::string::npos) ? "." : path.substr(0, slash); diff --git a/isa/CMakeLists.txt b/isa/CMakeLists.txt index 2f42c49..508b752 100644 --- a/isa/CMakeLists.txt +++ b/isa/CMakeLists.txt @@ -6,10 +6,6 @@ project(ISA VERSION 0.1 DESCRIPTION "指令集架构") -# C/C++ 标准版本 -set(CMAKE_C_STANDARD 11) -set(CMAKE_CXX_STANDARD 11) - add_library(isa STATIC ./src/Encode.cpp ./src/Image.cpp) From d4e2c1ae7cf52888e2e47bbf086e996284dc1a2e Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 10:51:45 +0800 Subject: [PATCH 20/69] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.4=20=E8=AF=8D?= =?UTF-8?q?=E6=B3=95=EF=BC=9A25=20=E5=85=B3=E9=94=AE=E5=AD=97=E3=80=81?= =?UTF-8?q?=E5=B0=8F=E5=86=99=E6=8A=98=E5=8F=A0=E3=80=81TIME=20=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E3=80=81lex=20error=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lexer.h/cpp:关键字表(大小写不敏感折小写)、(* *) 注释不嵌套、整数/T# 段式字面量、最长匹配符号 - token 带 source_file + 行列(起始位置,修了消费后行列的偏差) - lexer_test:75 断言(line1 三文件 token 流 21/34/27、大小写、符号、注释、行列、5 类负例) - Doc/compiler/词法.md:设计文档 + 执行计划 --- Doc/compiler/词法.md | 127 +++++++++++ Doc/索引.md | 2 + compiler/include/compiler/Lexer.h | 89 ++++++++ compiler/src/Lexer.cpp | 349 +++++++++++++++++++++++++++++- tests/CMakeLists.txt | 11 + tests/src/lexer_test.cpp | 210 ++++++++++++++++++ 6 files changed, 786 insertions(+), 2 deletions(-) create mode 100644 Doc/compiler/词法.md create mode 100644 compiler/include/compiler/Lexer.h create mode 100644 tests/src/lexer_test.cpp diff --git a/Doc/compiler/词法.md b/Doc/compiler/词法.md new file mode 100644 index 0000000..8a38b98 --- /dev/null +++ b/Doc/compiler/词法.md @@ -0,0 +1,127 @@ +# 词法(12.4) + +compiler 模块的词法分析。输入:单个 `.st` 源文件内容;输出:稳定 token 流(带 `source_file` + 行列)。语法与字段表见 [`初步计划.md`](../初步计划.md) 12.1、12.4。 + +## 做 / 不做 + +**做** + +- `(* *)` 注释、标识符、关键字、整数、`TIME` 字面量、`:=`、比较符、括号 +- 关键字与标识符统一折成**小写**内部形(与 IEC 一致,大小写不敏感) +- 每个 token 带 `source_file` + 行列,供以后报错 +- 非法字符 → `lex error` 硬错误(带行列) + +**不做(第一版)** + +- 字符串字面量、`REAL` 字面量、十六进制/科学计数 +- 注释嵌套(`(*` 到第一个 `*)` 结束,未闭合报错) +- 浮点数、`_` 数字分隔符 + +## 关键字表(12.1 冻结,只认这些) + +```text +PROGRAM FUNCTION FUNCTION_BLOCK END_PROGRAM END_FUNCTION END_FUNCTION_BLOCK +VAR VAR_INPUT VAR_OUTPUT VAR_GLOBAL VAR_EXTERNAL END_VAR +BOOL INT TIME +IF ELSIF ELSE END_IF WHILE END_WHILE +AND OR NOT +TRUE FALSE +TON TOF CTU +``` + +其余单词一律当标识符;`VAR_IN_OUT`、`REF`、`CLASS` 等由语法层(12.5)出明确错误。 + +## Token + +```cpp +enum class Tok { + // 关键字(25 个,逐一对应上表) + ... + // 字面量 + IDENT, // text = 小写名 + INT_LIT, // int_value + TIME_LIT, // int_value = 毫秒 + // 符号 + ASSIGN, // := + EQ, NE, LT, LE, GT, GE, // = <> < <= > >= + PLUS, MINUS, STAR, SLASH, // + - * / + LPAREN, RPAREN, COMMA, SEMI, // ( ) , ; + COLON, DOT, // : . + END, // 文件结束 +}; + +struct Token { + Tok type; + std::string text; // 标识符/关键字的小写文本;字面量原文 + int64_t int_value; // INT_LIT / TIME_LIT(毫秒) + std::string source_file; + uint32_t line; // 从 1 起 + uint32_t col; // 从 1 起 +}; +``` + +## 规则 + +### 大小写 + +- 关键字匹配**大小写不敏感**(`program` / `PROGRAM` / `Program` 同义) +- 标识符统一折小写存入 `text`(内部形);原始大小写不保留(v1 无字符串字面量,报错靠行列定位) + +### 字面量 + +- 整数:`[0-9]+`,`int64_t` 存储;超 `INT64_MAX` → `lex error` +- `TIME`:`T#`(不敏感)+ 一个或多个 `<整数><单位>` 段,单位不敏感:`ms s m h d`,累加为毫秒: + - `T#10ms` → 10 + - `T#1s250ms` → 1250 + - `T#2h` → 7200000 + - 缺单位 / 未知单位 / 空段 → `lex error` +- 负数不是字面量:`-5` 是 `MINUS` + 整数,由语法层处理 + +### 符号 + +- `:=` 优先于 `:`;`<>` 优先于 `<`/`>`;`<=`/`>=` 单独匹配 + +### 注释 + +- `(* ... *)`,**不嵌套**;未闭合 → `lex error` +- 注释内不产 token + +## 错误 + +稳定前缀 `lex error`,带文件与行列: + +```text +lex error: illegal character '@' (motor.st, line 3, col 7) +lex error: unterminated comment (main.st, line 1, col 1) +lex error: bad time literal 'T#3q' (main.st, line 4, col 10) +``` + +## 完成标准 + +1. 第 11 节三个文件(`examples/line1/*.st`)lex 成稳定 token 流;`lexer_test` 断言数量、类型、小写化、行列、`END` 收尾 +2. 非法字符 / 未闭合注释 / 坏 TIME 字面量 → `lex error` 负例过 +3. 全部构建 + `ctest` 无回归 + +--- + +## 执行计划(单步确认) + +1. **`Lexer.h`(新建 `compiler/include/compiler/Lexer.h`)** + - `Tok` 枚举:25 关键字 + `IDENT`/`INT_LIT`/`TIME_LIT` + 符号(`:= = <> < <= > >= + - * / ( ) , ; : .`)+ `END` + - `Token { type, text, int_value, source_file, line, col }`(行列从 1 起) + - `bool lex_file(path, out, err)` / `bool lex(source_file, content, out, err)` + +2. **`Lexer.cpp`(重写占位)** + - 25 关键字小写映射表,大小写不敏感,标识符统一折小写 + - `(* *)` 注释不嵌套、未闭合报错;非法字符报错 + - 整数 `[0-9]+`(超 `INT64_MAX` 报错);`T#<单位>` 段式解析累加毫秒(`ms s m h d`) + - `:=` / `<>` / `<=` / `>=` 最长匹配优先 + - 错误前缀 `lex error`,带 file/line/col + +3. **`lexer_test`(`tests/src/lexer_test.cpp` + CMake,链 `compiler` 库)** + - line1 三文件:token 数量 / 类型序列 / 小写化 / 行列 / `END` 收尾断言 + - 负例:`@` 非法字符、未闭合注释、`T#3q` 坏字面量、超大整数 → `lex error` 类别 + +4. **验证**:`cmake --build` + `ctest`(新增 `lexer_test` 后 5/5 全绿) + +5. **提交**:`Lexer.h/cpp` + `lexer_test` + 文档 diff --git a/Doc/索引.md b/Doc/索引.md index 1e278c4..3411680 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -8,6 +8,7 @@ doc/ 初步计划.md isa/指令与映像.md compiler/编译管线.md + compiler/词法.md vm/扫描周期.md executor/执行器入口.md ``` @@ -17,6 +18,7 @@ doc/ | [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | +| [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | diff --git a/compiler/include/compiler/Lexer.h b/compiler/include/compiler/Lexer.h new file mode 100644 index 0000000..153f5ac --- /dev/null +++ b/compiler/include/compiler/Lexer.h @@ -0,0 +1,89 @@ +/** + * @file Lexer.h + * @brief ST 子集词法分析 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include + +namespace compiler { + + // token 类型。关键字与 token 集见 Doc/compiler/词法.md。 + enum class Tok { + // 关键字(25 个,与 12.1 冻结表一致) + PROGRAM, + FUNCTION, + FUNCTION_BLOCK, + END_PROGRAM, + END_FUNCTION, + END_FUNCTION_BLOCK, + VAR, + VAR_INPUT, + VAR_OUTPUT, + VAR_GLOBAL, + VAR_EXTERNAL, + END_VAR, + BOOL, + INT, + TIME, + IF, + ELSIF, + ELSE, + END_IF, + WHILE, + END_WHILE, + AND, + OR, + NOT, + TRUE, + FALSE, + TON, + TOF, + CTU, + // 字面量 + IDENT, // text = 小写名 + INT_LIT, // int_value + TIME_LIT, // int_value = 毫秒 + // 符号 + ASSIGN, // := + EQ, // = + NE, // <> + LT, // < + LE, // <= + GT, // > + GE, // >= + PLUS, // + + MINUS, // - + STAR, // * + SLASH, // / + LPAREN, // ( + RPAREN, // ) + COMMA, // , + SEMI, // ; + COLON, // : + DOT, // . + END, // 文件结束 + }; + + struct Token { + Tok type; + std::string text; // 标识符/关键字的小写文本;字面量原文 + int64_t int_value; // INT_LIT / TIME_LIT(毫秒) + std::string source_file; + uint32_t line; // 从 1 起 + uint32_t col; // 从 1 起 + }; + + // 对源文本 lex 成 token 流(含末尾 END)。失败返回 false, + // err 前缀 "lex error",带文件与行列。 + bool lex(const std::string& source_file, const std::string& content, + std::vector* out, std::string* err); + + // 读文件后 lex(文件不存在 / 读取失败也算 lex error) + bool lex_file(const std::string& path, std::vector* out, std::string* err); +} diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index f73d4d0..c35616e 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -1,8 +1,353 @@ /** * @file Lexer.cpp - * @brief 词法分析(12.4 实现) - * @date 2026-08-19 + * @brief ST 子集词法分析 + * @author + * @date 2026-08-21 */ +#include "compiler/Lexer.h" + +#include +#include +#include +#include + namespace compiler { +namespace { + + // 25 个关键字:小写键 → Tok(大小写不敏感,见 Doc/compiler/词法.md) + const struct { + const char* key; + Tok tok; + } kKeywords[] = { + {"program", Tok::PROGRAM}, + {"function", Tok::FUNCTION}, + {"function_block", Tok::FUNCTION_BLOCK}, + {"end_program", Tok::END_PROGRAM}, + {"end_function", Tok::END_FUNCTION}, + {"end_function_block", Tok::END_FUNCTION_BLOCK}, + {"var", Tok::VAR}, + {"var_input", Tok::VAR_INPUT}, + {"var_output", Tok::VAR_OUTPUT}, + {"var_global", Tok::VAR_GLOBAL}, + {"var_external", Tok::VAR_EXTERNAL}, + {"end_var", Tok::END_VAR}, + {"bool", Tok::BOOL}, + {"int", Tok::INT}, + {"time", Tok::TIME}, + {"if", Tok::IF}, + {"elsif", Tok::ELSIF}, + {"else", Tok::ELSE}, + {"end_if", Tok::END_IF}, + {"while", Tok::WHILE}, + {"end_while", Tok::END_WHILE}, + {"and", Tok::AND}, + {"or", Tok::OR}, + {"not", Tok::NOT}, + {"true", Tok::TRUE}, + {"false", Tok::FALSE}, + {"ton", Tok::TON}, + {"tof", Tok::TOF}, + {"ctu", Tok::CTU}, + }; + + // TIME 字面量单位 → 毫秒倍数(不敏感) + int64_t time_unit_ms(const std::string& unit) { + if (unit == "ms") return 1; + if (unit == "s") return 1000; + if (unit == "m") return 60000; + if (unit == "h") return 3600000; + if (unit == "d") return 86400000; + return -1; + } + + class Lexer { + public: + Lexer(const std::string& source_file, const std::string& content, + std::vector* out, std::string* err) + : src_(content), sf_(source_file), out_(out), err_(err), + pos_(0), line_(1), col_(1) {} + + bool run() { + while (true) { + if (!skip_space_and_comments()) { + return false; + } + if (pos_ >= src_.size()) { + push(Tok::END, "", 0); + return true; + } + if (!lex_one()) { + return false; + } + } + } + + private: + bool fail(const std::string& msg) { + if (err_) { + char buf[128]; + std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)", + sf_.c_str(), line_, col_); + *err_ = "lex error: " + msg + buf; + } + return false; + } + + char cur() const { return src_[pos_]; } + bool at_end() const { return pos_ >= src_.size(); } + + void advance() { + if (cur() == '\n') { + ++line_; + col_ = 1; + } else { + ++col_; + } + ++pos_; + } + + // 跳过空白与 (* *) 注释(不嵌套);未闭合返回 false + bool skip_space_and_comments() { + while (!at_end()) { + if (std::isspace(static_cast(cur()))) { + advance(); + } else if (cur() == '(' && pos_ + 1 < src_.size() && + src_[pos_ + 1] == '*') { + advance(); + advance(); // 跳过 "(*" + bool closed = false; + while (!at_end()) { + if (cur() == '*' && pos_ + 1 < src_.size() && + src_[pos_ + 1] == ')') { + advance(); + advance(); + closed = true; + break; + } + advance(); + } + if (!closed) { + return fail("unterminated comment"); + } + } else { + break; + } + } + return true; + } + + void push(Tok type, const std::string& text, int64_t value) { + Token t; + t.type = type; + t.text = text; + t.int_value = value; + t.source_file = sf_; + t.line = tok_line_; + t.col = tok_col_; + out_->push_back(t); + } + + bool lex_one() { + // 记录本 token 起始位置(push 时消费已经推进了行列) + tok_line_ = line_; + tok_col_ = col_; + const char c = cur(); + + // TIME 字面量:T#(大小写不敏感)优先于标识符 + if ((c == 't' || c == 'T') && pos_ + 1 < src_.size() && src_[pos_ + 1] == '#') { + return lex_time(); + } + + // 标识符 / 关键字(统一折小写) + if (std::isalpha(static_cast(c)) || c == '_') { + const uint32_t start = pos_; + while (!at_end() && + (std::isalnum(static_cast(cur())) || cur() == '_')) { + advance(); + } + std::string word = src_.substr(start, pos_ - start); + for (char& ch : word) { + ch = static_cast(std::tolower(static_cast(ch))); + } + for (const auto& kw : kKeywords) { + if (word == kw.key) { + push(kw.tok, kw.key, 0); + return true; + } + } + push(Tok::IDENT, word, 0); + return true; + } + + // 整数 + if (std::isdigit(static_cast(c))) { + const uint32_t start = pos_; + while (!at_end() && std::isdigit(static_cast(cur()))) { + advance(); + } + const std::string digits = src_.substr(start, pos_ - start); + uint64_t v = 0; + for (const char ch : digits) { + const uint64_t d = static_cast(ch - '0'); + if (v > (static_cast(INT64_MAX) - d) / 10) { + return fail("integer literal too large"); + } + v = v * 10 + d; + } + push(Tok::INT_LIT, digits, static_cast(v)); + return true; + } + + // 符号(最长匹配优先) + switch (c) { + case ':': + if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { + advance(); + advance(); + push(Tok::ASSIGN, ":=", 0); + } else { + advance(); + push(Tok::COLON, ":", 0); + } + return true; + case '=': + advance(); + push(Tok::EQ, "=", 0); + return true; + case '<': + if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { + advance(); + advance(); + push(Tok::LE, "<=", 0); + } else if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '>') { + advance(); + advance(); + push(Tok::NE, "<>", 0); + } else { + advance(); + push(Tok::LT, "<", 0); + } + return true; + case '>': + if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { + advance(); + advance(); + push(Tok::GE, ">=", 0); + } else { + advance(); + push(Tok::GT, ">", 0); + } + return true; + case '+': + advance(); + push(Tok::PLUS, "+", 0); + return true; + case '-': + advance(); + push(Tok::MINUS, "-", 0); + return true; + case '*': + advance(); + push(Tok::STAR, "*", 0); + return true; + case '/': + advance(); + push(Tok::SLASH, "/", 0); + return true; + case '(': + advance(); + push(Tok::LPAREN, "(", 0); + return true; + case ')': + advance(); + push(Tok::RPAREN, ")", 0); + return true; + case ',': + advance(); + push(Tok::COMMA, ",", 0); + return true; + case ';': + advance(); + push(Tok::SEMI, ";", 0); + return true; + case '.': + advance(); + push(Tok::DOT, ".", 0); + return true; + default: + return fail(std::string("illegal character '") + c + "'"); + } + } + + // T#... 段式解析,累加毫秒 + bool lex_time() { + const uint32_t start = pos_; + advance(); // T + advance(); // # + int64_t total = 0; + bool any = false; + while (!at_end() && std::isdigit(static_cast(cur()))) { + uint64_t v = 0; + while (!at_end() && std::isdigit(static_cast(cur()))) { + const uint64_t d = static_cast(cur() - '0'); + if (v > (static_cast(INT64_MAX) - d) / 10) { + return fail("bad time literal"); + } + v = v * 10 + d; + advance(); + } + std::string unit; + while (!at_end() && std::isalpha(static_cast(cur()))) { + unit += static_cast( + std::tolower(static_cast(cur()))); + advance(); + } + const int64_t mult = time_unit_ms(unit); + if (mult < 0) { + return fail("bad time literal"); + } + total += static_cast(v) * mult; + any = true; + } + if (!any) { + return fail("bad time literal"); + } + push(Tok::TIME_LIT, src_.substr(start, pos_ - start), total); + return true; + } + + const std::string& src_; + const std::string& sf_; + std::vector* out_; + std::string* err_; + size_t pos_; + uint32_t line_; + uint32_t col_; + uint32_t tok_line_ = 1; + uint32_t tok_col_ = 1; + }; + +} // namespace + +bool lex(const std::string& source_file, const std::string& content, + std::vector* out, std::string* err) { + out->clear(); + Lexer l(source_file, content, out, err); + return l.run(); } + +bool lex_file(const std::string& path, std::vector* out, std::string* err) { + std::ifstream in(path, std::ios::binary); + if (!in) { + if (err) { + *err = "lex error: cannot open file (" + path + ")"; + } + return false; + } + std::string content((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + return lex(path, content, out, err); +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7765d03..abc0ccb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,3 +23,14 @@ target_compile_definitions(compiler_test PRIVATE add_test(NAME compiler_toml COMMAND compiler_test) + +# 词法测试:token 流 + 负例(REPO_ROOT 注入源目录绝对路径) +add_executable(lexer_test + ./src/lexer_test.cpp) + +target_link_libraries(lexer_test PRIVATE compiler) +target_compile_definitions(lexer_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME lexer_tokens + COMMAND lexer_test) diff --git a/tests/src/lexer_test.cpp b/tests/src/lexer_test.cpp new file mode 100644 index 0000000..2e0cca5 --- /dev/null +++ b/tests/src/lexer_test.cpp @@ -0,0 +1,210 @@ +/** + * @file lexer_test.cpp + * @brief 词法测试:line1 token 流 + 负例 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include + +#include "compiler/Lexer.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 1. 基本 token 分类与大小写折叠 ---- + +static bool test_basic() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "PROGRAM MAIN\nVAR\n a, b : BOOL;\nEND_VAR\nEND_PROGRAM", &ts, &err)); + CHECK(ts.size() == 12); + CHECK(ts[0].type == Tok::PROGRAM); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "main"); + CHECK(ts[2].type == Tok::VAR); + CHECK(ts[3].type == Tok::IDENT && ts[3].text == "a"); + CHECK(ts[4].type == Tok::COMMA); + CHECK(ts[5].type == Tok::IDENT && ts[5].text == "b"); + CHECK(ts[6].type == Tok::COLON); + CHECK(ts[7].type == Tok::BOOL); + CHECK(ts[8].type == Tok::SEMI); + CHECK(ts[9].type == Tok::END_VAR); + CHECK(ts[10].type == Tok::END_PROGRAM); + CHECK(ts[11].type == Tok::END); + CHECK(ts[11].line == 5); + return true; +} + +// ---- 2. 大小写不敏感 ---- + +static bool test_case() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "PrOgRaM mAiN eNd_PrOgRaM", &ts, &err)); + CHECK(ts[0].type == Tok::PROGRAM); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "main"); + CHECK(ts[2].type == Tok::END_PROGRAM); + CHECK(lex("t.st", "T#10MS t#1s250Ms", &ts, &err)); + CHECK(ts[0].type == Tok::TIME_LIT && ts[0].int_value == 10); + CHECK(ts[1].type == Tok::TIME_LIT && ts[1].int_value == 1250); + return true; +} + +// ---- 3. 符号 ---- + +static bool test_symbols() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "a := b <> c <= d >= e < f > g + h - i * j / k ( ) , ; : .", + &ts, &err)); + CHECK(ts[0].type == Tok::IDENT); + CHECK(ts[1].type == Tok::ASSIGN); + CHECK(ts[3].type == Tok::NE); + CHECK(ts[5].type == Tok::LE); + CHECK(ts[7].type == Tok::GE); + CHECK(ts[9].type == Tok::LT); + CHECK(ts[11].type == Tok::GT); + CHECK(ts[13].type == Tok::PLUS); + CHECK(ts[15].type == Tok::MINUS); + CHECK(ts[17].type == Tok::STAR); + CHECK(ts[19].type == Tok::SLASH); + CHECK(ts[21].type == Tok::LPAREN); + CHECK(ts[22].type == Tok::RPAREN); + CHECK(ts[23].type == Tok::COMMA); + CHECK(ts[24].type == Tok::SEMI); + CHECK(ts[25].type == Tok::COLON); + CHECK(ts[26].type == Tok::DOT); + return true; +} + +// ---- 4. 注释 ---- + +static bool test_comments() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "(* 整行注释 *)\nPROGRAM (* 行尾注释 *) MAIN", &ts, &err)); + CHECK(ts.size() == 3); + CHECK(ts[0].type == Tok::PROGRAM); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "main"); + CHECK(ts[2].type == Tok::END); + return true; +} + +// ---- 5. 行列号 ---- + +static bool test_pos() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("f.st", "a\n b : INT;", &ts, &err)); + CHECK(ts[0].type == Tok::IDENT && ts[0].line == 1 && ts[0].col == 1); + CHECK(ts[1].type == Tok::IDENT && ts[1].line == 2 && ts[1].col == 3); + CHECK(ts[2].type == Tok::COLON && ts[2].line == 2 && ts[2].col == 5); + CHECK(ts[3].type == Tok::INT && ts[3].line == 2 && ts[3].col == 7); + CHECK(ts[0].source_file == "f.st"); + return true; +} + +// ---- 6. 负例 ---- + +static bool expect_lex_err(const std::string& src, const char* prefix) { + std::vector ts; + std::string err; + if (compiler::lex("t.st", src, &ts, &err)) { + std::printf("FAIL: lexed ok: %s\n", src.c_str()); + return false; + } + if (err.find(prefix) != 0) { + std::printf("FAIL: want '%s', got '%s'\n", prefix, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +static bool test_negative() { + if (!expect_lex_err("a @ b", "lex error")) return false; + if (!expect_lex_err("(* unclosed", "lex error")) return false; + if (!expect_lex_err("T#3q", "lex error")) return false; + if (!expect_lex_err("T#", "lex error")) return false; + if (!expect_lex_err("99999999999999999999", "lex error")) return false; + return true; +} + +// ---- 7. line1 三个文件 ---- + +static bool test_line1() { + using namespace compiler; + const char* files[] = {"globals.st", "motor.st", "main.st"}; + const int expect_count[] = {21, 34, 27}; + for (int i = 0; i < 3; ++i) { + std::vector ts; + std::string err; + const std::string path = + std::string(REPO_ROOT) + "/examples/line1/" + files[i]; + CHECK(lex_file(path, &ts, &err)); + CHECK(ts.size() == static_cast(expect_count[i])); + CHECK(ts.back().type == Tok::END); + } + + // globals.st 局部抽查 + std::vector ts; + std::string err; + CHECK(lex_file(std::string(REPO_ROOT) + "/examples/line1/globals.st", &ts, &err)); + CHECK(ts[0].type == Tok::VAR_GLOBAL); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "emergencystop"); + CHECK(ts[3].type == Tok::BOOL); + CHECK(ts[4].type == Tok::ASSIGN); + CHECK(ts[5].type == Tok::FALSE); + CHECK(ts[ts.size() - 2].type == Tok::END_VAR); + + // main.st 抽查:FB 调用与字段访问 + CHECK(lex_file(std::string(REPO_ROOT) + "/examples/line1/main.st", &ts, &err)); + bool saw_fb_call = false; + for (size_t k = 0; k + 3 < ts.size(); ++k) { + if (ts[k].type == Tok::IDENT && ts[k].text == "starter" && + ts[k + 1].type == Tok::LPAREN && ts[k + 2].type == Tok::IDENT) { + saw_fb_call = true; + } + } + CHECK(saw_fb_call); + bool saw_field = false; + for (size_t k = 0; k + 1 < ts.size(); ++k) { + if (ts[k].type == Tok::IDENT && ts[k].text == "starter" && + ts[k + 1].type == Tok::DOT) { + saw_field = true; + } + } + CHECK(saw_field); + return true; +} + +int main() { + if (!test_basic()) return 1; + if (!test_case()) return 1; + if (!test_symbols()) return 1; + if (!test_comments()) return 1; + if (!test_pos()) return 1; + if (!test_negative()) return 1; + if (!test_line1()) return 1; + std::printf("lexer_test: %d checks passed\n", g_checks); + return 0; +} From 4ba44459e2bca6255ed224cae6d67783e1863e95 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:07:34 +0800 Subject: [PATCH 21/69] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.5=20=E9=80=92?= =?UTF-8?q?=E5=BD=92=E4=B8=8B=E9=99=8D=E8=AF=AD=E6=B3=95=EF=BC=9AAST=20+?= =?UTF-8?q?=20=E6=8B=92=E7=BB=9D=E6=B8=85=E5=8D=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parser.h/cpp:POU 外壳(PROGRAM/FUNCTION/FUNCTION_BLOCK)、变量段、语句(赋值/IF/WHILE/FB 调用)、表达式优先级爬升(NOT/AND/OR 短路/比较/算术/函数调用/负号) - Unit 结构:顶层 VAR_GLOBAL 段(GVL 文件形态)+ POU 列表 - 拒绝:VAR_IN_OUT/REF/CLASS/ANY 等保留名、链式比较、FB 字段赋值、顶层非 GVL 段 - THEN/DO 补入关键字表(12.5 修订,同步 12.1 与词法文档) - parser_test:98 断言(line1 三文件 AST、FUNCTION 调用、IF/WHILE/TON、9 类负例),ctest 6/6 --- Doc/compiler/词法.md | 3 + Doc/compiler/语法.md | 183 +++++++ Doc/初步计划.md | 1 + Doc/索引.md | 2 + compiler/CMakeLists.txt | 3 +- compiler/include/compiler/Lexer.h | 2 + compiler/include/compiler/Parser.h | 133 +++++ compiler/src/Lexer.cpp | 2 + compiler/src/Parser.cpp | 747 +++++++++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/parser_test.cpp | 301 ++++++++++++ 11 files changed, 1387 insertions(+), 1 deletion(-) create mode 100644 Doc/compiler/语法.md create mode 100644 compiler/include/compiler/Parser.h create mode 100644 compiler/src/Parser.cpp create mode 100644 tests/src/parser_test.cpp diff --git a/Doc/compiler/词法.md b/Doc/compiler/词法.md index 8a38b98..908b864 100644 --- a/Doc/compiler/词法.md +++ b/Doc/compiler/词法.md @@ -29,6 +29,9 @@ TRUE FALSE TON TOF CTU ``` +> 12.5 修订:`THEN`、`DO` 补入关键字表(`IF ... THEN` / `WHILE ... DO` 语法需要), +> 与 12.1 原表合并,其余不变。 + 其余单词一律当标识符;`VAR_IN_OUT`、`REF`、`CLASS` 等由语法层(12.5)出明确错误。 ## Token diff --git a/Doc/compiler/语法.md b/Doc/compiler/语法.md new file mode 100644 index 0000000..11ef30f --- /dev/null +++ b/Doc/compiler/语法.md @@ -0,0 +1,183 @@ +# 语法(12.5 递归下降) + +compiler 模块的语法分析。输入:单个 `.st` 文件的 token 流(见 [`词法.md`](词法.md));输出:**AST**(不分配寄存器、不写映像)。全工程定案见 [`初步计划.md`](../初步计划.md) 12.1、12.5。 + +## 做 / 不做 + +**做** + +- POU 外壳:`PROGRAM` / `FUNCTION` / `FUNCTION_BLOCK` 与对应 `END_*` +- 变量段:`VAR` / `VAR_INPUT` / `VAR_OUTPUT` / `VAR_GLOBAL` / `VAR_EXTERNAL` +- 类型名只接受 `BOOL` `INT` `TIME`;内置 FB 名 `TON` `TOF` `CTU`;标识符(用户 FB 类型) +- 语句:赋值、`IF` / `ELSIF` / `ELSE`、`WHILE`、FB 调用 `fb(in := …)`、字段 `fb.Q` +- 表达式:`NOT`、`AND` / `OR`(AST 保留短路语义,12.8 编跳转)、比较、加减乘除、一元负号、函数调用 `Add(3, 4)` +- 明确拒绝(`syntax error`,不静默):`VAR_IN_OUT`、`VAR_TEMP`、`REF`、`CLASS`、`ANY` 等保留名 + +**不做(第一版)** + +- 寄存器分配、跳转回填、映像(12.8) +- 链式比较 `a < b < c`、字符串、`REAL`、指针、`VAR_IN_OUT`、接口、`TYPE` 定义 +- 语义检查(未声明变量等归 12.6 / 12.7) + +## AST 结构 + +```cpp +// 类型引用:类型位置出现的名字 +enum class TypeKind { Bool, Int, Time, FbBuiltin, FbUser }; +struct TypeRef { + TypeKind kind; + std::string name; // FbUser 时为用户 FB 类型名(小写) +}; + +struct VarDecl { + std::string name; + TypeRef type; + bool has_init; // 可选初值(v1 仅 BOOL/INT/TIME 字面量) + int64_t init_value; // BOOL 0/1、INT 值、TIME 毫秒 + uint32_t line, col; +}; + +enum class VarSection { Local, Input, Output, Global, External }; +struct VarBlock { + VarSection section; + std::vector vars; +}; + +// 表达式 +enum class ExprKind { + LitBool, LitInt, LitTime, + VarRef, // 标识符(局部/全局/外部/实例名) + Field, // 对象.字段:fb.Q / fb.ET + Not, And, Or, + Cmp, Add, Sub, Mul, Div, // 二目;比较不连锁 + Neg, // 一元负号 + Call, // 函数调用(仅 FUNCTION,有返回值) +}; +enum class BinOp { Eq, Ne, Lt, Le, Gt, Ge, Add, Sub, Mul, Div }; + +struct Expr { + ExprKind kind; + BinOp op; // 二目运算 + std::string name; // VarRef / Field 对象名 / Call 函数名 + std::string field; // Field 字段名 + int64_t int_value; // 字面量值 + std::unique_ptr lhs, rhs, operand; // 二目 / NOT / Neg + std::vector> args; // Call 实参 +}; + +// 语句 +enum class StmtKind { Assign, If, While, FbCall }; +struct FbArg { std::string name; std::unique_ptr value; }; +struct IfBranch { std::unique_ptr cond; std::vector body; }; + +struct Stmt { + StmtKind kind; + // Assign + std::string target; // 左值标识符 + bool target_is_field; // 左值是 fb.field(v1:只允许读取,禁止赋值) + std::string field; + std::unique_ptr value; + // If / While + std::unique_ptr cond; + std::vector body; // IF/WHILE 体 + std::vector elsifs; + std::vector else_body; + // FbCall + std::string instance; + std::vector args; +}; + +enum class PouKind { Program, Function, FunctionBlock }; +struct POU { + PouKind kind; + std::string name; + TypeRef result_type; // FUNCTION 才有 + std::vector blocks; + std::vector body; + std::string source_file; +}; +``` + +## 文法(递归下降) + +```text +file := (VAR_GLOBAL vars END_VAR | POU)* +POU := PROGRAM ident var_block* body END_PROGRAM + | FUNCTION ident ':' type var_block* body END_FUNCTION + | FUNCTION_BLOCK ident var_block* body END_FUNCTION_BLOCK +var_block := VAR vars END_VAR + | VAR_INPUT vars END_VAR | VAR_OUTPUT vars END_VAR + | VAR_GLOBAL vars END_VAR | VAR_EXTERNAL vars END_VAR +vars := (ident (',' ident)* ':' type (':=' literal)? ';')* +type := BOOL | INT | TIME | TON | TOF | CTU | ident +body := stmt* +stmt := ident ('.' ident)? ':=' expr ';' (* 赋值 *) + | ident '(' arg (',' arg)* ')' ';' (* FB 调用 *) + | IF expr THEN body (ELSIF expr THEN body)* (ELSE body)? END_IF + | WHILE expr DO body END_WHILE +arg := ident ':=' expr +expr := or_expr +or_expr := and_expr (OR and_expr)* +and_expr := not_expr (AND not_expr)* +not_expr := NOT not_expr | cmp_expr +cmp_expr := add_expr (('='|'<>'|'<'|'<='|'>'|'>=') add_expr)? +add_expr := mul_expr (('+'|'-') mul_expr)* +mul_expr := unary (('*'|'/') unary)* +unary := '-' unary | primary +primary := INT_LIT | TIME_LIT | TRUE | FALSE | ident + | ident '.' ident (* fb.field *) + | ident '(' expr (',' expr)* ')' (* 函数调用 *) + | '(' expr ')' +``` + +要点: + +- `file` 顶层允许无 POU 的 `VAR_GLOBAL` 段(**GVL 文件形态**,如 `globals.st`); + 其他变量段出现在顶层 → syntax error。解析结果: + ```cpp + struct Unit { + std::vector globals; // 顶层 VAR_GLOBAL 段 + std::vector pous; + }; + ``` +- `THEN` / `DO` 是关键字(12.5 修订补入词法表,见 [`词法.md`](词法.md)) +- 赋值左侧 `ident.ident` 是字段引用,v1 只能读(右侧),左侧字段赋值报错 +- `ident(...)` 是**语句**即 FB 调用(无返回值);在**表达式**里即函数调用(有返回值)——靠上下文区分,与 12.1 语义一致 +- 比较不连锁(`a < b < c` → syntax error) +- `AND` / `OR` 短路语义保留在 AST(`And`/`Or` 节点),12.8 编成跳转 + +## 明确拒绝清单 + +`IDENT` 文本(小写后)命中下列保留名 → `syntax error: 'X' is not supported`: + +```text +var_in_out var_temp ref class any pointer interface method +``` + +## 错误 + +稳定前缀 `syntax error`,带文件与行列: + +```text +syntax error: expected 'END_PROGRAM' (main.st, line 5, col 3) +syntax error: 'VAR_IN_OUT' is not supported (motor.st, line 2, col 5) +syntax error: chained comparison not supported (main.st, line 7, col 15) +``` + +## 完成标准 + +1. `examples/line1/*.st` 三文件 parse 成功,AST 结构符合文法 +2. `parser_test`:AST 断言(POU 种类/变量段/语句/表达式优先级)+ 负例(缺 `END_*`、`VAR_IN_OUT`、链式比较、字段赋值、坏语句)→ `syntax error` +3. 全部构建 + `ctest` 无回归 +4. 用例 10 / 14 / 19 属 12.6 / 12.7 层(非语法层),不在本阶段点亮 + +--- + +## 执行计划(单步确认) + +1. **`Parser.h`(新建 `compiler/include/compiler/Parser.h`)**:上述 AST 结构 + `bool parse_pous(source_file, content, out, err)` +2. **`Parser.cpp` 前半**:POU 外壳 + 变量段 + 类型(文法 program / POU / var_block / vars / type) +3. **`Parser.cpp` 后半**:语句 + 表达式(优先级爬升)+ 拒绝清单 +4. **`parser_test`(`tests/src/parser_test.cpp` + CMake)**:line1 三文件 AST 断言 + 负例 +5. **验证**:`cmake --build` + `ctest`(新增 `parser_syntax` 后 6/6 全绿) +6. **提交**:`Parser.h/cpp` + `parser_test` + 文档 diff --git a/Doc/初步计划.md b/Doc/初步计划.md index 4b3163f..ab003b8 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -367,6 +367,7 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom `VAR` `VAR_INPUT` `VAR_OUTPUT` `VAR_GLOBAL` `VAR_EXTERNAL` `END_VAR` `BOOL` `INT` `TIME` `IF` `ELSIF` `ELSE` `END_IF` `WHILE` `END_WHILE` +`THEN` `DO` (12.5 修订补入:`IF…THEN` / `WHILE…DO` 语法需要) `AND` `OR` `NOT` `TRUE` `FALSE` `TON` `TOF` `CTU` diff --git a/Doc/索引.md b/Doc/索引.md index 3411680..c96b7b9 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -9,6 +9,7 @@ doc/ isa/指令与映像.md compiler/编译管线.md compiler/词法.md + compiler/语法.md vm/扫描周期.md executor/执行器入口.md ``` @@ -19,6 +20,7 @@ doc/ | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | +| [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index eba0270..7bab162 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -9,7 +9,8 @@ project(Compiler # 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 add_library(compiler STATIC ./src/Lexer.cpp - ./src/Project.cpp) + ./src/Project.cpp + ./src/Parser.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) diff --git a/compiler/include/compiler/Lexer.h b/compiler/include/compiler/Lexer.h index 153f5ac..0d3b4bf 100644 --- a/compiler/include/compiler/Lexer.h +++ b/compiler/include/compiler/Lexer.h @@ -37,6 +37,8 @@ namespace compiler { END_IF, WHILE, END_WHILE, + THEN, // 12.5 修订:IF/WHILE 语法需要,补入关键字表 + DO, // 同上 AND, OR, NOT, diff --git a/compiler/include/compiler/Parser.h b/compiler/include/compiler/Parser.h new file mode 100644 index 0000000..7295b86 --- /dev/null +++ b/compiler/include/compiler/Parser.h @@ -0,0 +1,133 @@ +/** + * @file Parser.h + * @brief ST 子集递归下降语法分析(AST 定义 + 入口) + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include +#include + +namespace compiler { + + // ---- 类型引用 ---- + + enum class TypeKind { Bool, Int, Time, FbBuiltin, FbUser }; + + struct TypeRef { + TypeKind kind = TypeKind::Bool; + std::string name; // FbUser 时为用户 FB 类型名(小写) + }; + + // ---- 变量段 ---- + + struct VarDecl { + std::string name; + TypeRef type; + bool has_init = false; // 可选初值(v1 仅字面量) + int64_t init_value = 0; // BOOL 0/1、INT 值、TIME 毫秒 + uint32_t line = 0; + uint32_t col = 0; + }; + + enum class VarSection { Local, Input, Output, Global, External }; + + struct VarBlock { + VarSection section = VarSection::Local; + std::vector vars; + }; + + // ---- 表达式 ---- + + enum class ExprKind { + LitBool, // int_value 0/1 + LitInt, // int_value + LitTime, // int_value 毫秒 + VarRef, // name + Field, // name.field + Not, // operand + And, Or, // lhs, rhs(短路语义保留到 12.8) + Cmp, Add, Sub, Mul, Div, // lhs, rhs,op + Neg, // operand(一元负号) + Call, // name + args(函数调用,有返回值) + }; + + enum class BinOp { Eq, Ne, Lt, Le, Gt, Ge, Add, Sub, Mul, Div }; + + struct Expr { + ExprKind kind = ExprKind::LitBool; + BinOp op = BinOp::Eq; // 二目运算 + std::string name; // VarRef / Field 对象 / Call 函数名 + std::string field; // Field 字段名 + int64_t int_value = 0; // 字面量值 + std::unique_ptr lhs; + std::unique_ptr rhs; + std::unique_ptr operand; // Not / Neg + std::vector> args; // Call 实参 + }; + + // ---- 语句 ---- + + enum class StmtKind { Assign, If, While, FbCall }; + + struct Stmt; // 前置声明(IfBranch / Stmt 相互引用) + + struct FbArg { + std::string name; + std::unique_ptr value; + }; + + struct IfBranch { + std::unique_ptr cond; + std::vector body; + }; + + // 前置声明:Stmt 内部自引用 + struct Stmt { + StmtKind kind = StmtKind::Assign; + // Assign + std::string target; // 左值标识符 + bool target_is_field = false; // 左值 fb.field(v1 禁止赋值,语法层拒绝) + std::string field; + std::unique_ptr value; + // If / While + std::unique_ptr cond; + std::vector body; + std::vector elsifs; + std::vector else_body; + // FbCall + std::string instance; + std::vector args; + }; + + // ---- POU ---- + + enum class PouKind { Program, Function, FunctionBlock }; + + struct POU { + PouKind kind = PouKind::Program; + std::string name; + TypeRef result_type; // FUNCTION 才有 + std::vector blocks; + std::vector body; + std::string source_file; + }; + + // 一个 .st 文件的解析结果:GVL 文件形态(无 POU)时只有 globals 非空 + struct Unit { + std::vector globals; // 顶层 VAR_GLOBAL 段(仅 GVL 文件) + std::vector pous; + }; + + // 解析一个 .st 文件的全部内容(顶层 VAR_GLOBAL 段或 POU)。 + // 失败返回 false,err 前缀 "syntax error",带文件与行列。 + bool parse_pous(const std::string& source_file, const std::string& content, + Unit* out, std::string* err); + + // 读文件后解析 + bool parse_pous_file(const std::string& path, Unit* out, std::string* err); +} diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index c35616e..4131e17 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -41,6 +41,8 @@ namespace { {"end_if", Tok::END_IF}, {"while", Tok::WHILE}, {"end_while", Tok::END_WHILE}, + {"then", Tok::THEN}, + {"do", Tok::DO}, {"and", Tok::AND}, {"or", Tok::OR}, {"not", Tok::NOT}, diff --git a/compiler/src/Parser.cpp b/compiler/src/Parser.cpp new file mode 100644 index 0000000..6778934 --- /dev/null +++ b/compiler/src/Parser.cpp @@ -0,0 +1,747 @@ +/** + * @file Parser.cpp + * @brief ST 子集递归下降语法分析 + * @author + * @date 2026-08-21 + */ + +#include "compiler/Parser.h" + +#include +#include +#include + +#include "compiler/Lexer.h" + +namespace compiler { +namespace { + + // 明确拒绝的保留名(小写命中即报错,见 Doc/compiler/语法.md) + const char* const kForbidden[] = { + "var_in_out", "var_temp", "ref", "class", "any", + "pointer", "interface", "method", + }; + + bool is_forbidden(const std::string& word) { + for (const char* f : kForbidden) { + if (word == f) { + return true; + } + } + return false; + } + + class Parser { + public: + Parser(const std::string& source_file, const std::string& content, + Unit* out, std::string* err) + : err_(err), out_(out) { + std::string lex_err; + if (!lex(source_file, content, &tokens_, &lex_err)) { + ok_ = false; + *err_ = lex_err; + return; + } + pos_ = 0; + } + + bool run() { + if (!ok_) { + return false; + } + while (!check(Tok::END)) { + if (is_var_section()) { + // 顶层变量段只允许 VAR_GLOBAL(GVL 文件形态) + if (!check(Tok::VAR_GLOBAL)) { + return fail("only VAR_GLOBAL allowed at file top level"); + } + VarBlock b; + if (!parse_var_block(&b)) { + return false; + } + out_->globals.push_back(std::move(b)); + continue; + } + POU p; + if (!parse_pou(&p)) { + return false; + } + out_->pous.push_back(std::move(p)); + } + return true; + } + + private: + // ---- 错误 ---- + + bool fail(const std::string& msg) { + if (err_) { + const Token& t = cur(); + char buf[128]; + std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)", + t.source_file.c_str(), t.line, t.col); + *err_ = "syntax error: " + msg + buf; + } + return false; + } + + // ---- token 视图 ---- + + const Token& cur() const { return tokens_[pos_]; } + Tok type() const { return cur().type; } + bool check(Tok t) const { return type() == t; } + + const Token& advance() { + const Token& t = tokens_[pos_]; + if (pos_ + 1 < tokens_.size()) { + ++pos_; + } + return t; + } + + bool expect(Tok t, const char* what) { + if (!check(t)) { + return fail(std::string("expected '") + what + "'"); + } + advance(); + return true; + } + + // ---- POU ---- + + bool parse_pou(POU* p) { + if (check(Tok::PROGRAM)) { + advance(); + p->kind = PouKind::Program; + } else if (check(Tok::FUNCTION)) { + advance(); + p->kind = PouKind::Function; + } else if (check(Tok::FUNCTION_BLOCK)) { + advance(); + p->kind = PouKind::FunctionBlock; + } else { + return fail("expected POU (PROGRAM / FUNCTION / FUNCTION_BLOCK)"); + } + + if (!parse_ident(&p->name)) { + return false; + } + + if (p->kind == PouKind::Function) { + if (!expect(Tok::COLON, ":")) { + return false; + } + if (!parse_type(&p->result_type)) { + return false; + } + } + + while (is_var_section()) { + VarBlock b; + if (!parse_var_block(&b)) { + return false; + } + p->blocks.push_back(std::move(b)); + } + + while (!check_end_for(p->kind)) { + Stmt s; + if (!parse_stmt(&s)) { + return false; + } + p->body.push_back(std::move(s)); + } + + const char* end_kw = nullptr; + switch (p->kind) { + case PouKind::Program: end_kw = "END_PROGRAM"; break; + case PouKind::Function: end_kw = "END_FUNCTION"; break; + case PouKind::FunctionBlock: end_kw = "END_FUNCTION_BLOCK"; break; + } + return expect(end_tok_for(p->kind), end_kw); + } + + bool is_var_section() const { + switch (type()) { + case Tok::VAR: + case Tok::VAR_INPUT: + case Tok::VAR_OUTPUT: + case Tok::VAR_GLOBAL: + case Tok::VAR_EXTERNAL: + return true; + default: + return false; + } + } + + Tok end_tok_for(PouKind k) const { + switch (k) { + case PouKind::Program: return Tok::END_PROGRAM; + case PouKind::Function: return Tok::END_FUNCTION; + case PouKind::FunctionBlock: return Tok::END_FUNCTION_BLOCK; + } + return Tok::END; + } + + bool check_end_for(PouKind k) const { return check(end_tok_for(k)); } + + // ---- 变量段 ---- + + bool parse_var_block(VarBlock* b) { + switch (type()) { + case Tok::VAR: b->section = VarSection::Local; break; + case Tok::VAR_INPUT: b->section = VarSection::Input; break; + case Tok::VAR_OUTPUT: b->section = VarSection::Output; break; + case Tok::VAR_GLOBAL: b->section = VarSection::Global; break; + case Tok::VAR_EXTERNAL: b->section = VarSection::External; break; + default: + return fail("expected variable section"); + } + advance(); + + while (!check(Tok::END_VAR)) { + std::vector line; + if (!parse_vars_line(&line)) { + return false; + } + for (VarDecl& d : line) { + b->vars.push_back(std::move(d)); + } + } + advance(); // END_VAR + return true; + } + + // ident (',' ident)* ':' type (':=' literal)? ';' —— 展开成多个 VarDecl + bool parse_vars_line(std::vector* out) { + std::vector names; + while (true) { + VarDecl d; + d.line = cur().line; + d.col = cur().col; + if (!parse_ident(&d.name)) { + return false; + } + names.push_back(std::move(d)); + if (!check(Tok::COMMA)) { + break; + } + advance(); + } + if (!expect(Tok::COLON, ":")) { + return false; + } + TypeRef t; + if (!parse_type(&t)) { + return false; + } + bool has_init = false; + int64_t init = 0; + if (check(Tok::ASSIGN)) { + advance(); + Expr e; + if (!parse_literal_expr(&e)) { + return false; + } + has_init = true; + init = e.int_value; + } + if (!expect(Tok::SEMI, ";")) { + return false; + } + for (VarDecl& d : names) { + d.type = t; + d.has_init = has_init; + d.init_value = init; + out->push_back(std::move(d)); + } + return true; + } + + // 初值只允许字面量(BOOL / INT / TIME / TRUE / FALSE) + bool parse_literal_expr(Expr* e) { + switch (type()) { + case Tok::TRUE: + e->kind = ExprKind::LitBool; + e->int_value = 1; + advance(); + return true; + case Tok::FALSE: + e->kind = ExprKind::LitBool; + e->int_value = 0; + advance(); + return true; + case Tok::INT_LIT: + e->kind = ExprKind::LitInt; + e->int_value = cur().int_value; + advance(); + return true; + case Tok::TIME_LIT: + e->kind = ExprKind::LitTime; + e->int_value = cur().int_value; + advance(); + return true; + default: + return fail("expected literal initializer"); + } + } + + // 类型:BOOL | INT | TIME | TON | TOF | CTU | ident(用户 FB 类型) + bool parse_type(TypeRef* t) { + switch (type()) { + case Tok::BOOL: + t->kind = TypeKind::Bool; + advance(); + return true; + case Tok::INT: + t->kind = TypeKind::Int; + advance(); + return true; + case Tok::TIME: + t->kind = TypeKind::Time; + advance(); + return true; + case Tok::TON: + t->kind = TypeKind::FbBuiltin; + t->name = "ton"; + advance(); + return true; + case Tok::TOF: + t->kind = TypeKind::FbBuiltin; + t->name = "tof"; + advance(); + return true; + case Tok::CTU: + t->kind = TypeKind::FbBuiltin; + t->name = "ctu"; + advance(); + return true; + case Tok::IDENT: { + const std::string word = cur().text; + if (is_forbidden(word)) { + return fail("'" + word + "' is not supported"); + } + t->kind = TypeKind::FbUser; + t->name = word; + advance(); + return true; + } + default: + return fail("expected type name"); + } + } + + // ---- 语句 ---- + + bool parse_stmt(Stmt* s) { + if (check(Tok::IF)) { + return parse_if(s); + } + if (check(Tok::WHILE)) { + return parse_while(s); + } + if (!check(Tok::IDENT)) { + return fail("expected statement"); + } + + // ident '(' → FB 调用;ident ('.' ident)? ':=' → 赋值 + const size_t save = pos_; + advance(); + const bool is_call = check(Tok::LPAREN); + pos_ = save; + return is_call ? parse_fb_call(s) : parse_assign(s); + } + + bool parse_assign(Stmt* s) { + s->kind = StmtKind::Assign; + if (!parse_ident(&s->target)) { + return false; + } + if (check(Tok::DOT)) { + return fail("assignment to FB field is not supported"); + } + if (!expect(Tok::ASSIGN, ":=")) { + return false; + } + s->value = std::make_unique(); + if (!parse_expr(s->value.get())) { + return false; + } + return expect(Tok::SEMI, ";"); + } + + bool parse_fb_call(Stmt* s) { + s->kind = StmtKind::FbCall; + if (!parse_ident(&s->instance)) { + return false; + } + if (!expect(Tok::LPAREN, "(")) { + return false; + } + if (!check(Tok::RPAREN)) { + while (true) { + FbArg a; + if (!parse_ident(&a.name)) { + return false; + } + if (!expect(Tok::ASSIGN, ":=")) { + return false; + } + a.value = std::make_unique(); + if (!parse_expr(a.value.get())) { + return false; + } + s->args.push_back(std::move(a)); + if (!check(Tok::COMMA)) { + break; + } + advance(); + } + } + if (!expect(Tok::RPAREN, ")")) { + return false; + } + return expect(Tok::SEMI, ";"); + } + + bool parse_if(Stmt* s) { + s->kind = StmtKind::If; + advance(); // IF + s->cond = std::make_unique(); + if (!parse_expr(s->cond.get())) { + return false; + } + if (!expect(Tok::THEN, "THEN")) { + return false; + } + if (!parse_body(&s->body)) { + return false; + } + while (check(Tok::ELSIF)) { + advance(); + IfBranch b; + b.cond = std::make_unique(); + if (!parse_expr(b.cond.get())) { + return false; + } + if (!expect(Tok::THEN, "THEN")) { + return false; + } + if (!parse_body(&b.body)) { + return false; + } + s->elsifs.push_back(std::move(b)); + } + if (check(Tok::ELSE)) { + advance(); + if (!parse_body(&s->else_body)) { + return false; + } + } + return expect(Tok::END_IF, "END_IF"); + } + + bool parse_while(Stmt* s) { + s->kind = StmtKind::While; + advance(); // WHILE + s->cond = std::make_unique(); + if (!parse_expr(s->cond.get())) { + return false; + } + if (!expect(Tok::DO, "DO")) { + return false; + } + if (!parse_body(&s->body)) { + return false; + } + return expect(Tok::END_WHILE, "END_WHILE"); + } + + bool parse_body(std::vector* out) { + while (!check(Tok::ELSIF) && !check(Tok::ELSE) && !check(Tok::END_IF) && + !check(Tok::END_WHILE) && !check(Tok::END)) { + Stmt s; + if (!parse_stmt(&s)) { + return false; + } + out->push_back(std::move(s)); + } + return true; + } + + // ---- 表达式(优先级爬升)---- + + bool parse_expr(Expr* e) { return parse_or(e); } + + bool parse_or(Expr* e) { + std::unique_ptr lhs = std::make_unique(); + if (!parse_and(lhs.get())) { + return false; + } + while (check(Tok::OR)) { + advance(); + auto node = std::make_unique(); + node->kind = ExprKind::Or; + node->lhs = std::move(lhs); + node->rhs = std::make_unique(); + if (!parse_and(node->rhs.get())) { + return false; + } + lhs = std::move(node); + } + *e = std::move(*lhs); + return true; + } + + bool parse_and(Expr* e) { + std::unique_ptr lhs = std::make_unique(); + if (!parse_not(lhs.get())) { + return false; + } + while (check(Tok::AND)) { + advance(); + auto node = std::make_unique(); + node->kind = ExprKind::And; + node->lhs = std::move(lhs); + node->rhs = std::make_unique(); + if (!parse_not(node->rhs.get())) { + return false; + } + lhs = std::move(node); + } + *e = std::move(*lhs); + return true; + } + + bool parse_not(Expr* e) { + if (check(Tok::NOT)) { + advance(); + e->kind = ExprKind::Not; + e->operand = std::make_unique(); + return parse_not(e->operand.get()); + } + return parse_cmp(e); + } + + bool parse_cmp(Expr* e) { + std::unique_ptr lhs = std::make_unique(); + if (!parse_add(lhs.get())) { + return false; + } + BinOp op; + switch (type()) { + case Tok::EQ: op = BinOp::Eq; break; + case Tok::NE: op = BinOp::Ne; break; + case Tok::LT: op = BinOp::Lt; break; + case Tok::LE: op = BinOp::Le; break; + case Tok::GT: op = BinOp::Gt; break; + case Tok::GE: op = BinOp::Ge; break; + default: + *e = std::move(*lhs); + return true; + } + advance(); + auto rhs = std::make_unique(); + if (!parse_add(rhs.get())) { + return false; + } + if (is_cmp_op(type())) { + return fail("chained comparison not supported"); + } + e->kind = ExprKind::Cmp; + e->op = op; + e->lhs = std::move(lhs); + e->rhs = std::move(rhs); + return true; + } + + bool is_cmp_op(Tok t) const { + return t == Tok::EQ || t == Tok::NE || t == Tok::LT || t == Tok::LE || + t == Tok::GT || t == Tok::GE; + } + + bool parse_add(Expr* e) { + std::unique_ptr lhs = std::make_unique(); + if (!parse_mul(lhs.get())) { + return false; + } + while (true) { + ExprKind k; + if (check(Tok::PLUS)) { + k = ExprKind::Add; + } else if (check(Tok::MINUS)) { + k = ExprKind::Sub; + } else { + break; + } + advance(); + auto node = std::make_unique(); + node->kind = k; + node->lhs = std::move(lhs); + node->rhs = std::make_unique(); + if (!parse_mul(node->rhs.get())) { + return false; + } + lhs = std::move(node); + } + *e = std::move(*lhs); + return true; + } + + bool parse_mul(Expr* e) { + std::unique_ptr lhs = std::make_unique(); + if (!parse_unary(lhs.get())) { + return false; + } + while (true) { + ExprKind k; + if (check(Tok::STAR)) { + k = ExprKind::Mul; + } else if (check(Tok::SLASH)) { + k = ExprKind::Div; + } else { + break; + } + advance(); + auto node = std::make_unique(); + node->kind = k; + node->lhs = std::move(lhs); + node->rhs = std::make_unique(); + if (!parse_unary(node->rhs.get())) { + return false; + } + lhs = std::move(node); + } + *e = std::move(*lhs); + return true; + } + + bool parse_unary(Expr* e) { + if (check(Tok::MINUS)) { + advance(); + e->kind = ExprKind::Neg; + e->operand = std::make_unique(); + return parse_unary(e->operand.get()); + } + return parse_primary(e); + } + + bool parse_primary(Expr* e) { + switch (type()) { + case Tok::INT_LIT: + e->kind = ExprKind::LitInt; + e->int_value = cur().int_value; + advance(); + return true; + case Tok::TIME_LIT: + e->kind = ExprKind::LitTime; + e->int_value = cur().int_value; + advance(); + return true; + case Tok::TRUE: + e->kind = ExprKind::LitBool; + e->int_value = 1; + advance(); + return true; + case Tok::FALSE: + e->kind = ExprKind::LitBool; + e->int_value = 0; + advance(); + return true; + case Tok::LPAREN: { + advance(); + if (!parse_expr(e)) { + return false; + } + return expect(Tok::RPAREN, ")"); + } + case Tok::IDENT: { + const std::string word = cur().text; + if (is_forbidden(word)) { + return fail("'" + word + "' is not supported"); + } + advance(); + if (check(Tok::DOT)) { + // fb.field + advance(); + e->kind = ExprKind::Field; + e->name = word; + if (!parse_ident(&e->field)) { + return false; + } + return true; + } + if (check(Tok::LPAREN)) { + // 函数调用(FUNCTION,有返回值) + e->kind = ExprKind::Call; + e->name = word; + advance(); + if (!check(Tok::RPAREN)) { + while (true) { + auto arg = std::make_unique(); + if (!parse_expr(arg.get())) { + return false; + } + e->args.push_back(std::move(arg)); + if (!check(Tok::COMMA)) { + break; + } + advance(); + } + } + return expect(Tok::RPAREN, ")"); + } + e->kind = ExprKind::VarRef; + e->name = word; + return true; + } + default: + return fail("expected expression"); + } + } + + bool parse_ident(std::string* out) { + if (!check(Tok::IDENT)) { + return fail("expected identifier"); + } + if (is_forbidden(cur().text)) { + return fail("'" + cur().text + "' is not supported"); + } + *out = cur().text; + advance(); + return true; + } + + // ---- 成员 ---- + + std::string* err_; + std::vector tokens_; + Unit* out_; + size_t pos_ = 0; + bool ok_ = true; + }; + +} // namespace + +bool parse_pous(const std::string& source_file, const std::string& content, + Unit* out, std::string* err) { + out->globals.clear(); + out->pous.clear(); + Parser p(source_file, content, out, err); + return p.run(); +} + +bool parse_pous_file(const std::string& path, Unit* out, std::string* err) { + std::ifstream in(path, std::ios::binary); + if (!in) { + if (err) { + *err = "syntax error: cannot open file (" + path + ")"; + } + return false; + } + std::string content((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + return parse_pous(path, content, out, err); +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index abc0ccb..ea1428a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,3 +34,14 @@ target_compile_definitions(lexer_test PRIVATE add_test(NAME lexer_tokens COMMAND lexer_test) + +# 语法测试:line1 AST 结构 + 负例(REPO_ROOT 注入源目录绝对路径) +add_executable(parser_test + ./src/parser_test.cpp) + +target_link_libraries(parser_test PRIVATE compiler) +target_compile_definitions(parser_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME parser_syntax + COMMAND parser_test) diff --git a/tests/src/parser_test.cpp b/tests/src/parser_test.cpp new file mode 100644 index 0000000..9f8954f --- /dev/null +++ b/tests/src/parser_test.cpp @@ -0,0 +1,301 @@ +/** + * @file parser_test.cpp + * @brief 语法测试:line1 AST 结构 + 负例 + * @author + * @date 2026-08-21 + */ + +#include +#include + +#include "compiler/Parser.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 1. GVL 文件(顶层 VAR_GLOBAL,无 POU)---- + +static bool test_gvl_file() { + using namespace compiler; + Unit u; + std::string err; + CHECK(parse_pous_file(std::string(REPO_ROOT) + "/examples/line1/globals.st", &u, &err)); + CHECK(u.globals.size() == 1); + CHECK(u.pous.empty()); + const VarBlock& g = u.globals[0]; + CHECK(g.section == VarSection::Global); + CHECK(g.vars.size() == 4); + CHECK(g.vars[0].name == "emergencystop"); + CHECK(g.vars[0].type.kind == TypeKind::Bool); + CHECK(g.vars[0].has_init && g.vars[0].init_value == 0); // := FALSE + CHECK(!g.vars[1].has_init); // I0_0 + return true; +} + +// ---- 2. motor.st:FUNCTION_BLOCK ---- + +static bool test_fb() { + using namespace compiler; + Unit u; + std::string err; + CHECK(parse_pous_file(std::string(REPO_ROOT) + "/examples/line1/motor.st", &u, &err)); + CHECK(u.globals.empty()); + CHECK(u.pous.size() == 1); + const POU& p = u.pous[0]; + CHECK(p.kind == PouKind::FunctionBlock); + CHECK(p.name == "motorstarter"); + CHECK(p.blocks.size() == 3); + CHECK(p.blocks[0].section == VarSection::External); + CHECK(p.blocks[0].vars[0].name == "emergencystop"); + CHECK(p.blocks[1].section == VarSection::Input); + CHECK(p.blocks[1].vars.size() == 2); + CHECK(p.blocks[2].section == VarSection::Output); + CHECK(p.blocks[2].vars[0].name == "q"); + CHECK(p.body.size() == 1); + const Stmt& s = p.body[0]; + CHECK(s.kind == StmtKind::Assign); + CHECK(s.target == "q"); + CHECK(!s.target_is_field); + // Q := start AND NOT stop AND NOT EmergencyStop + // 左结合:(start AND NOT stop) AND NOT EmergencyStop + CHECK(s.value->kind == ExprKind::And); + CHECK(s.value->rhs->kind == ExprKind::Not); + CHECK(s.value->rhs->operand->kind == ExprKind::VarRef && + s.value->rhs->operand->name == "emergencystop"); + CHECK(s.value->lhs->kind == ExprKind::And); + CHECK(s.value->lhs->lhs->kind == ExprKind::VarRef && + s.value->lhs->lhs->name == "start"); + CHECK(s.value->lhs->rhs->kind == ExprKind::Not); + CHECK(s.value->lhs->rhs->operand->kind == ExprKind::VarRef && + s.value->lhs->rhs->operand->name == "stop"); + return true; +} + +// ---- 3. main.st:PROGRAM + FB 调用 + 字段读 ---- + +static bool test_program() { + using namespace compiler; + Unit u; + std::string err; + CHECK(parse_pous_file(std::string(REPO_ROOT) + "/examples/line1/main.st", &u, &err)); + CHECK(u.pous.size() == 1); + const POU& p = u.pous[0]; + CHECK(p.kind == PouKind::Program); + CHECK(p.name == "main"); + CHECK(p.blocks.size() == 1); + CHECK(p.blocks[0].section == VarSection::Local); + CHECK(p.blocks[0].vars[0].name == "starter"); + CHECK(p.blocks[0].vars[0].type.kind == TypeKind::FbUser); + CHECK(p.blocks[0].vars[0].type.name == "motorstarter"); + + CHECK(p.body.size() == 2); + const Stmt& call = p.body[0]; + CHECK(call.kind == StmtKind::FbCall); + CHECK(call.instance == "starter"); + CHECK(call.args.size() == 2); + CHECK(call.args[0].name == "start"); + CHECK(call.args[0].value->kind == ExprKind::VarRef && + call.args[0].value->name == "i0_0"); + CHECK(call.args[1].name == "stop"); + CHECK(call.args[1].value->kind == ExprKind::VarRef && + call.args[1].value->name == "i0_1"); + + const Stmt& assign = p.body[1]; + CHECK(assign.kind == StmtKind::Assign); + CHECK(assign.target == "q0_0"); + CHECK(assign.value->kind == ExprKind::Field); + CHECK(assign.value->name == "starter"); + CHECK(assign.value->field == "q"); + return true; +} + +// ---- 4. FUNCTION:返回值类型 + 函数调用表达式 ---- + +static bool test_function() { + using namespace compiler; + Unit u; + std::string err; + const char* src = + "FUNCTION Add : INT\n" + "VAR_INPUT\n" + " a, b : INT;\n" + "END_VAR\n" + " Add := a + b;\n" + "END_FUNCTION\n" + "PROGRAM MAIN\n" + "VAR\n" + " x : INT;\n" + "END_VAR\n" + " x := Add(3, 4) * 2;\n" + "END_PROGRAM\n"; + CHECK(parse_pous("t.st", src, &u, &err)); + CHECK(u.pous.size() == 2); + const POU& f = u.pous[0]; + CHECK(f.kind == PouKind::Function); + CHECK(f.name == "add"); + CHECK(f.result_type.kind == TypeKind::Int); + CHECK(f.body[0].kind == StmtKind::Assign); + CHECK(f.body[0].value->kind == ExprKind::Add); + + const POU& m = u.pous[1]; + const Stmt& s = m.body[0]; + CHECK(s.value->kind == ExprKind::Mul); + CHECK(s.value->lhs->kind == ExprKind::Call); + CHECK(s.value->lhs->name == "add"); + CHECK(s.value->lhs->args.size() == 2); + CHECK(s.value->lhs->args[0]->kind == ExprKind::LitInt && + s.value->lhs->args[0]->int_value == 3); + CHECK(s.value->lhs->args[1]->kind == ExprKind::LitInt && + s.value->lhs->args[1]->int_value == 4); + return true; +} + +// ---- 5. IF / WHILE / 内建 FB 类型 ---- + +static bool test_control() { + using namespace compiler; + Unit u; + std::string err; + const char* src = + "PROGRAM MAIN\n" + "VAR\n" + " sel : INT;\n" + " out : INT;\n" + " t : TON;\n" + " n : INT;\n" + "END_VAR\n" + " IF sel = 0 THEN\n" + " out := 10;\n" + " ELSIF sel = 1 THEN\n" + " out := 20;\n" + " ELSE\n" + " out := 30;\n" + " END_IF\n" + " n := 0;\n" + " WHILE n < 10 DO\n" + " n := n + 1;\n" + " END_WHILE\n" + " t(in := TRUE, pt := T#30ms);\n" + "END_PROGRAM\n"; + CHECK(parse_pous("t.st", src, &u, &err)); + const POU& p = u.pous[0]; + CHECK(p.blocks[0].vars[2].name == "t"); + CHECK(p.blocks[0].vars[2].type.kind == TypeKind::FbBuiltin); + CHECK(p.blocks[0].vars[2].type.name == "ton"); + + CHECK(p.body.size() == 4); + const Stmt& ifs = p.body[0]; + CHECK(ifs.kind == StmtKind::If); + CHECK(ifs.cond->kind == ExprKind::Cmp); + CHECK(ifs.cond->op == BinOp::Eq); + CHECK(ifs.cond->lhs->kind == ExprKind::VarRef && ifs.cond->lhs->name == "sel"); + CHECK(ifs.body.size() == 1); + CHECK(ifs.elsifs.size() == 1); + CHECK(ifs.elsifs[0].cond->lhs->name == "sel"); + CHECK(ifs.else_body.size() == 1); + + const Stmt& w = p.body[2]; + CHECK(w.kind == StmtKind::While); + CHECK(w.cond->kind == ExprKind::Cmp); + CHECK(w.cond->op == BinOp::Lt); + CHECK(w.body.size() == 1); + + const Stmt& ton = p.body[3]; + CHECK(ton.kind == StmtKind::FbCall); + CHECK(ton.instance == "t"); + CHECK(ton.args[1].name == "pt"); + CHECK(ton.args[1].value->kind == ExprKind::LitTime); + CHECK(ton.args[1].value->int_value == 30); + return true; +} + +// ---- 6. 负例 ---- + +static bool expect_syntax_err(const std::string& src, const char* keyword) { + compiler::Unit u; + std::string err; + if (compiler::parse_pous("t.st", src, &u, &err)) { + std::printf("FAIL: parsed ok: %s\n", src.c_str()); + return false; + } + if (err.find("syntax error") != 0) { + std::printf("FAIL: want 'syntax error', got '%s'\n", err.c_str()); + return false; + } + if (std::string(keyword).empty()) { + ++g_checks; + return true; + } + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL: want keyword '%s', got '%s'\n", keyword, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +static bool test_negative() { + // VAR_IN_OUT / REF / CLASS / ANY 明确拒绝 + if (!expect_syntax_err( + "PROGRAM MAIN\nVAR_IN_OUT\nx : INT;\nEND_VAR\nEND_PROGRAM", "not supported")) { + return false; + } + if (!expect_syntax_err("FUNCTION F : INT\n F := 1;\n REF := 2;\nEND_FUNCTION", + "not supported")) { + return false; + } + // 缺 END_* + if (!expect_syntax_err("PROGRAM MAIN\nIF x THEN\ny := 1;\nEND_PROGRAM", "")) { + return false; + } + if (!expect_syntax_err("FUNCTION_BLOCK F\nEND_PROGRAM", "")) { + return false; + } + // 链式比较 + if (!expect_syntax_err("PROGRAM MAIN\na := b < c < d;\nEND_PROGRAM", + "chained comparison")) { + return false; + } + // 字段赋值 + if (!expect_syntax_err("PROGRAM MAIN\nstarter.Q := TRUE;\nEND_PROGRAM", + "FB field")) { + return false; + } + // 顶层非 VAR_GLOBAL 段 + if (!expect_syntax_err("VAR\nx : INT;\nEND_VAR\n", "top level")) { + return false; + } + // 初值非字面量 + if (!expect_syntax_err("PROGRAM MAIN\nVAR\nx : INT := y;\nEND_VAR\nEND_PROGRAM", "")) { + return false; + } + // 坏类型名 + if (!expect_syntax_err("PROGRAM MAIN\nVAR\nx : ;\nEND_VAR\nEND_PROGRAM", + "expected type name")) { + return false; + } + return true; +} + +int main() { + if (!test_gvl_file()) return 1; + if (!test_fb()) return 1; + if (!test_program()) return 1; + if (!test_function()) return 1; + if (!test_control()) return 1; + if (!test_negative()) return 1; + std::printf("parser_test: %d checks passed\n", g_checks); + return 0; +} From 6f02c18bbb9b984bdaf6109cd26c631487b6ed05 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:14:03 +0800 Subject: [PATCH 22/69] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.6=20=E7=AC=A6?= =?UTF-8?q?=E5=8F=B7=E8=A1=A8=E4=B8=8E=E9=93=BE=E6=8E=A5=EF=BC=9A=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=AE=9A=E6=A7=BD=E3=80=81=E5=BC=95=E7=94=A8=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E3=80=81=E5=BE=AA=E7=8E=AF=E6=A3=80=E6=B5=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Linker.h/cpp:收集导出(仅 gvl 文件顶层 VAR_GLOBAL、声明顺序定槽、重复名拒绝) - VAR_EXTERNAL 接同一槽、类型一致;FB 实例必须先声明;字段存在性校验;函数调用图 DFS 环检测 - io.var 必须在 GVL;内建 TON/TOF/CTU 布局冻结;用户 FB 字段 = input/output/var(external 不是字段) - 函数名赋值 = 结果值写入(用例 13 约定);dump_symbols 打印 name kind type address file - linker_test:51 断言(用例 09/10/11/12/16/19 + line1/17 正例),ctest 7/7;20 用例扫描 15 绿 5 按期望拒 --- Doc/compiler/符号表与链接.md | 111 ++++++ Doc/索引.md | 2 + compiler/CMakeLists.txt | 3 +- compiler/include/compiler/Linker.h | 82 +++++ compiler/src/Linker.cpp | 548 +++++++++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/linker_test.cpp | 229 ++++++++++++ 7 files changed, 985 insertions(+), 1 deletion(-) create mode 100644 Doc/compiler/符号表与链接.md create mode 100644 compiler/include/compiler/Linker.h create mode 100644 compiler/src/Linker.cpp create mode 100644 tests/src/linker_test.cpp diff --git a/Doc/compiler/符号表与链接.md b/Doc/compiler/符号表与链接.md new file mode 100644 index 0000000..f540ed9 --- /dev/null +++ b/Doc/compiler/符号表与链接.md @@ -0,0 +1,111 @@ +# 符号表与链接(12.6) + +compiler 模块的符号收集与链接。输入:`Project`(toml)+ 各 `.st` 文件的 `Unit`(AST,见 [`语法.md`](语法.md));输出:链接结果(全局槽表、POU 符号表、函数表、FB 实例布局)。全工程定案见 [`初步计划.md`](../初步计划.md) 12.6。 + +## 做 / 不做 + +**做** + +- 先扫完全部文件再解析引用(禁止边解析边执行) +- 全局收集:仅 `gvl.file` 顶层 `VAR_GLOBAL`,声明顺序 = 全局槽顺序 +- `VAR_EXTERNAL` 接已有槽,种类/类型一致,不是新变量 +- `FUNCTION` 标无状态;`FUNCTION_BLOCK` 必须先有实例才能调用 +- `CALL` 目标解析成 `fn_id`(立即数),无函数指针 +- 校验:同名全局、未声明 `io.var`、重复 POU 名、未声明 FB 实例、函数循环调用 → 失败 +- FB 实例布局:按类型算字段清单(输入/输出/内部/内建 TON/TOF/CTU 固定字段) + +**不做(第一版)** + +- 寄存器分配(12.8)、类型检查(12.7) +- 多 GVL 文件、I/Q/M 与全局分槽(v1 **单一数据区**,见下) + +## 数据区定址(v1 定案) + +- **单一数据区**:全部全局(含 I/Q/M)按 `globals.st` 声明顺序编槽号 0..n-1 +- 映像头 `n_globals` = 声明总数;`n_i` / `n_q` / `n_m` = 0(第一版不拆,见 [`初步计划.md`](../初步计划.md) 5.1) +- `LOAD_I` / `STORE_Q` / `LOAD_GLOBAL` / `STORE_GLOBAL` 操作码不同,槽号指向同一数据区 +- io 绑定(toml)只负责 channel/bit → 槽号映射(sidecar),不创造变量 + +## 符号表一行 + +```cpp +enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const }; + +struct Symbol { + SymbolKind kind; + std::string name; // 小写 + std::string type_name; // bool / int / time / fb 类型名(小写) + uint32_t address; // 全局槽号(Global/External);POU 局部暂 0(12.8 分配) + std::string source_file; + uint32_t line, col; +}; +``` + +`kind` 与 12.1 冻结表一致:`global` / `external` / `local` / `input` / `output` / `pou` / `fb_instance` / `const`(`const` 本期未用,保留)。 + +## 链接流程 + +```text +1. 收集导出 + 遍历全部文件: + - 顶层 globals:仅 gvl.file 允许;同名 → duplicate global;声明顺序编槽号 + - POU 导出:program / function / fb 名登记;重复 → duplicate POU +2. 逐 POU 建局部符号 + VAR / VAR_INPUT / VAR_OUTPUT → local / input / output + VAR_EXTERNAL → 查全局表,必须存在、类型一致 → External(同一槽) + POU 内 VAR_GLOBAL → error(仅 gvl 顶层允许) +3. 语句引用解析 + - FB 调用 instance(...) → 局部表查实例,类型必须是 FB → 否则 undeclared FB instance + - 字段读 fb.field → 实例类型有该字段(用户 FB 的 input/output;内建固定字段) + - VarRef / 赋值左值 → 局部表或全局表查得到 → 否则 undeclared identifier + - 函数调用 fn(...) → POU 表查得到且是 FUNCTION → 记依赖边;FUNCTION 不得调用 PROGRAM/FB +4. 循环调用检测 + 函数依赖图 DFS 找环 → recursive call(用例 19) +5. io 校验 + io.var 必须在全局表 → 否则 io.var not in GVL(用例 12) +6. 输出 + 全局表 / POU 符号表 / 函数表(fn_id 按收集序)/ FB 实例布局 +``` + +## FB 实例布局 + +- 用户 FB:字段 = 全部 `VAR_INPUT` + `VAR_OUTPUT` + `VAR`(内部),按声明段序排 +- 内建(冻结,isa 规格): + +| FB | 字段(名 → 类型) | +|---|---| +| TON | in BOOL, pt TIME, q BOOL, et TIME | +| TOF | in BOOL, pt TIME, q BOOL, et TIME | +| CTU | cu BOOL, r BOOL, pv INT, q BOOL, cv INT | + +- 实例布局 = 类型字段表的一份拷贝;字段偏移 12.8 按对齐规则算 + +## 错误 + +稳定前缀 `link error`: + +```text +link error: duplicate global 'g1' (globals.st, line 4, col 1) +link error: VAR_GLOBAL only allowed in gvl file (other.st, line 2, col 1) +link error: io.var 'Nope' not in GVL (project.toml) +link error: undeclared FB instance 'starter' (main.st, line 4, col 5) +link error: recursive call involving 'a' (main.st, line 2, col 4) +link error: undeclared identifier 'x' (main.st, line 3, col 3) +``` + +## 完成标准 + +1. 用例 9(同一槽)、10(非 GVL 写全局)、11(同名全局)、12(io.var 未声明)、16(未声明实例)、19(循环调用)按表变绿 +2. 符号表能打印 `name kind type address file` +3. 全部构建 + `ctest` 无回归 + +--- + +## 执行计划(单步确认) + +1. **`Linker.h`(新建 `compiler/include/compiler/Linker.h`)**:`Symbol`/`SymbolKind` + 链接结果结构 + `link_project` 声明 +2. **`Linker.cpp` 前半**:收集导出(gvl 校验 / 全局定槽 / POU 登记)+ 逐 POU 局部符号 +3. **`Linker.cpp` 后半**:引用解析(FB 实例 / 字段 / 变量 / 函数)+ 循环检测 + io 校验 +4. **`linker_test`(`tests/src/linker_test.cpp` + CMake)**:用例 09/10/11/12/16/19 + 符号表打印断言 +5. **验证**:`cmake --build` + `ctest`(新增 `linker_links` 后 7/7 全绿) +6. **提交**:`Linker.h/cpp` + `linker_test` + 文档 diff --git a/Doc/索引.md b/Doc/索引.md index c96b7b9..e4a511d 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -10,6 +10,7 @@ doc/ compiler/编译管线.md compiler/词法.md compiler/语法.md + compiler/符号表与链接.md vm/扫描周期.md executor/执行器入口.md ``` @@ -21,6 +22,7 @@ doc/ | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | | [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 | +| [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 7bab162..40a8484 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -10,7 +10,8 @@ project(Compiler add_library(compiler STATIC ./src/Lexer.cpp ./src/Project.cpp - ./src/Parser.cpp) + ./src/Parser.cpp + ./src/Linker.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) diff --git a/compiler/include/compiler/Linker.h b/compiler/include/compiler/Linker.h new file mode 100644 index 0000000..34b4976 --- /dev/null +++ b/compiler/include/compiler/Linker.h @@ -0,0 +1,82 @@ +/** + * @file Linker.h + * @brief 符号表与链接 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include +#include + +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + // 符号种类(与 Doc/初步计划.md 12.1 一致) + enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const }; + + struct Symbol { + SymbolKind kind = SymbolKind::Local; + std::string name; // 小写 + std::string type_name; // bool / int / time / fb 类型名(小写) + uint32_t address = 0; // 全局槽号(Global/External);局部暂 0(12.8 分配) + std::string source_file; + uint32_t line = 0; + uint32_t col = 0; + }; + + // FB 类型字段(实例布局用;v1 字段类型仅 BOOL/INT/TIME) + struct FbField { + std::string name; + TypeKind type = TypeKind::Bool; + }; + + struct FbLayout { + std::string type_name; // 类型名(小写) + std::vector fields; // 段序:input → output → 内部 var + }; + + // 一个源文件的 AST(供链接输入) + struct SourceUnit { + std::string path; + Unit ast; + }; + + // 链接结果 + struct LinkResult { + // 全局槽表:下标即槽号(声明顺序) + std::vector globals; + std::map global_index; // 名 → 槽号 + + // 用户 FB 类型布局(按类型名) + std::map fb_types; + + // 每 POU 的局部符号与 FB 实例 + struct PouScope { + std::string name; + PouKind kind = PouKind::Program; + std::vector syms; // local/input/output/external/fb_instance + std::map fb_instances; // 实例名 → 布局 + }; + std::vector scopes; + + // 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序) + std::vector fn_order; + }; + + // 链接一个工程:校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、 + // 函数循环调用检测、io 校验。失败返回 false,err 前缀 "link error"。 + bool link_project(const Project& proj, const std::vector& units, + LinkResult* out, std::string* err); + + // 读取并解析一个 .st 文件成 SourceUnit + bool load_unit(const std::string& path, SourceUnit* out, std::string* err); + + // 符号表文本:每行 "name kind type address file"(全局表 + 各 POU 局部表) + std::string dump_symbols(const LinkResult& r); +} diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp new file mode 100644 index 0000000..c3ebaa3 --- /dev/null +++ b/compiler/src/Linker.cpp @@ -0,0 +1,548 @@ +/** + * @file Linker.cpp + * @brief 符号表与链接 + * @author + * @date 2026-08-21 + */ + +#include "compiler/Linker.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace compiler { +namespace { + + const char* kind_name(SymbolKind k) { + switch (k) { + case SymbolKind::Global: return "global"; + case SymbolKind::External: return "external"; + case SymbolKind::Local: return "local"; + case SymbolKind::Input: return "input"; + case SymbolKind::Output: return "output"; + case SymbolKind::Pou: return "pou"; + case SymbolKind::FbInstance: return "fb_instance"; + case SymbolKind::Const: return "const"; + } + return "?"; + } + + std::string type_str(const TypeRef& t) { + switch (t.kind) { + case TypeKind::Bool: return "bool"; + case TypeKind::Int: return "int"; + case TypeKind::Time: return "time"; + case TypeKind::FbBuiltin: + case TypeKind::FbUser: return t.name; + } + return "?"; + } + + bool is_scalar(TypeKind k) { + return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time; + } + + // 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md) + const FbLayout kTonLayout{"ton", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, + {"q", TypeKind::Bool}, {"et", TypeKind::Time}}}; + const FbLayout kTofLayout{"tof", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, + {"q", TypeKind::Bool}, {"et", TypeKind::Time}}}; + const FbLayout kCtuLayout{"ctu", {{"cu", TypeKind::Bool}, {"r", TypeKind::Bool}, + {"pv", TypeKind::Int}, {"q", TypeKind::Bool}, + {"cv", TypeKind::Int}}}; + + const FbLayout* builtin_layout(const std::string& name) { + if (name == "ton") return &kTonLayout; + if (name == "tof") return &kTofLayout; + if (name == "ctu") return &kCtuLayout; + return nullptr; + } + + class Linker { + public: + Linker(const Project& proj, const std::vector& units, + LinkResult* out, std::string* err) + : proj_(proj), units_(units), out_(out), err_(err) { + gvl_path_ = std::filesystem::weakly_canonical( + std::filesystem::path(proj.base_dir) / proj.gvl_file); + } + + bool run() { + if (!collect_exports()) return false; + if (!collect_fb_types()) return false; + if (!resolve_scopes()) return false; + if (!check_cycles()) return false; + if (!check_io()) return false; + if (!has_program_main()) { + return fail("", "missing entry 'program MAIN'", 0, 0); + } + return true; + } + + private: + bool fail(const std::string& file, const std::string& msg, + uint32_t line, uint32_t col) { + if (err_) { + char buf[256]; + if (line) { + std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)", + file.c_str(), line, col); + } else { + std::snprintf(buf, sizeof buf, " (%s)", file.c_str()); + } + *err_ = "link error: " + msg + buf; + } + return false; + } + + bool is_gvl_file(const std::string& path) const { + if (proj_.gvl_file.empty()) { + return false; + } + return std::filesystem::weakly_canonical(path) == gvl_path_; + } + + // ---- 1. 收集导出 ---- + + bool collect_exports() { + for (const SourceUnit& u : units_) { + const bool is_gvl = is_gvl_file(u.path); + + for (const VarBlock& b : u.ast.globals) { + if (!is_gvl) { + return fail(u.path, "VAR_GLOBAL only allowed in gvl file", + b.vars.empty() ? 0 : b.vars[0].line, + b.vars.empty() ? 0 : b.vars[0].col); + } + for (const VarDecl& d : b.vars) { + if (out_->global_index.count(d.name)) { + return fail(u.path, "duplicate global '" + d.name + "'", + d.line, d.col); + } + Symbol s; + s.kind = SymbolKind::Global; + s.name = d.name; + s.type_name = type_str(d.type); + s.address = static_cast(out_->globals.size()); + s.source_file = u.path; + s.line = d.line; + s.col = d.col; + out_->global_index[s.name] = s.address; + out_->globals.push_back(s); + } + } + + for (const POU& p : u.ast.pous) { + for (const LinkResult::PouScope& sc : out_->scopes) { + if (sc.name == p.name) { + return fail(u.path, "duplicate POU '" + p.name + "'", + 0, 0); + } + } + LinkResult::PouScope sc; + sc.name = p.name; + sc.kind = p.kind; + out_->scopes.push_back(std::move(sc)); + out_->fn_order.push_back(p.name); + } + } + return true; + } + + // ---- 2. 用户 FB 类型布局 ---- + + bool collect_fb_types() { + for (const SourceUnit& u : units_) { + for (const POU& p : u.ast.pous) { + if (p.kind != PouKind::FunctionBlock) { + continue; + } + FbLayout lay; + lay.type_name = p.name; + for (const VarBlock& b : p.blocks) { + // 字段 = input / output / 内部 var;external 与 global 不是字段 + if (b.section == VarSection::External || + b.section == VarSection::Global) { + continue; + } + for (const VarDecl& d : b.vars) { + if (!is_scalar(d.type.kind)) { + return fail(u.path, + "FB field type must be BOOL/INT/TIME ('" + + d.name + "')", + d.line, d.col); + } + FbField f; + f.name = d.name; + f.type = d.type.kind; + lay.fields.push_back(f); + } + } + out_->fb_types[p.name] = lay; + } + } + return true; + } + + // ---- 3. 逐 POU 建局部符号并解析引用 ---- + + bool resolve_scopes() { + for (SourceUnit& u : const_cast&>(units_)) { + for (const POU& p : u.ast.pous) { + LinkResult::PouScope& sc = scope_of(p.name); + if (!build_scope_syms(u.path, p, sc)) { + return false; + } + for (const Stmt& st : p.body) { + if (!check_stmt(u.path, sc, p, st)) { + return false; + } + } + } + } + return true; + } + + LinkResult::PouScope& scope_of(const std::string& name) { + for (LinkResult::PouScope& sc : out_->scopes) { + if (sc.name == name) { + return sc; + } + } + static LinkResult::PouScope kEmpty; + return kEmpty; + } + + bool build_scope_syms(const std::string& file, const POU& p, + LinkResult::PouScope& sc) { + for (const VarBlock& b : p.blocks) { + for (const VarDecl& d : b.vars) { + switch (b.section) { + case VarSection::Global: + return fail(file, "VAR_GLOBAL only allowed in gvl file", + d.line, d.col); + case VarSection::External: { + const auto it = out_->global_index.find(d.name); + if (it == out_->global_index.end()) { + return fail(file, "undeclared external '" + d.name + "'", + d.line, d.col); + } + const Symbol& g = out_->globals[it->second]; + if (g.type_name != type_str(d.type)) { + return fail(file, "external type mismatch for '" + d.name + "'", + d.line, d.col); + } + Symbol s; + s.kind = SymbolKind::External; + s.name = d.name; + s.type_name = g.type_name; + s.address = g.address; + s.source_file = file; + s.line = d.line; + s.col = d.col; + sc.syms.push_back(s); + break; + } + default: { + const bool is_fb = d.type.kind == TypeKind::FbUser || + d.type.kind == TypeKind::FbBuiltin; + Symbol s; + s.kind = is_fb ? SymbolKind::FbInstance + : (b.section == VarSection::Input + ? SymbolKind::Input + : b.section == VarSection::Output + ? SymbolKind::Output + : SymbolKind::Local); + s.name = d.name; + s.type_name = type_str(d.type); + s.source_file = file; + s.line = d.line; + s.col = d.col; + sc.syms.push_back(s); + if (is_fb) { + FbLayout lay; + if (d.type.kind == TypeKind::FbBuiltin) { + const FbLayout* bl = builtin_layout(d.type.name); + if (!bl) { + return fail(file, "unknown builtin FB '" + d.type.name + "'", + d.line, d.col); + } + lay = *bl; + } else { + const auto it = out_->fb_types.find(d.type.name); + if (it == out_->fb_types.end()) { + return fail(file, "unknown FB type '" + d.type.name + "'", + d.line, d.col); + } + lay = it->second; + } + sc.fb_instances[d.name] = lay; + } + break; + } + } + } + } + return true; + } + + // ---- 引用解析 ---- + + const Symbol* find_sym(const LinkResult::PouScope& sc, + const std::string& name) const { + for (const Symbol& s : sc.syms) { + if (s.name == name) { + return &s; + } + } + return nullptr; + } + + bool check_stmt(const std::string& file, const LinkResult::PouScope& sc, + const POU& pou, const Stmt& st) { + switch (st.kind) { + case StmtKind::Assign: { + // FUNCTION 内对函数名赋值 = 结果值写入(用例 13 约定) + const bool is_result_assign = + pou.kind == PouKind::Function && st.target == pou.name; + if (!is_result_assign && find_sym(sc, st.target) == nullptr && + out_->global_index.count(st.target) == 0) { + return fail(file, "undeclared identifier '" + st.target + "'", + 0, 0); + } + return check_expr(file, sc, *st.value); + } + case StmtKind::FbCall: { + const Symbol* inst = find_sym(sc, st.instance); + if (inst == nullptr) { + return fail(file, "undeclared FB instance '" + st.instance + "'", + 0, 0); + } + if (inst->kind != SymbolKind::FbInstance) { + return fail(file, "'" + st.instance + "' is not an FB instance", + 0, 0); + } + const auto it = sc.fb_instances.find(st.instance); + const FbLayout& lay = it->second; + for (const FbArg& a : st.args) { + bool found = false; + for (const FbField& f : lay.fields) { + if (f.name == a.name) { + found = true; + break; + } + } + if (!found) { + return fail(file, + "unknown input '" + a.name + "' for FB '" + + st.instance + "'", + 0, 0); + } + if (!check_expr(file, sc, *a.value)) { + return false; + } + } + return true; + } + case StmtKind::If: + if (!check_expr(file, sc, *st.cond)) { + return false; + } + for (const Stmt& s : st.body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + for (const IfBranch& b : st.elsifs) { + if (!check_expr(file, sc, *b.cond)) return false; + for (const Stmt& s : b.body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + } + for (const Stmt& s : st.else_body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + return true; + case StmtKind::While: + if (!check_expr(file, sc, *st.cond)) { + return false; + } + for (const Stmt& s : st.body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + return true; + } + return true; + } + + bool check_expr(const std::string& file, const LinkResult::PouScope& sc, + const Expr& e) { + switch (e.kind) { + case ExprKind::VarRef: + if (find_sym(sc, e.name) == nullptr && + out_->global_index.count(e.name) == 0) { + return fail(file, "undeclared identifier '" + e.name + "'", 0, 0); + } + return true; + case ExprKind::Field: { + const Symbol* inst = find_sym(sc, e.name); + if (inst == nullptr || inst->kind != SymbolKind::FbInstance) { + return fail(file, "undeclared FB instance '" + e.name + "'", 0, 0); + } + const auto it = sc.fb_instances.find(e.name); + bool found = false; + for (const FbField& f : it->second.fields) { + if (f.name == e.field) { + found = true; + break; + } + } + if (!found) { + return fail(file, "unknown field '" + e.field + "' for FB '" + + e.name + "'", + 0, 0); + } + return true; + } + case ExprKind::Call: { + PouKind k = PouKind::Program; + if (!find_function(e.name, &k)) { + return fail(file, "undeclared function '" + e.name + "'", 0, 0); + } + if (k != PouKind::Function) { + return fail(file, "'" + e.name + "' is not a function", 0, 0); + } + call_edges_[sc.name].insert(e.name); + for (const auto& a : e.args) { + if (!check_expr(file, sc, *a)) return false; + } + return true; + } + default: + break; + } + if (e.lhs && !check_expr(file, sc, *e.lhs)) return false; + if (e.rhs && !check_expr(file, sc, *e.rhs)) return false; + if (e.operand && !check_expr(file, sc, *e.operand)) return false; + return true; + } + + bool find_function(const std::string& name, PouKind* kind) const { + for (const LinkResult::PouScope& sc : out_->scopes) { + if (sc.name == name) { + *kind = sc.kind; + return true; + } + } + return false; + } + + // ---- 4. 循环调用检测(函数依赖图 DFS)---- + + bool check_cycles() { + std::set visiting; + std::set done; + for (const auto& [fn, _] : call_edges_) { + if (!dfs(fn, visiting, done)) { + return false; + } + } + return true; + } + + bool dfs(const std::string& fn, std::set& visiting, + std::set& done) { + if (done.count(fn)) { + return true; + } + if (visiting.count(fn)) { + return fail("", "recursive call involving '" + fn + "'", 0, 0); + } + visiting.insert(fn); + const auto it = call_edges_.find(fn); + if (it != call_edges_.end()) { + for (const std::string& callee : it->second) { + if (!dfs(callee, visiting, done)) { + return false; + } + } + } + visiting.erase(fn); + done.insert(fn); + return true; + } + + // ---- 5. io 校验 ---- + + bool check_io() { + for (const isa::IoBinding& b : proj_.io) { + std::string key = b.var; + for (char& ch : key) { + ch = static_cast(std::tolower(static_cast(ch))); + } + if (out_->global_index.count(key) == 0) { + return fail(proj_.name + "/project.toml", + "io.var '" + b.var + "' not in GVL", 0, 0); + } + } + return true; + } + + // ---- 6. 入口 ---- + + bool has_program_main() const { + for (const LinkResult::PouScope& sc : out_->scopes) { + if (sc.kind == PouKind::Program && sc.name == "main") { + return true; + } + } + return false; + } + + const Project& proj_; + const std::vector& units_; + LinkResult* out_; + std::string* err_; + std::filesystem::path gvl_path_; + std::map> call_edges_; + }; + +} // namespace + +bool link_project(const Project& proj, const std::vector& units, + LinkResult* out, std::string* err) { + out->globals.clear(); + out->global_index.clear(); + out->fb_types.clear(); + out->scopes.clear(); + out->fn_order.clear(); + Linker l(proj, units, out, err); + return l.run(); +} + +bool load_unit(const std::string& path, SourceUnit* out, std::string* err) { + out->path = path; + return parse_pous_file(path, &out->ast, err); +} + +std::string dump_symbols(const LinkResult& r) { + std::string out; + for (const Symbol& s : r.globals) { + out += s.name + " " + kind_name(s.kind) + " " + s.type_name + " " + + std::to_string(s.address) + " " + s.source_file + "\n"; + } + for (const LinkResult::PouScope& sc : r.scopes) { + out += "[" + sc.name + "]\n"; + for (const Symbol& s : sc.syms) { + out += s.name + " " + kind_name(s.kind) + " " + s.type_name + " " + + std::to_string(s.address) + " " + s.source_file + "\n"; + } + } + return out; +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea1428a..afd924d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,3 +45,14 @@ target_compile_definitions(parser_test PRIVATE add_test(NAME parser_syntax COMMAND parser_test) + +# 链接测试:用例 09/10/11/12/16/19 + line1 正例(REPO_ROOT 注入源目录绝对路径) +add_executable(linker_test + ./src/linker_test.cpp) + +target_link_libraries(linker_test PRIVATE compiler) +target_compile_definitions(linker_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME linker_links + COMMAND linker_test) diff --git a/tests/src/linker_test.cpp b/tests/src/linker_test.cpp new file mode 100644 index 0000000..b832389 --- /dev/null +++ b/tests/src/linker_test.cpp @@ -0,0 +1,229 @@ +/** + * @file linker_test.cpp + * @brief 链接测试:用例 09/10/11/12/16/19 + line1 正例 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Project.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// 从用例目录加载工程并链接 +static bool link_case(const char* dir, compiler::LinkResult* out, std::string* err) { + using namespace compiler; + const std::string toml = std::string(REPO_ROOT) + "/tests/cases/" + dir + "/project.toml"; + Project p; + if (!parse_project(toml, &p, err)) { + return false; + } + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + f, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + return link_project(p, units, out, err); +} + +static bool expect_link_err(const char* dir, const char* keyword) { + compiler::LinkResult r; + std::string err; + if (link_case(dir, &r, &err)) { + std::printf("FAIL %s: linked ok\n", dir); + return false; + } + if (err.find("link error") != 0) { + std::printf("FAIL %s: want 'link error', got '%s'\n", dir, err.c_str()); + return false; + } + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL %s: want '%s', got '%s'\n", dir, keyword, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +// ---- 1. 用例 09:GVL + VAR_EXTERNAL 同一槽 ---- + +static bool test_case09() { + using namespace compiler; + LinkResult r; + std::string err; + CHECK(link_case("09_gvl_external", &r, &err)); + CHECK(r.globals.size() == 1); + CHECK(r.globals[0].name == "g1"); + CHECK(r.globals[0].kind == SymbolKind::Global); + CHECK(r.globals[0].type_name == "int"); + CHECK(r.globals[0].address == 0); + + CHECK(r.scopes.size() == 1); + const LinkResult::PouScope& main = r.scopes[0]; + CHECK(main.name == "main"); + bool saw_external = false; + for (const Symbol& s : main.syms) { + if (s.name == "g1") { + saw_external = true; + CHECK(s.kind == SymbolKind::External); + CHECK(s.address == 0); // 同一槽 + CHECK(s.type_name == "int"); + } + } + // 符号表打印:name kind type address file + const std::string dump = dump_symbols(r); + CHECK(dump.find("g1 global int 0 ") != std::string::npos); + CHECK(dump.find("[main]") != std::string::npos); + CHECK(dump.find("g1 external int 0 ") != std::string::npos); + return true; +} + +// ---- 2. 用例 10 / 11 / 12 ---- + +static bool test_neg_cases() { + if (!expect_link_err("10_gvl_wrong_file", "VAR_GLOBAL only allowed in gvl file")) { + return false; + } + if (!expect_link_err("11_duplicate_global", "duplicate global")) { + return false; + } + if (!expect_link_err("12_io_unknown_var", "io.var 'Nope' not in GVL")) { + return false; + } + return true; +} + +// ---- 3. 用例 16:未声明实例 ---- + +static bool test_case16() { + if (!expect_link_err("16_fb_undeclared", "undeclared FB instance")) { + return false; + } + return true; +} + +// ---- 4. 用例 19:循环调用 ---- + +static bool test_case19() { + if (!expect_link_err("19_recursive_call", "recursive call")) { + return false; + } + return true; +} + +// ---- 5. line1 正例:全局槽序 / FB 布局 / 字段 / 实例 ---- + +static bool test_line1() { + using namespace compiler; + LinkResult r; + std::string err; + const std::string toml = std::string(REPO_ROOT) + "/examples/line1/project.toml"; + Project p; + CHECK(parse_project(toml, &p, &err)); + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + CHECK(load_unit(p.base_dir + "/" + f, &u, &err)); + units.push_back(std::move(u)); + } + CHECK(link_project(p, units, &r, &err)); + + // 全局槽序 = 声明顺序 + CHECK(r.globals.size() == 4); + CHECK(r.globals[0].name == "emergencystop"); + CHECK(r.globals[1].name == "i0_0"); + CHECK(r.globals[2].name == "i0_1"); + CHECK(r.globals[3].name == "q0_0"); + + // 用户 FB 布局:MotorStarter 字段 = input/output(external 不是字段) + const auto it = r.fb_types.find("motorstarter"); + CHECK(it != r.fb_types.end()); + CHECK(it->second.fields.size() == 3); + CHECK(it->second.fields[0].name == "start"); + CHECK(it->second.fields[1].name == "stop"); + CHECK(it->second.fields[2].name == "q"); + CHECK(it->second.fields[2].type == TypeKind::Bool); + + // 内建 TON 布局(用例 17 的 t) + const FbLayout* ton = nullptr; + for (const auto& [name, lay] : r.fb_types) { + (void)name; + (void)lay; + } + // 直接构造 TON 实例验证内建表(见 case17 用) + // main 作用域:starter 实例 + 字段引用 + CHECK(r.scopes.size() == 2); // motorstarter + main + const LinkResult::PouScope& main = r.scopes[1]; + bool saw_starter = false; + for (const Symbol& s : main.syms) { + if (s.name == "starter") { + saw_starter = true; + CHECK(s.kind == SymbolKind::FbInstance); + CHECK(s.type_name == "motorstarter"); + } + } + CHECK(saw_starter); + const auto sit = main.fb_instances.find("starter"); + CHECK(sit != main.fb_instances.end()); + CHECK(sit->second.fields.size() == 3); + return true; +} + +// ---- 6. 内建 FB 布局(TON)---- + +static bool test_builtin() { + using namespace compiler; + LinkResult r; + std::string err; + const std::string toml = std::string(REPO_ROOT) + "/tests/cases/17_ton/project.toml"; + Project p; + CHECK(parse_project(toml, &p, &err)); + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + CHECK(load_unit(p.base_dir + "/" + f, &u, &err)); + units.push_back(std::move(u)); + } + CHECK(link_project(p, units, &r, &err)); + CHECK(r.scopes.size() == 1); + const LinkResult::PouScope& main = r.scopes[0]; + const auto it = main.fb_instances.find("t"); + CHECK(it != main.fb_instances.end()); + CHECK(it->second.fields.size() == 4); + CHECK(it->second.fields[0].name == "in" && it->second.fields[0].type == TypeKind::Bool); + CHECK(it->second.fields[1].name == "pt" && it->second.fields[1].type == TypeKind::Time); + CHECK(it->second.fields[2].name == "q" && it->second.fields[2].type == TypeKind::Bool); + CHECK(it->second.fields[3].name == "et" && it->second.fields[3].type == TypeKind::Time); + return true; +} + +int main() { + if (!test_case09()) return 1; + if (!test_neg_cases()) return 1; + if (!test_case16()) return 1; + if (!test_case19()) return 1; + if (!test_line1()) return 1; + if (!test_builtin()) return 1; + std::printf("linker_test: %d checks passed\n", g_checks); + return 0; +} From 93a4662d6a71a114e1bbb763e35325a4fa173e1a Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:23:37 +0800 Subject: [PATCH 23/69] =?UTF-8?q?=E4=B8=89=E4=B8=AA=20compiler=20=E6=BA=90?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=A1=A5=20Doxygen=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=9A=E5=87=BD=E6=95=B0=E5=8A=9F=E8=83=BD=E4=B8=8E=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E8=AF=B4=E6=98=8E=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lexer.cpp:13 个函数 @brief/@param/@return(含 T# 字面量、行列语义) - Parser.cpp:24 个函数(含表达式优先级爬升、token 前瞻区分) - Linker.cpp:24 个函数(含六步流程、三色 DFS 环检测、io 校验) - 文件头统一:设计说明 + 函数清单;代码零改动,ctest 7/7 通过 --- compiler/src/Lexer.cpp | 165 +++++++++++++++++++--- compiler/src/Linker.cpp | 207 +++++++++++++++++++++++++++- compiler/src/Parser.cpp | 294 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 627 insertions(+), 39 deletions(-) diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index 4131e17..85d0d1f 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -3,6 +3,28 @@ * @brief ST 子集词法分析 * @author * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/compiler/词法.md): + * - 主循环:跳过空白/注释 → 文件末尾推 END → 否则 lex_one 识别一个 token + * - 大小写:关键字匹配与标识符存储统一折小写(与 IEC 一致) + * - 行列:line/col 从 1 起;token 记录【起始】位置(消费会推进行列, + * 故 lex_one 入口先捕获 tok_line_/tok_col_) + * - 错误:稳定前缀 "lex error" + 文件 + 行列,供测试按类别匹配 + * + * 函数清单: + * - time_unit_ms TIME 字面量单位 → 毫秒倍数(未知返回 -1) + * - Lexer::Lexer (构造)存源文本/文件名/输出指针,行列初始 (1,1) + * - Lexer::run 主循环:跳空白注释 → 文件尾推 END → lex_one + * - Lexer::fail 组装 "lex error: (file, line, col)" 返回 false + * - Lexer::cur 当前字符(不消费) + * - Lexer::at_end 是否已到源文末尾 + * - Lexer::advance 消费一个字符并推进行列(\n 行+1 列复位) + * - Lexer::skip_space_and_comments 跳过空白与 (* *) 注释(不嵌套,未闭合报错) + * - Lexer::push 用 token 起始位置产出 token 入流 + * - Lexer::lex_one 识别一个 token(T# → 标识符/关键字 → 整数 → 符号 → 报错) + * - Lexer::lex_time 解析 T#<单位>... 段式 TIME 字面量,累加毫秒 + * - lex 对外入口:清空输出 → 构造 Lexer → run + * - lex_file 读文件后调 lex;打不开算 lex error */ #include "compiler/Lexer.h" @@ -15,61 +37,91 @@ namespace compiler { namespace { - // 25 个关键字:小写键 → Tok(大小写不敏感,见 Doc/compiler/词法.md) + // 关键字表:小写键 → Tok。 + // 与 Doc/compiler/词法.md 的冻结表一致(12.5 修订补入 then/do); + // 数值即 token 类型,只追加不删改。 const struct { const char* key; Tok tok; } kKeywords[] = { + // POU 外壳 {"program", Tok::PROGRAM}, {"function", Tok::FUNCTION}, {"function_block", Tok::FUNCTION_BLOCK}, {"end_program", Tok::END_PROGRAM}, {"end_function", Tok::END_FUNCTION}, {"end_function_block", Tok::END_FUNCTION_BLOCK}, + // 变量段 {"var", Tok::VAR}, {"var_input", Tok::VAR_INPUT}, {"var_output", Tok::VAR_OUTPUT}, {"var_global", Tok::VAR_GLOBAL}, {"var_external", Tok::VAR_EXTERNAL}, {"end_var", Tok::END_VAR}, + // 类型 {"bool", Tok::BOOL}, {"int", Tok::INT}, {"time", Tok::TIME}, + // 控制 {"if", Tok::IF}, {"elsif", Tok::ELSIF}, {"else", Tok::ELSE}, {"end_if", Tok::END_IF}, {"while", Tok::WHILE}, {"end_while", Tok::END_WHILE}, - {"then", Tok::THEN}, - {"do", Tok::DO}, + {"then", Tok::THEN}, // 12.5 修订:IF…THEN 语法需要 + {"do", Tok::DO}, // 12.5 修订:WHILE…DO 语法需要 + // 逻辑 {"and", Tok::AND}, {"or", Tok::OR}, {"not", Tok::NOT}, + // 字面量 {"true", Tok::TRUE}, {"false", Tok::FALSE}, + // 内建 FB 类型名(v1 冻结三件套) {"ton", Tok::TON}, {"tof", Tok::TOF}, {"ctu", Tok::CTU}, }; - // TIME 字面量单位 → 毫秒倍数(不敏感) + /** + * @brief TIME 字面量单位换算为毫秒倍数 + * @param unit 单位字符串(小写):ms / s / m / h / d + * @return 对应毫秒倍数;未知单位返回 -1 + */ int64_t time_unit_ms(const std::string& unit) { if (unit == "ms") return 1; if (unit == "s") return 1000; if (unit == "m") return 60000; if (unit == "h") return 3600000; if (unit == "d") return 86400000; - return -1; + return -1; // 未知单位 } + /** + * @brief ST 单遍扫描器 + * + * @details pos_ 指向当前字符,line_/col_ 是它所在的 1 起行列。 + * 所有成员函数不抛异常,出错即返回 false 并写 err_。 + */ class Lexer { public: + /** + * @brief 构造扫描器 + * @param source_file 源文件名(报错与 token 的 source_file 用) + * @param content 源文本(引用外部存储,不拷贝) + * @param out token 流输出(run 前应为空) + * @param err 错误输出;可为 nullptr(静默) + */ Lexer(const std::string& source_file, const std::string& content, std::vector* out, std::string* err) : src_(content), sf_(source_file), out_(out), err_(err), pos_(0), line_(1), col_(1) {} + /** + * @brief 主循环:跳空白/注释 → 文件尾推 END → lex_one + * @return true 成功(out 以 END 收尾);false 词法错误(err 已写) + */ bool run() { while (true) { if (!skip_space_and_comments()) { @@ -86,6 +138,11 @@ namespace { } private: + /** + * @brief 组装 "lex error: (file, line, col)" 写入 err + * @param msg 错误描述(不含前缀与位置) + * @return 恒 false(便于 return fail(...) 一行退出) + */ bool fail(const std::string& msg) { if (err_) { char buf[128]; @@ -96,9 +153,24 @@ namespace { return false; } + /** + * @brief 当前字符(不消费) + * @return src_[pos_];调用前需保证 !at_end() + */ char cur() const { return src_[pos_]; } + + /** + * @brief 是否已到源文末尾 + * @return pos_ >= src_.size() + */ bool at_end() const { return pos_ >= src_.size(); } + /** + * @brief 消费一个字符并推进行列 + * + * @details '\n' → 行 +1、列复位 1;其他字符 → 列 +1。 + * 注意:token 的起始行列要在此前捕获(见 lex_one)。 + */ void advance() { if (cur() == '\n') { ++line_; @@ -109,7 +181,13 @@ namespace { ++pos_; } - // 跳过空白与 (* *) 注释(不嵌套);未闭合返回 false + /** + * @brief 跳过空白与 (* *) 注释 + * + * @details 第一版注释不嵌套:遇到第一个 "*) 即结束 + * (IEC 允许嵌套,留待以后)。未闭合 → lex error。 + * @return true 正常;false 未闭合注释(err 已写) + */ bool skip_space_and_comments() { while (!at_end()) { if (std::isspace(static_cast(cur()))) { @@ -139,6 +217,15 @@ namespace { return true; } + /** + * @brief 产出 token 入流 + * + * @details 位置用 tok_line_/tok_col_(本 token 起始位置, + * 而非 push 时刻的位置——此时消费已推进了行列)。 + * @param type token 类型 + * @param text 文本(标识符/关键字的小写形;字面量原文) + * @param value 字面量值(INT_LIT / TIME_LIT 毫秒;其他为 0) + */ void push(Tok type, const std::string& text, int64_t value) { Token t; t.type = type; @@ -150,6 +237,17 @@ namespace { out_->push_back(t); } + /** + * @brief 识别一个 token(当前字符开始) + * + * @details 分支顺序有讲究: + * 1. T#(TIME 字面量)必须优先于标识符——'T' 是字母,否则会被 + * 标识符分支吃掉;"timer" 这类普通词不受影响(只匹配 T#)。 + * 2. 标识符/关键字(含下划线,字母/数字/下划线组成)。 + * 3. 整数(纯数字;负数不是字面量,-5 是 MINUS + 5)。 + * 4. 符号::= <> <= >= 最长匹配优先。 + * @return true 成功(已消费并 push);false 非法字符(err 已写) + */ bool lex_one() { // 记录本 token 起始位置(push 时消费已经推进了行列) tok_line_ = line_; @@ -161,7 +259,8 @@ namespace { return lex_time(); } - // 标识符 / 关键字(统一折小写) + // 标识符 / 关键字:统一折小写存入 text(内部形), + // 关键字命中即分类,否则 IDENT(用户定义名 / 内建类型名都走这里)。 if (std::isalpha(static_cast(c)) || c == '_') { const uint32_t start = pos_; while (!at_end() && @@ -182,7 +281,8 @@ namespace { return true; } - // 整数 + // 整数:十进制 [0-9]+,int64_t 存储; + // 溢出检查在累加前做(v 上限 INT64_MAX)。 if (std::isdigit(static_cast(c))) { const uint32_t start = pos_; while (!at_end() && std::isdigit(static_cast(cur()))) { @@ -201,7 +301,8 @@ namespace { return true; } - // 符号(最长匹配优先) + // 符号:两字符符(:= <> <= >=)优先于单字符; + // 其余单字符各归一类,非法字符直接报错。 switch (c) { case ':': if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { @@ -282,7 +383,14 @@ namespace { } } - // T#... 段式解析,累加毫秒 + /** + * @brief 解析 T#<整数><单位> 段式 TIME 字面量 + * + * @details 段可多个,每段 = 数字 + 单位(ms/s/m/h/d,大小写不敏感): + * T#10ms → 10;T#1s250ms → 1250;T#2h → 7200000。 + * @return true 成功(push TIME_LIT,int_value 为毫秒); + * false 缺段 / 未知单位 / 数字溢出(err 已写) + */ bool lex_time() { const uint32_t start = pos_; advance(); // T @@ -319,19 +427,31 @@ namespace { return true; } - const std::string& src_; - const std::string& sf_; - std::vector* out_; - std::string* err_; - size_t pos_; - uint32_t line_; - uint32_t col_; - uint32_t tok_line_ = 1; - uint32_t tok_col_ = 1; + // ---- 状态 ---- + const std::string& src_; // 源文本(外部所有,不拷贝) + const std::string& sf_; // 源文件名(报错用) + std::vector* out_; // token 流输出 + std::string* err_; // 错误输出(可空) + size_t pos_; // 当前字符下标 + uint32_t line_; // 当前行(1 起) + uint32_t col_; // 当前列(1 起) + uint32_t tok_line_ = 1; // 本 token 起始行(push 时用) + uint32_t tok_col_ = 1; // 本 token 起始列(push 时用) }; } // namespace +/** + * @brief 对源文本做词法分析(对外入口) + * + * @details 成功时 out 以 END token 结尾;失败时 out 内容不保证 + * (调用方以返回值为准)。 + * @param source_file 源文件名(写入每个 token 的 source_file) + * @param content 源文本 + * @param out 输出 token 流(先清空) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "lex error") + */ bool lex(const std::string& source_file, const std::string& content, std::vector* out, std::string* err) { out->clear(); @@ -339,6 +459,13 @@ bool lex(const std::string& source_file, const std::string& content, return l.run(); } +/** + * @brief 读取文件后词法分析 + * @param path 文件路径 + * @param out 输出 token 流 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(文件打不开也算 lex error) + */ bool lex_file(const std::string& path, std::vector* out, std::string* err) { std::ifstream in(path, std::ios::binary); if (!in) { diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp index c3ebaa3..e4fce44 100644 --- a/compiler/src/Linker.cpp +++ b/compiler/src/Linker.cpp @@ -3,6 +3,38 @@ * @brief 符号表与链接 * @author * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/compiler/符号表与链接.md): + * - 流程:收集导出(gvl 校验/全局定槽/POU 登记)→ 用户 FB 类型布局 + * → 逐 POU 局部符号与引用解析 → 函数调用图环检测 → io 校验 → 入口校验 + * - 数据区定址:v1 单一数据区,全局(含 I/Q/M)按声明顺序编槽号 + * - 错误:稳定前缀 "link error" + 文件(+ 行列可选) + * + * 函数清单: + * - kind_name 符号种类 → 文本名(global/external/local/…) + * - type_str 类型引用 → 文本名(bool/int/time/fb 名) + * - is_scalar 是否为标量类型(BOOL/INT/TIME) + * - builtin_layout 内建 FB 名 → 冻结布局(ton/tof/ctu),未知返回 nullptr + * - Linker::Linker (构造)存工程/源文件/输出指针,规范化 gvl 路径 + * - Linker::run 链接主流程(6 步,任一步失败即停) + * - Linker::fail 组装 "link error: (file[, line, col])" 返回 false + * - Linker::is_gvl_file 路径是否工程指定的 gvl 文件(规范化比较) + * - collect_exports 收集全局(仅 gvl 顶层)与 POU 导出,重复名拒绝 + * - collect_fb_types 收集用户 FUNCTION_BLOCK 的字段布局 + * - resolve_scopes 逐 POU 建局部符号并解析语句引用 + * - scope_of 按 POU 名取作用域(找不到返回静态空作用域) + * - build_scope_syms 建一个 POU 的局部符号(含 EXTERNAL 接槽 / FB 实例布局) + * - find_sym 在 POU 局部符号里按名查找 + * - check_stmt 校验一条语句(赋值/FB 调用/IF/WHILE 递归) + * - check_expr 校验一个表达式(变量/字段/函数调用/递归子树) + * - find_function 按名查 POU 表并回传种类 + * - check_cycles 函数依赖图 DFS 环检测入口 + * - dfs 单个函数的环检测(visiting/done 三色标记) + * - check_io io 绑定必须已在 GVL(大小写折叠比较) + * - has_program_main 必须存在 PROGRAM main(入口) + * - link_project 对外入口:清空输出 → 构造 Linker → run + * - load_unit 读取并解析一个 .st 成 SourceUnit + * - dump_symbols 符号表文本:每行 "name kind type address file" */ #include "compiler/Linker.h" @@ -19,6 +51,12 @@ namespace compiler { namespace { + /** + * @brief 符号种类 → 文本名(dump_symbols 用) + * @param k 符号种类 + * @return "global" / "external" / "local" / "input" / "output" / + * "pou" / "fb_instance" / "const";未知返回 "?" + */ const char* kind_name(SymbolKind k) { switch (k) { case SymbolKind::Global: return "global"; @@ -33,6 +71,11 @@ namespace { return "?"; } + /** + * @brief 类型引用 → 文本名(符号表 / 类型比较用) + * @param t 类型引用 + * @return "bool" / "int" / "time" / FB 类型名(小写);未知返回 "?" + */ std::string type_str(const TypeRef& t) { switch (t.kind) { case TypeKind::Bool: return "bool"; @@ -44,6 +87,11 @@ namespace { return "?"; } + /** + * @brief 是否为标量类型(BOOL / INT / TIME) + * @param k 类型种类 + * @return true 标量(FB 字段只允许标量,见 collect_fb_types) + */ bool is_scalar(TypeKind k) { return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time; } @@ -57,6 +105,11 @@ namespace { {"pv", TypeKind::Int}, {"q", TypeKind::Bool}, {"cv", TypeKind::Int}}}; + /** + * @brief 内建 FB 名 → 冻结布局 + * @param name FB 类型名(小写) + * @return 布局指针(ton / tof / ctu);未知名字返回 nullptr + */ const FbLayout* builtin_layout(const std::string& name) { if (name == "ton") return &kTonLayout; if (name == "tof") return &kTofLayout; @@ -64,15 +117,34 @@ namespace { return nullptr; } + /** + * @brief 链接器 + * + * @details 六步流程见 run()。错误即停(首个错误优先,不做恢复)。 + */ class Linker { public: + /** + * @brief 构造链接器 + * @param proj 工程定义(toml 解析结果;io 绑定 / gvl 文件 / base_dir) + * @param units 全部源文件的 AST(compile_files 集合逐一 load_unit) + * @param out 链接结果(调用方先构造空 LinkResult) + * @param err 错误输出;可为 nullptr(静默) + */ Linker(const Project& proj, const std::vector& units, LinkResult* out, std::string* err) : proj_(proj), units_(units), out_(out), err_(err) { + // gvl 路径规范化一次,供 is_gvl_file 比较(容忍相对/绝对写法差异) gvl_path_ = std::filesystem::weakly_canonical( std::filesystem::path(proj.base_dir) / proj.gvl_file); } + /** + * @brief 链接主流程(6 步) + * @details 1. 收集导出 → 2. 用户 FB 类型布局 → 3. 逐 POU 符号与引用 + * → 4. 循环调用检测 → 5. io 校验 → 6. 入口校验 + * @return true 链接成功;false 任一步失败(err 已写) + */ bool run() { if (!collect_exports()) return false; if (!collect_fb_types()) return false; @@ -86,6 +158,14 @@ namespace { } private: + /** + * @brief 组装 "link error: (file[, line, col])" 写入 err + * @param file 出错文件(可为空串,用于无位置错误) + * @param msg 错误描述(不含前缀与位置) + * @param line 行列(1 起);0 表示不带行列只带文件 + * @param col 列号 + * @return 恒 false(便于 return fail(...) 一行退出) + */ bool fail(const std::string& file, const std::string& msg, uint32_t line, uint32_t col) { if (err_) { @@ -101,6 +181,12 @@ namespace { return false; } + /** + * @brief 路径是否工程指定的 gvl 文件 + * @param path 待比较路径 + * @return true 是 gvl 文件(规范化后与 gvl_path_ 相同); + * 工程未配置 gvl 时恒 false + */ bool is_gvl_file(const std::string& path) const { if (proj_.gvl_file.empty()) { return false; @@ -110,6 +196,11 @@ namespace { // ---- 1. 收集导出 ---- + /** + * @brief 收集导出:顶层全局(仅 gvl 文件)与 POU 登记 + * @details 全局按声明顺序编槽号(下标即槽号);同名全局 / 同名 POU 拒绝 + * @return true 成功;false(err 已写) + */ bool collect_exports() { for (const SourceUnit& u : units_) { const bool is_gvl = is_gvl_file(u.path); @@ -157,6 +248,12 @@ namespace { // ---- 2. 用户 FB 类型布局 ---- + /** + * @brief 收集用户 FUNCTION_BLOCK 的字段布局 + * @details 字段 = input / output / 内部 var(段序); + * external 与 global 不是字段;字段类型仅允许标量 + * @return true 成功;false(FB 字段类型非法,err 已写) + */ bool collect_fb_types() { for (const SourceUnit& u : units_) { for (const POU& p : u.ast.pous) { @@ -192,6 +289,10 @@ namespace { // ---- 3. 逐 POU 建局部符号并解析引用 ---- + /** + * @brief 逐 POU 建局部符号并解析语句引用 + * @return true 成功;false(err 已写) + */ bool resolve_scopes() { for (SourceUnit& u : const_cast&>(units_)) { for (const POU& p : u.ast.pous) { @@ -209,6 +310,11 @@ namespace { return true; } + /** + * @brief 按 POU 名取作用域 + * @param name POU 名(小写) + * @return 对应 PouScope 引用;找不到返回静态空作用域(不应发生) + */ LinkResult::PouScope& scope_of(const std::string& name) { for (LinkResult::PouScope& sc : out_->scopes) { if (sc.name == name) { @@ -219,6 +325,16 @@ namespace { return kEmpty; } + /** + * @brief 建一个 POU 的局部符号 + * @details VAR/INPUT/OUTPUT → 对应种类;VAR_EXTERNAL → 查全局表接同一槽 + * (类型必须一致);FB 类型 → FbInstance + 布局(内建取冻结表, + * 用户类型查 fb_types);POU 内 VAR_GLOBAL → 拒绝 + * @param file 出错文件 + * @param p POU AST + * @param sc 输出作用域(syms / fb_instances 被填充) + * @return true 成功;false(err 已写) + */ bool build_scope_syms(const std::string& file, const POU& p, LinkResult::PouScope& sc) { for (const VarBlock& b : p.blocks) { @@ -294,6 +410,12 @@ namespace { // ---- 引用解析 ---- + /** + * @brief 在 POU 局部符号里按名查找 + * @param sc 作用域 + * @param name 符号名(小写) + * @return 符号指针;未找到返回 nullptr + */ const Symbol* find_sym(const LinkResult::PouScope& sc, const std::string& name) const { for (const Symbol& s : sc.syms) { @@ -304,6 +426,17 @@ namespace { return nullptr; } + /** + * @brief 校验一条语句 + * @details Assign:左值必须已声明(FUNCTION 内函数名赋值除外); + * FbCall:实例必须存在且是 FB,命名实参必须是该实例的字段; + * If / While:递归校验条件与各分支体 + * @param file 出错文件 + * @param sc 当前 POU 作用域 + * @param pou 当前 POU(函数名赋值判定用) + * @param st 语句 AST + * @return true 合法;false(err 已写) + */ bool check_stmt(const std::string& file, const LinkResult::PouScope& sc, const POU& pou, const Stmt& st) { switch (st.kind) { @@ -379,6 +512,16 @@ namespace { return true; } + /** + * @brief 校验一个表达式 + * @details VarRef:局部或全局表查得到;Field:对象必须是 FB 实例 + * 且字段存在;Call:目标必须存在且是 FUNCTION(记调用边); + * 其他节点递归校验子树 + * @param file 出错文件 + * @param sc 当前 POU 作用域 + * @param e 表达式 AST + * @return true 合法;false(err 已写) + */ bool check_expr(const std::string& file, const LinkResult::PouScope& sc, const Expr& e) { switch (e.kind) { @@ -431,6 +574,12 @@ namespace { return true; } + /** + * @brief 按名查 POU 表并回传种类 + * @param name POU 名(小写) + * @param kind 输出该 POU 的种类(PROGRAM / FUNCTION / FUNCTION_BLOCK) + * @return true 找到;false 未找到 + */ bool find_function(const std::string& name, PouKind* kind) const { for (const LinkResult::PouScope& sc : out_->scopes) { if (sc.name == name) { @@ -443,6 +592,11 @@ namespace { // ---- 4. 循环调用检测(函数依赖图 DFS)---- + /** + * @brief 函数依赖图环检测入口 + * @details 对每个有出边的函数做 DFS;任一条路径回到栈上节点即环 + * @return true 无环;false 发现环(err 已写) + */ bool check_cycles() { std::set visiting; std::set done; @@ -454,6 +608,13 @@ namespace { return true; } + /** + * @brief 单个函数的环检测(三色标记 DFS) + * @param fn 当前函数名 + * @param visiting 栈上节点(灰色,正在展开) + * @param done 已确认无环的节点(黑色) + * @return true 无环;false 命中灰色节点 = 环(err 已写) + */ bool dfs(const std::string& fn, std::set& visiting, std::set& done) { if (done.count(fn)) { @@ -478,6 +639,12 @@ namespace { // ---- 5. io 校验 ---- + /** + * @brief io 绑定必须已在 GVL 声明 + * @details 绑定名来自 toml(原始大小写),与全局表比较前折小写; + * io 不创造变量,只做 channel/bit → 槽号映射 + * @return true 全部绑定合法;false(err 已写) + */ bool check_io() { for (const isa::IoBinding& b : proj_.io) { std::string key = b.var; @@ -494,6 +661,10 @@ namespace { // ---- 6. 入口 ---- + /** + * @brief 必须存在 PROGRAM main(与 toml entry = "program MAIN" 呼应) + * @return true 存在 + */ bool has_program_main() const { for (const LinkResult::PouScope& sc : out_->scopes) { if (sc.kind == PouKind::Program && sc.name == "main") { @@ -503,16 +674,27 @@ namespace { return false; } - const Project& proj_; - const std::vector& units_; - LinkResult* out_; - std::string* err_; - std::filesystem::path gvl_path_; - std::map> call_edges_; + // ---- 成员 ---- + const Project& proj_; // 工程定义(toml) + const std::vector& units_; // 全部源文件 AST + LinkResult* out_; // 链接结果 + std::string* err_; // 错误输出(可空) + std::filesystem::path gvl_path_; // 规范化后的 gvl 路径 + std::map> call_edges_; // 调用者 → 被调函数集 }; } // namespace +/** + * @brief 链接一个工程(对外入口) + * @details 校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、 + * 函数循环调用检测、io 校验、入口校验 + * @param proj 工程定义(toml 解析结果) + * @param units 全部源文件的 AST(compile_files 集合逐一 load_unit 得到) + * @param out 链接结果(各字段先清空后填充) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "link error") + */ bool link_project(const Project& proj, const std::vector& units, LinkResult* out, std::string* err) { out->globals.clear(); @@ -524,11 +706,24 @@ bool link_project(const Project& proj, const std::vector& units, return l.run(); } +/** + * @brief 读取并解析一个 .st 文件成 SourceUnit + * @param path 文件路径 + * @param out 输出 SourceUnit(path + ast) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(文件打不开 / 语法错误,透传 lex/syntax error) + */ bool load_unit(const std::string& path, SourceUnit* out, std::string* err) { out->path = path; return parse_pous_file(path, &out->ast, err); } +/** + * @brief 符号表文本(完成标准:能打印 name kind type address file) + * @param r 链接结果 + * @return 多行文本:全局表逐行 "name kind type address file", + * 每个 POU 以 "[名字]" 开头后接其局部符号行 + */ std::string dump_symbols(const LinkResult& r) { std::string out; for (const Symbol& s : r.globals) { diff --git a/compiler/src/Parser.cpp b/compiler/src/Parser.cpp index 6778934..38a570d 100644 --- a/compiler/src/Parser.cpp +++ b/compiler/src/Parser.cpp @@ -3,6 +3,47 @@ * @brief ST 子集递归下降语法分析 * @author * @date 2026-08-21 + * + * @details 设计说明(文法详见 Doc/compiler/语法.md): + * - 输入:lexer 产出的 token 流(含末尾 END);输出:Unit(顶层全局段 + POU 列表) + * - 递归下降 + 每层一个函数,token 流只前移不回溯(FB 调用/赋值用一 token 前瞻区分) + * - 表达式按优先级爬升:or → and → not → cmp → add → mul → unary → primary, + * AND/OR 的短路语义保留在 AST(12.8 编成跳转) + * - 错误:稳定前缀 "syntax error" + 当前 token 的文件与行列 + * + * 函数清单: + * - is_forbidden 命中保留名(var_in_out/ref/class/any…)→ true + * - Parser::Parser (构造)先整体 lex 成 token 流;词法失败透传 lex error + * - Parser::run 顶层循环:顶层 VAR_GLOBAL 段(GVL 形态)或 POU 直到 END + * - Parser::fail 组装 "syntax error: (file, line, col)" 返回 false + * - Parser::cur/type/check token 视图:当前 token / 类型 / 是否指定类型 + * - Parser::advance 前进一个 token(END 处不动) + * - Parser::expect 断言当前是 t,否则报 "expected 'what'" + * - parse_pou POU 外壳:PROGRAM/FUNCTION[:类型]/FUNCTION_BLOCK + 段 + 体 + END_* + * - is_var_section 当前 token 是否五种变量段关键字之一 + * - end_tok_for POU 种类 → 配对结束关键字 + * - check_end_for 是否已到本 POU 的配对结束关键字(语句体循环出口) + * - parse_var_block VAR_* … END_VAR:逐行收集声明,定段种类 + * - parse_vars_line 一行声明展开成多个 VarDecl(共享类型与初值) + * - parse_literal_expr 初值:只吃字面量(TRUE/FALSE/INT/TIME) + * - parse_type 类型名:BOOL/INT/TIME/TON/TOF/CTU/标识符 + * - parse_stmt 语句分发:IF/WHILE 直入;IDENT 前瞻区分 FB 调用 vs 赋值 + * - parse_assign ident := expr ;(fb.field 左值拒绝) + * - parse_fb_call ident(名 := expr, …); 命名实参 FB 调用 + * - parse_if IF … THEN … (ELSIF …)* (ELSE …)? END_IF + * - parse_while WHILE … DO … END_WHILE + * - parse_body 连续语句,遇 ELSIF/ELSE/END_IF/END_WHILE/END 停 + * - parse_expr 表达式入口 → parse_or + * - parse_or/and OR/AND 层(短路保留在 AST,左结合) + * - parse_not NOT 前缀一元(可连写) + * - parse_cmp 比较层(a (file, line, col)" 写入 err + * @details 位置取当前 token(指向出错处,不回溯到行首) + * @param msg 错误描述(不含前缀与位置) + * @return 恒 false(便于 return fail(...) 一行退出) + */ bool fail(const std::string& msg) { if (err_) { const Token& t = cur(); @@ -87,10 +161,30 @@ namespace { // ---- token 视图 ---- + /** + * @brief 当前 token(不消费) + * @return tokens_[pos_] 的引用 + */ const Token& cur() const { return tokens_[pos_]; } + + /** + * @brief 当前 token 的类型 + * @return cur().type + */ Tok type() const { return cur().type; } + + /** + * @brief 当前 token 是否为指定类型 + * @param t 期望的 token 类型 + * @return true 匹配 + */ bool check(Tok t) const { return type() == t; } + /** + * @brief 前进一个 token + * @details END 处原地不动(pos_ 永不越界) + * @return 前进前的 token 引用 + */ const Token& advance() { const Token& t = tokens_[pos_]; if (pos_ + 1 < tokens_.size()) { @@ -99,6 +193,12 @@ namespace { return t; } + /** + * @brief 断言当前 token 是 t,否则报错 + * @param t 期望的 token 类型 + * @param what 错误信息中的名字(如 "END_IF") + * @return true 匹配并前进;false 不匹配(err 已写) + */ bool expect(Tok t, const char* what) { if (!check(t)) { return fail(std::string("expected '") + what + "'"); @@ -109,6 +209,14 @@ namespace { // ---- POU ---- + /** + * @brief 解析 POU 外壳 + * + * @details PROGRAM / FUNCTION [: 返回类型] / FUNCTION_BLOCK, + * 后接 0+ 变量段、0+ 语句,最后必须是配对 END_*。 + * @param p 输出 POU + * @return true 成功;false 语法错误(err 已写) + */ bool parse_pou(POU* p) { if (check(Tok::PROGRAM)) { advance(); @@ -127,6 +235,7 @@ namespace { return false; } + // 仅 FUNCTION 有返回类型:FUNCTION Add : INT if (p->kind == PouKind::Function) { if (!expect(Tok::COLON, ":")) { return false; @@ -136,6 +245,7 @@ namespace { } } + // 变量段可多个、可缺省,按声明顺序保留 while (is_var_section()) { VarBlock b; if (!parse_var_block(&b)) { @@ -144,6 +254,7 @@ namespace { p->blocks.push_back(std::move(b)); } + // 语句体直到配对 END_* while (!check_end_for(p->kind)) { Stmt s; if (!parse_stmt(&s)) { @@ -161,6 +272,10 @@ namespace { return expect(end_tok_for(p->kind), end_kw); } + /** + * @brief 当前 token 是否为五种变量段关键字之一 + * @return true 为 VAR / VAR_INPUT / VAR_OUTPUT / VAR_GLOBAL / VAR_EXTERNAL + */ bool is_var_section() const { switch (type()) { case Tok::VAR: @@ -174,6 +289,12 @@ namespace { } } + /** + * @brief POU 种类 → 配对结束关键字 + * @param k POU 种类 + * @return PROGRAM → END_PROGRAM、FUNCTION → END_FUNCTION、 + * FUNCTION_BLOCK → END_FUNCTION_BLOCK + */ Tok end_tok_for(PouKind k) const { switch (k) { case PouKind::Program: return Tok::END_PROGRAM; @@ -183,10 +304,20 @@ namespace { return Tok::END; } + /** + * @brief 当前 token 是否已到本 POU 的配对结束关键字 + * @param k POU 种类 + * @return true 语句体循环应停止 + */ bool check_end_for(PouKind k) const { return check(end_tok_for(k)); } // ---- 变量段 ---- + /** + * @brief 解析 VAR_* … END_VAR 变量段 + * @param b 输出 VarBlock(section + vars) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_var_block(VarBlock* b) { switch (type()) { case Tok::VAR: b->section = VarSection::Local; break; @@ -212,7 +343,12 @@ namespace { return true; } - // ident (',' ident)* ':' type (':=' literal)? ';' —— 展开成多个 VarDecl + /** + * @brief 解析一行声明:ident (',' ident)* ':' type (':=' literal)? ';' + * @details 同一行多个名字共享类型与初值,展开成多个 VarDecl + * @param out 输出 VarDecl 列表 + * @return true 成功;false 语法错误(err 已写) + */ bool parse_vars_line(std::vector* out) { std::vector names; while (true) { @@ -258,7 +394,12 @@ namespace { return true; } - // 初值只允许字面量(BOOL / INT / TIME / TRUE / FALSE) + /** + * @brief 解析初值字面量 + * @details 只允许字面量(TRUE/FALSE/INT/TIME);表达式初值 v1 不做 + * @param e 输出 Expr(LitBool / LitInt / LitTime) + * @return true 成功;false 非字面量(err 已写) + */ bool parse_literal_expr(Expr* e) { switch (type()) { case Tok::TRUE: @@ -286,7 +427,13 @@ namespace { } } - // 类型:BOOL | INT | TIME | TON | TOF | CTU | ident(用户 FB 类型) + /** + * @brief 解析类型名 + * @details 内建标量 BOOL/INT/TIME、内建 FB TON/TOF/CTU、 + * 或标识符(用户 FB 类型,存在性由 12.6 链接层校验) + * @param t 输出 TypeRef + * @return true 成功;false 语法错误(err 已写) + */ bool parse_type(TypeRef* t) { switch (type()) { case Tok::BOOL: @@ -333,6 +480,14 @@ namespace { // ---- 语句 ---- + /** + * @brief 语句分发 + * @details IF / WHILE 关键字直入;IDENT 用一 token 前瞻区分: + * ident '(' → FB 调用(无返回值,语句形态); + * ident ('.' ident)? ':=' → 赋值 + * @param s 输出 Stmt + * @return true 成功;false 语法错误(err 已写) + */ bool parse_stmt(Stmt* s) { if (check(Tok::IF)) { return parse_if(s); @@ -352,6 +507,12 @@ namespace { return is_call ? parse_fb_call(s) : parse_assign(s); } + /** + * @brief 解析赋值语句:ident ':=' expr ';' + * @details 左值仅标识符;ident.ident 左值(fb.field 赋值)v1 明确拒绝 + * @param s 输出 Stmt(kind=Assign) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_assign(Stmt* s) { s->kind = StmtKind::Assign; if (!parse_ident(&s->target)) { @@ -370,6 +531,13 @@ namespace { return expect(Tok::SEMI, ";"); } + /** + * @brief 解析 FB 调用语句:ident '(' arg (',' arg)* ')' ';' + * @details 全部命名实参(位置实参 v1 不做); + * 字段名与类型由 12.6 / 12.7 校验 + * @param s 输出 Stmt(kind=FbCall) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_fb_call(Stmt* s) { s->kind = StmtKind::FbCall; if (!parse_ident(&s->instance)) { @@ -404,6 +572,11 @@ namespace { return expect(Tok::SEMI, ";"); } + /** + * @brief 解析 IF 语句:IF cond THEN body (ELSIF cond THEN body)* (ELSE body)? END_IF + * @param s 输出 Stmt(kind=If) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_if(Stmt* s) { s->kind = StmtKind::If; advance(); // IF @@ -441,6 +614,12 @@ namespace { return expect(Tok::END_IF, "END_IF"); } + /** + * @brief 解析 WHILE 语句:WHILE cond DO body END_WHILE + * @details 终止性由 cycle_limit 保证,不做证明 + * @param s 输出 Stmt(kind=While) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_while(Stmt* s) { s->kind = StmtKind::While; advance(); // WHILE @@ -457,6 +636,13 @@ namespace { return expect(Tok::END_WHILE, "END_WHILE"); } + /** + * @brief 解析语句体(连续语句) + * @details 遇 ELSIF / ELSE / END_IF / END_WHILE / END 停止; + * 停止 token 由调用方消费,这里不越界 + * @param out 输出 Stmt 列表 + * @return true 成功;false 语法错误(err 已写) + */ bool parse_body(std::vector* out) { while (!check(Tok::ELSIF) && !check(Tok::ELSE) && !check(Tok::END_IF) && !check(Tok::END_WHILE) && !check(Tok::END)) { @@ -471,8 +657,20 @@ namespace { // ---- 表达式(优先级爬升)---- + /** + * @brief 表达式入口 + * @details 优先级从低到高:or < and < not < cmp < add < mul < unary < primary。 + * 每个二目层用「局部链 + 末尾 move」累积左结合树(避免自引用指针)。 + * @param e 输出 Expr 根 + * @return true 成功;false 语法错误(err 已写) + */ bool parse_expr(Expr* e) { return parse_or(e); } + /** + * @brief OR 层:and_expr (OR and_expr)*(左结合) + * @param e 输出 Expr 根(kind=Or 链或透传下层) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_or(Expr* e) { std::unique_ptr lhs = std::make_unique(); if (!parse_and(lhs.get())) { @@ -481,7 +679,7 @@ namespace { while (check(Tok::OR)) { advance(); auto node = std::make_unique(); - node->kind = ExprKind::Or; + node->kind = ExprKind::Or; // 短路语义保留到 12.8 编跳转 node->lhs = std::move(lhs); node->rhs = std::make_unique(); if (!parse_and(node->rhs.get())) { @@ -493,6 +691,11 @@ namespace { return true; } + /** + * @brief AND 层:not_expr (AND not_expr)*(左结合) + * @param e 输出 Expr 根(kind=And 链或透传下层) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_and(Expr* e) { std::unique_ptr lhs = std::make_unique(); if (!parse_not(lhs.get())) { @@ -501,7 +704,7 @@ namespace { while (check(Tok::AND)) { advance(); auto node = std::make_unique(); - node->kind = ExprKind::And; + node->kind = ExprKind::And; // 短路语义保留到 12.8 编跳转 node->lhs = std::move(lhs); node->rhs = std::make_unique(); if (!parse_not(node->rhs.get())) { @@ -513,6 +716,12 @@ namespace { return true; } + /** + * @brief NOT 层:NOT not_expr | cmp_expr + * @details NOT 是前缀一元,可连写(NOT NOT x);比 AND/OR 紧 + * @param e 输出 Expr 根(kind=Not 或透传 cmp) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_not(Expr* e) { if (check(Tok::NOT)) { advance(); @@ -523,6 +732,12 @@ namespace { return parse_cmp(e); } + /** + * @brief 比较层:add_expr (比较符 add_expr)? + * @details 比较不连锁(a < b < c 拒绝);结果类型 BOOL 由 12.7 校验 + * @param e 输出 Expr 根(kind=Cmp 或透传 add) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_cmp(Expr* e) { std::unique_ptr lhs = std::make_unique(); if (!parse_add(lhs.get())) { @@ -537,7 +752,7 @@ namespace { case Tok::GT: op = BinOp::Gt; break; case Tok::GE: op = BinOp::Ge; break; default: - *e = std::move(*lhs); + *e = std::move(*lhs); // 无比较符 → 原样上抛 return true; } advance(); @@ -545,6 +760,7 @@ namespace { if (!parse_add(rhs.get())) { return false; } + // 右侧之后再出现比较符 → 链式,v1 拒绝 if (is_cmp_op(type())) { return fail("chained comparison not supported"); } @@ -555,11 +771,21 @@ namespace { return true; } + /** + * @brief 是否比较运算符 token(= <> < <= > >=) + * @param t token 类型 + * @return true 是比较运算符 + */ bool is_cmp_op(Tok t) const { return t == Tok::EQ || t == Tok::NE || t == Tok::LT || t == Tok::LE || t == Tok::GT || t == Tok::GE; } + /** + * @brief 加减层:mul_expr (('+'|'-') mul_expr)*(左结合) + * @param e 输出 Expr 根(kind=Add/Sub 链或透传下层) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_add(Expr* e) { std::unique_ptr lhs = std::make_unique(); if (!parse_mul(lhs.get())) { @@ -588,6 +814,11 @@ namespace { return true; } + /** + * @brief 乘除层:unary (('*'|'/') unary)*(左结合) + * @param e 输出 Expr 根(kind=Mul/Div 链或透传下层) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_mul(Expr* e) { std::unique_ptr lhs = std::make_unique(); if (!parse_unary(lhs.get())) { @@ -616,6 +847,12 @@ namespace { return true; } + /** + * @brief 一元负号层:'-' unary | primary + * @details 可连写(- - x);注意 -5 不是字面量,而是 Neg(LitInt 5) + * @param e 输出 Expr 根(kind=Neg 或透传 primary) + * @return true 成功;false 语法错误(err 已写) + */ bool parse_unary(Expr* e) { if (check(Tok::MINUS)) { advance(); @@ -626,6 +863,13 @@ namespace { return parse_primary(e); } + /** + * @brief 原子:字面量 / 括号表达式 / ident + * @details ident 后的 '.' 或 '(' 决定形态(变量 | fb.field | 函数调用), + * 与语句层的区分逻辑一致 + * @param e 输出 Expr + * @return true 成功;false 语法错误(err 已写) + */ bool parse_primary(Expr* e) { switch (type()) { case Tok::INT_LIT: @@ -662,7 +906,7 @@ namespace { } advance(); if (check(Tok::DOT)) { - // fb.field + // fb.field(字段存在性由 12.6 校验) advance(); e->kind = ExprKind::Field; e->name = word; @@ -672,7 +916,7 @@ namespace { return true; } if (check(Tok::LPAREN)) { - // 函数调用(FUNCTION,有返回值) + // 函数调用(FUNCTION,有返回值;与语句层 FB 调用区分) e->kind = ExprKind::Call; e->name = word; advance(); @@ -700,6 +944,12 @@ namespace { } } + /** + * @brief 解析标识符 + * @details 必须是 IDENT 且非保留名 + * @param out 输出小写标识符文本 + * @return true 成功;false 语法错误(err 已写) + */ bool parse_ident(std::string* out) { if (!check(Tok::IDENT)) { return fail("expected identifier"); @@ -714,15 +964,24 @@ namespace { // ---- 成员 ---- - std::string* err_; - std::vector tokens_; - Unit* out_; - size_t pos_ = 0; - bool ok_ = true; + std::string* err_; // 错误输出(可空) + std::vector tokens_; // 整段 token 流(构造时一次性 lex 完成) + Unit* out_; // 解析结果 + size_t pos_ = 0; // 当前 token 下标 + bool ok_ = true; // 词法阶段是否成功 }; } // namespace +/** + * @brief 对源文本做语法分析(对外入口) + * @details 成功时 out 填满 globals/pous;失败时 out 内容不保证(以返回值为准) + * @param source_file 源文件名(报错用) + * @param content 源文本 + * @param out 输出 Unit(globals / pous,先清空) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "syntax error") + */ bool parse_pous(const std::string& source_file, const std::string& content, Unit* out, std::string* err) { out->globals.clear(); @@ -731,6 +990,13 @@ bool parse_pous(const std::string& source_file, const std::string& content, return p.run(); } +/** + * @brief 读取文件后语法分析 + * @param path 文件路径 + * @param out 输出 Unit(globals / pous) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(文件打不开也算 syntax error) + */ bool parse_pous_file(const std::string& path, Unit* out, std::string* err) { std::ifstream in(path, std::ios::binary); if (!in) { From 9cd4997a3a7cb886cf63127db1cc50894b8c2895 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:26:55 +0800 Subject: [PATCH 24/69] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.7=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=A3=80=E6=9F=A5=EF=BC=9A=E8=A1=A8=E8=BE=BE=E5=BC=8F?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E3=80=81=E8=AF=AD=E5=8F=A5=E8=A7=84=E5=88=99?= =?UTF-8?q?=E3=80=81FUNCTION=20=E7=A6=81=E5=86=99=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Typecheck.h/cpp:三型(BOOL/INT/TIME)求值,AND/OR/NOT 只吃 BOOL、算术只吃 INT(TIME 无算术)、比较同型、IF/WHILE 条件必须 BOOL - FUNCTION 禁写全局(含经 VAR_EXTERNAL)——用例 14 点亮;函数名赋值=返回类型 - FB 命名实参匹配字段类型;FB 实例不能当值 - typecheck_test:23 断言(用例 14 + 8 类负例 + 13 个正例),ctest 8/8 --- Doc/compiler/类型检查.md | 78 ++++ Doc/索引.md | 2 + compiler/CMakeLists.txt | 3 +- compiler/include/compiler/Typecheck.h | 26 ++ compiler/src/Typecheck.cpp | 494 ++++++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/typecheck_test.cpp | 213 +++++++++++ 7 files changed, 826 insertions(+), 1 deletion(-) create mode 100644 Doc/compiler/类型检查.md create mode 100644 compiler/include/compiler/Typecheck.h create mode 100644 compiler/src/Typecheck.cpp create mode 100644 tests/src/typecheck_test.cpp diff --git a/Doc/compiler/类型检查.md b/Doc/compiler/类型检查.md new file mode 100644 index 0000000..ed91df7 --- /dev/null +++ b/Doc/compiler/类型检查.md @@ -0,0 +1,78 @@ +# 类型检查(12.7) + +compiler 模块的类型规则检查。输入:`Project` + 各 `.st` 的 `Unit`(AST)+ `LinkResult`(符号已解析);输出:通过或 `type error`。全工程定案见 [`初步计划.md`](../初步计划.md) 12.7。 + +## 做 / 不做 + +**做** + +- `:=`、比较、算术的类型规则;`AND` / `OR` / `NOT` 只吃 `BOOL` +- `FUNCTION` 禁止写全局(含经 `VAR_EXTERNAL`)——用例 14 +- FB 调用:命名输入类型匹配;`fb.Q` 字段存在且类型对 +- 表达式求值类型:`BOOL` / `INT` / `TIME` 三型 +- 无隐式宽化;`INT` 与 `TIME` 不混用 + +**不做(第一版)** + +- 常量折叠、溢出区间检查(INT 字面量范围,饱和在 VM 层) +- `FUNCTION` 实参与形参的个数/类型核对(12.7 未要求,只核对 FB 命名输入) +- 变量初始化表达式的类型核对(语法层已限字面量) + +## 表达式求值类型 + +```cpp +enum class TType { Bool, Int, Time }; +``` + +| 节点 | 规则 | 结果 | +|---|---|---| +| `LitBool` / `LitInt` / `LitTime` | — | Bool / Int / Time | +| `VarRef` | 查符号(局部/输入/输出/外部/全局)类型 | 符号类型 | +| `Field` | 实例字段类型(布局已由 12.6 提供) | 字段类型 | +| `Not` | 操作数必须 Bool | Bool | +| `And` / `Or` | 两侧必须 Bool(短路已由语法层保留) | Bool | +| `Cmp` | 两侧同型(Bool/Int/Time 均可比较) | Bool | +| `Add/Sub/Mul/Div` | 两侧必须 Int(**TIME 无算术**,INT 与 TIME 不混用) | Int | +| `Neg` | 操作数必须 Int | Int | +| `Call` | 函数存在(12.6 已验);结果 = 函数返回类型 | 返回类型 | +| `VarRef` 指向 FB 实例 | 实例不能当值 | type error | + +## 语句规则 + +| 语句 | 规则 | +|---|---| +| `Assign`(普通) | 左值类型 == 表达式类型;左值可为局部/输入/输出/外部/全局 | +| `Assign`(函数名,FUNCTION 内) | 表达式类型 == 函数返回类型(用例 13 约定) | +| **`FUNCTION` 内左值是全局/外部** | **拒绝:`function cannot write global`(用例 14,含经 VAR_EXTERNAL)** | +| `FbCall` | 每个命名实参表达式类型 == 实例对应字段类型(布局 12.6 已校验存在性) | +| `If` / `While` | 条件表达式必须 Bool | + +## 错误 + +稳定前缀 `type error`,带文件与行列(尽量带): + +```text +type error: AND operands must be BOOL (main.st, line 5, col 9) +type error: type mismatch in assignment to 'q0_0' (INT vs BOOL) (main.st, line 6, col 13) +type error: function cannot write global 'g1' (main.st, line 3, col 5) +type error: TIME has no arithmetic (main.st, line 4, col 12) +type error: FB input 'pt' expects TIME (main.st, line 5, col 16) +``` + +## 完成标准 + +1. 用例 14(FUNCTION 写全局)→ `type error`(12.6 已放行,本阶段点亮) +2. line1 / 用例 07 / 13 / 15 / 17 等合法工程类型检查通过 +3. 负例:AND 吃 INT、赋值类型不匹配、INT+TIME、NOT INT、FB 输入类型不匹配、比较异型 → `type error` +4. 全部构建 + `ctest` 无回归 + +--- + +## 执行计划(单步确认) + +1. **`Typecheck.h`(新建 `compiler/include/compiler/Typecheck.h`)**:`TType` + `check_project` 声明 +2. **`Typecheck.cpp` 前半**:符号类型解析(VarRef/Field/Call/字面量)+ 表达式递归定类型 +3. **`Typecheck.cpp` 后半**:语句规则(Assign 含函数名/FUNCTION 禁写全局、FbCall 实参、IF/WHILE 条件) +4. **`typecheck_test`(`tests/src/typecheck_test.cpp` + CMake)**:用例 14 + 负例 + 正例 +5. **验证**:`cmake --build` + `ctest`(新增 `typecheck_types` 后 8/8 全绿) +6. **提交**:`Typecheck.h/cpp` + `typecheck_test` + 文档 diff --git a/Doc/索引.md b/Doc/索引.md index e4a511d..4eaf3af 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -11,6 +11,7 @@ doc/ compiler/词法.md compiler/语法.md compiler/符号表与链接.md + compiler/类型检查.md vm/扫描周期.md executor/执行器入口.md ``` @@ -23,6 +24,7 @@ doc/ | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | | [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 | | [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 | +| [`compiler/类型检查.md`](compiler/类型检查.md) | 12.7 类型检查:表达式类型、语句规则、错误类别 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 40a8484..efcffe0 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -11,7 +11,8 @@ add_library(compiler STATIC ./src/Lexer.cpp ./src/Project.cpp ./src/Parser.cpp - ./src/Linker.cpp) + ./src/Linker.cpp + ./src/Typecheck.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) diff --git a/compiler/include/compiler/Typecheck.h b/compiler/include/compiler/Typecheck.h new file mode 100644 index 0000000..0f40ce9 --- /dev/null +++ b/compiler/include/compiler/Typecheck.h @@ -0,0 +1,26 @@ +/** + * @file Typecheck.h + * @brief 类型检查 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + // 表达式求值类型(v1 三型,无隐式宽化) + enum class TType { Bool, Int, Time }; + + // 对工程做类型检查(在链接成功后调用)。 + // 规则见 Doc/compiler/类型检查.md。失败返回 false,err 前缀 "type error"。 + bool check_project(const Project& proj, const std::vector& units, + const LinkResult& link, std::string* err); +} diff --git a/compiler/src/Typecheck.cpp b/compiler/src/Typecheck.cpp new file mode 100644 index 0000000..bcee19f --- /dev/null +++ b/compiler/src/Typecheck.cpp @@ -0,0 +1,494 @@ +/** + * @file Typecheck.cpp + * @brief 类型检查 + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/compiler/类型检查.md): + * - 在链接成功后运行:符号/字段/函数存在性已由 12.6 保证,本阶段只查类型 + * - 表达式求值类型三型:BOOL / INT / TIME;无隐式宽化,INT 与 TIME 不混用 + * - 语句规则:赋值类型相等、FUNCTION 禁写全局(含 EXTERNAL,用例 14)、 + * FB 命名实参匹配字段类型、IF/WHILE 条件必须 BOOL + * - 错误:稳定前缀 "type error" + 文件(Expr/Stmt 无行列信息,只带文件) + * + * 函数清单: + * - tname TType → 文本名(BOOL/INT/TIME) + * - to_type(TypeKind) TypeKind → TType(FB 类型返回 false) + * - to_type(string) 符号 type_name 字符串 → TType + * - Checker::Checker (构造)存工程/源文件/链接结果/错误输出 + * - Checker::run 逐 POU 逐语句检查 + * - Checker::fail 组装 "type error: (file)" 返回 false + * - find_scope 按 POU 名取作用域 + * - find_sym 在作用域里按名查符号 + * - find_global 按名查全局符号 + * - find_pou 按名查 POU AST(函数返回类型用) + * - check_stmt 语句规则分发(赋值/FB 调用/IF/WHILE) + * - assign_target_type 解析赋值左值类型(局部/全局/函数名),FUNCTION 禁写全局 + * - check_expr 表达式定类型(递归) + * - check_project 对外入口:逐 POU 检查 + */ + +#include "compiler/Typecheck.h" + +#include +#include +#include + +namespace compiler { +namespace { + + /** + * @brief TType → 文本名(错误报文用) + * @param t 求值类型 + * @return "BOOL" / "INT" / "TIME" + */ + const char* tname(TType t) { + switch (t) { + case TType::Bool: return "BOOL"; + case TType::Int: return "INT"; + case TType::Time: return "TIME"; + } + return "?"; + } + + /** + * @brief TypeKind → TType + * @param k 类型种类 + * @param out 输出求值类型 + * @return true 标量(BOOL/INT/TIME);FB 类型返回 false + */ + bool to_type(TypeKind k, TType* out) { + switch (k) { + case TypeKind::Bool: *out = TType::Bool; return true; + case TypeKind::Int: *out = TType::Int; return true; + case TypeKind::Time: *out = TType::Time; return true; + default: return false; + } + } + + /** + * @brief 符号 type_name 字符串 → TType + * @param name "bool" / "int" / "time"(小写) + * @param out 输出求值类型 + * @return true 标量;FB 类型名返回 false + */ + bool to_type(const std::string& name, TType* out) { + if (name == "bool") { *out = TType::Bool; return true; } + if (name == "int") { *out = TType::Int; return true; } + if (name == "time") { *out = TType::Time; return true; } + return false; + } + + /** + * @brief 类型检查器 + * + * @details 链接结果只读;不修改 AST 与符号表。 + */ + class Checker { + public: + /** + * @brief 构造检查器 + * @param proj 工程定义(未用,保留接口对称;io 类型映射留待 12.8) + * @param units 全部源文件的 AST + * @param link 链接结果(符号/布局已解析) + * @param err 错误输出;可为 nullptr(静默) + */ + Checker(const Project& proj, const std::vector& units, + const LinkResult& link, std::string* err) + : proj_(proj), units_(units), link_(link), err_(err) {} + + /** + * @brief 逐 POU 逐语句做类型检查 + * @return true 全部通过;false 首个类型错误(err 已写) + */ + bool run() { + for (const SourceUnit& u : units_) { + for (const POU& p : u.ast.pous) { + const LinkResult::PouScope* sc = find_scope(p.name); + if (sc == nullptr) { + continue; // 不应发生(12.6 已登记) + } + for (const Stmt& st : p.body) { + if (!check_stmt(u.path, *sc, p, st)) { + return false; + } + } + } + } + return true; + } + + private: + /** + * @brief 组装 "type error: (file)" 写入 err + * @details Expr/Stmt 不携带行列,只报文件;err_ 为 nullptr 时静默 + * @param file 出错文件 + * @param msg 错误描述(不含前缀) + * @return 恒 false(便于 return fail(...) 一行退出) + */ + bool fail(const std::string& file, const std::string& msg) { + if (err_) { + *err_ = "type error: " + msg + " (" + file + ")"; + } + return false; + } + + /** + * @brief 按 POU 名取作用域 + * @param name POU 名(小写) + * @return 作用域指针;未找到返回 nullptr + */ + const LinkResult::PouScope* find_scope(const std::string& name) const { + for (const LinkResult::PouScope& sc : link_.scopes) { + if (sc.name == name) { + return ≻ + } + } + return nullptr; + } + + /** + * @brief 在作用域里按名查符号 + * @param sc 作用域 + * @param name 符号名(小写) + * @return 符号指针;未找到返回 nullptr + */ + const Symbol* find_sym(const LinkResult::PouScope& sc, + const std::string& name) const { + for (const Symbol& s : sc.syms) { + if (s.name == name) { + return &s; + } + } + return nullptr; + } + + /** + * @brief 按名查全局符号 + * @param name 全局名(小写) + * @return 符号指针;未找到返回 nullptr + */ + const Symbol* find_global(const std::string& name) const { + const auto it = link_.global_index.find(name); + if (it == link_.global_index.end()) { + return nullptr; + } + return &link_.globals[it->second]; + } + + /** + * @brief 按名查 POU AST(函数返回类型用) + * @param name POU 名(小写) + * @return POU 指针;未找到返回 nullptr + */ + const POU* find_pou(const std::string& name) const { + for (const SourceUnit& u : units_) { + for (const POU& p : u.ast.pous) { + if (p.name == name) { + return &p; + } + } + } + return nullptr; + } + + /** + * @brief 语句规则分发 + * @param file 出错文件 + * @param sc 当前作用域 + * @param pou 当前 POU(函数名赋值 / 禁写全局判定用) + * @param st 语句 AST + * @return true 合法;false(err 已写) + */ + bool check_stmt(const std::string& file, const LinkResult::PouScope& sc, + const POU& pou, const Stmt& st) { + switch (st.kind) { + case StmtKind::Assign: { + TType lhs; + if (!assign_target_type(file, sc, pou, st.target, &lhs)) { + return false; + } + TType rhs; + if (!check_expr(file, sc, *st.value, &rhs)) { + return false; + } + if (lhs != rhs) { + return fail(file, "type mismatch in assignment to '" + st.target + + "' (" + tname(lhs) + " vs " + tname(rhs) + ")"); + } + return true; + } + case StmtKind::FbCall: { + const auto it = sc.fb_instances.find(st.instance); + if (it == sc.fb_instances.end()) { + return fail(file, "no layout for FB instance '" + st.instance + "'"); + } + for (const FbArg& a : st.args) { + TType want = TType::Bool; + bool found = false; + for (const FbField& f : it->second.fields) { + if (f.name == a.name) { + found = to_type(f.type, &want); + break; + } + } + if (!found) { + return fail(file, "unknown input '" + a.name + "' for FB '" + + st.instance + "'"); + } + TType got; + if (!check_expr(file, sc, *a.value, &got)) { + return false; + } + if (got != want) { + return fail(file, "FB input '" + a.name + "' expects " + + tname(want)); + } + } + return true; + } + case StmtKind::If: + if (!check_cond(file, sc, *st.cond)) { + return false; + } + for (const Stmt& s : st.body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + for (const IfBranch& b : st.elsifs) { + if (!check_cond(file, sc, *b.cond)) return false; + for (const Stmt& s : b.body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + } + for (const Stmt& s : st.else_body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + return true; + case StmtKind::While: + if (!check_cond(file, sc, *st.cond)) { + return false; + } + for (const Stmt& s : st.body) { + if (!check_stmt(file, sc, pou, s)) return false; + } + return true; + } + return true; + } + + /** + * @brief 条件表达式必须为 BOOL(IF / WHILE 共用) + * @param file 出错文件 + * @param sc 当前作用域 + * @param e 条件表达式 + * @return true 合法;false(err 已写) + */ + bool check_cond(const std::string& file, const LinkResult::PouScope& sc, + const Expr& e) { + TType t; + if (!check_expr(file, sc, e, &t)) { + return false; + } + if (t != TType::Bool) { + return fail(file, "condition must be BOOL, got " + std::string(tname(t))); + } + return true; + } + + /** + * @brief 解析赋值左值类型 + * @details 普通左值:局部/输入/输出/外部/全局 → 符号类型; + * FUNCTION 内函数名 → 返回类型;**FUNCTION 内左值是全局/外部 → 拒绝** + * (用例 14:FUNCTION 禁止写全局,含经 VAR_EXTERNAL) + * @param file 出错文件 + * @param sc 当前作用域 + * @param pou 当前 POU + * @param target 左值标识符 + * @param out 输出左值类型 + * @return true 合法;false(err 已写) + */ + bool assign_target_type(const std::string& file, + const LinkResult::PouScope& sc, const POU& pou, + const std::string& target, TType* out) { + // FUNCTION 内对函数名赋值 = 结果值写入(用例 13 约定) + if (pou.kind == PouKind::Function && target == pou.name) { + return to_type(pou.result_type.kind, out) || + fail(file, "function result must be scalar"); + } + + const Symbol* s = find_sym(sc, target); + if (s == nullptr) { + s = find_global(target); + } + if (s == nullptr) { + return fail(file, "undeclared identifier '" + target + "'"); + } + + // FUNCTION 禁写全局(含经 VAR_EXTERNAL) + if (pou.kind == PouKind::Function && + (s->kind == SymbolKind::Global || s->kind == SymbolKind::External)) { + return fail(file, "function cannot write global '" + target + "'"); + } + + if (!to_type(s->type_name, out)) { + return fail(file, "'" + target + "' has no scalar type"); + } + return true; + } + + /** + * @brief 表达式定类型(递归) + * @param file 出错文件 + * @param sc 当前作用域 + * @param e 表达式 AST + * @param out 输出求值类型 + * @return true 合法;false(err 已写) + */ + bool check_expr(const std::string& file, const LinkResult::PouScope& sc, + const Expr& e, TType* out) { + switch (e.kind) { + case ExprKind::LitBool: + *out = TType::Bool; + return true; + case ExprKind::LitInt: + *out = TType::Int; + return true; + case ExprKind::LitTime: + *out = TType::Time; + return true; + case ExprKind::VarRef: { + const Symbol* s = find_sym(sc, e.name); + if (s == nullptr) { + s = find_global(e.name); + } + if (s == nullptr) { + return fail(file, "undeclared identifier '" + e.name + "'"); + } + if (s->kind == SymbolKind::FbInstance) { + return fail(file, "FB instance '" + e.name + "' cannot be used as a value"); + } + if (!to_type(s->type_name, out)) { + return fail(file, "'" + e.name + "' has no scalar type"); + } + return true; + } + case ExprKind::Field: { + const auto it = sc.fb_instances.find(e.name); + if (it == sc.fb_instances.end()) { + return fail(file, "no layout for FB instance '" + e.name + "'"); + } + for (const FbField& f : it->second.fields) { + if (f.name == e.field) { + return to_type(f.type, out) || + fail(file, "field '" + e.field + "' has no scalar type"); + } + } + return fail(file, "unknown field '" + e.field + "' for FB '" + e.name + "'"); + } + case ExprKind::Not: { + TType t; + if (!check_expr(file, sc, *e.operand, &t)) { + return false; + } + if (t != TType::Bool) { + return fail(file, "NOT operand must be BOOL"); + } + *out = TType::Bool; + return true; + } + case ExprKind::And: + case ExprKind::Or: { + TType l, r; + if (!check_expr(file, sc, *e.lhs, &l) || !check_expr(file, sc, *e.rhs, &r)) { + return false; + } + if (l != TType::Bool || r != TType::Bool) { + const char* op = (e.kind == ExprKind::And) ? "AND" : "OR"; + return fail(file, std::string(op) + " operands must be BOOL"); + } + *out = TType::Bool; + return true; + } + case ExprKind::Cmp: { + TType l, r; + if (!check_expr(file, sc, *e.lhs, &l) || !check_expr(file, sc, *e.rhs, &r)) { + return false; + } + if (l != r) { + return fail(file, "comparison of mismatched types (" + + std::string(tname(l)) + " vs " + + std::string(tname(r)) + ")"); + } + *out = TType::Bool; + return true; + } + case ExprKind::Add: + case ExprKind::Sub: + case ExprKind::Mul: + case ExprKind::Div: { + TType l, r; + if (!check_expr(file, sc, *e.lhs, &l) || !check_expr(file, sc, *e.rhs, &r)) { + return false; + } + if (l != TType::Int || r != TType::Int) { + return fail(file, "arithmetic operands must be INT (TIME has no arithmetic)"); + } + *out = TType::Int; + return true; + } + case ExprKind::Neg: { + TType t; + if (!check_expr(file, sc, *e.operand, &t)) { + return false; + } + if (t != TType::Int) { + return fail(file, "unary minus operand must be INT"); + } + *out = TType::Int; + return true; + } + case ExprKind::Call: { + const POU* f = find_pou(e.name); + if (f == nullptr) { + return fail(file, "undeclared function '" + e.name + "'"); + } + for (const auto& a : e.args) { + TType at; + if (!check_expr(file, sc, *a, &at)) { + return false; + } + } + if (!to_type(f->result_type.kind, out)) { + return fail(file, "function '" + e.name + "' result must be scalar"); + } + return true; + } + } + return fail(file, "unsupported expression"); + } + + // ---- 成员 ---- + const Project& proj_; // 工程定义(本阶段未用) + const std::vector& units_; // 全部源文件 AST + const LinkResult& link_; // 链接结果(只读) + std::string* err_; // 错误输出(可空) + }; + +} // namespace + +/** + * @brief 对工程做类型检查(对外入口,链接成功后调用) + * @details 逐 POU 逐语句检查;规则见 Doc/compiler/类型检查.md + * @param proj 工程定义(本阶段未用,保留接口对称) + * @param units 全部源文件的 AST + * @param link 链接结果(符号/布局已解析) + * @param err 错误输出;可为 nullptr(静默) + * @return true 全部通过;false 失败(err 前缀 "type error") + */ +bool check_project(const Project& proj, const std::vector& units, + const LinkResult& link, std::string* err) { + Checker c(proj, units, link, err); + return c.run(); +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index afd924d..204615e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,3 +56,14 @@ target_compile_definitions(linker_test PRIVATE add_test(NAME linker_links COMMAND linker_test) + +# 类型检查测试:用例 14 + 负例 + 正例(REPO_ROOT 注入源目录绝对路径) +add_executable(typecheck_test + ./src/typecheck_test.cpp) + +target_link_libraries(typecheck_test PRIVATE compiler) +target_compile_definitions(typecheck_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME typecheck_types + COMMAND typecheck_test) diff --git a/tests/src/typecheck_test.cpp b/tests/src/typecheck_test.cpp new file mode 100644 index 0000000..e680967 --- /dev/null +++ b/tests/src/typecheck_test.cpp @@ -0,0 +1,213 @@ +/** + * @file typecheck_test.cpp + * @brief 类型检查测试:用例 14 + 负例 + 正例 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Project.h" +#include "compiler/Typecheck.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// 从用例目录加载、链接、类型检查 +static bool check_case(const char* dir, compiler::LinkResult* r, std::string* err) { + using namespace compiler; + const std::string toml = std::string(REPO_ROOT) + "/tests/cases/" + dir + "/project.toml"; + Project p; + if (!parse_project(toml, &p, err)) { + return false; + } + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + f, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + if (!link_project(p, units, r, err)) { + return false; + } + return check_project(p, units, *r, err); +} + +// 从内联源码构造工程(写临时目录),加载、链接、类型检查 +static bool check_src(const char* src_name, const char* st_content, + std::string* err) { + using namespace compiler; + const std::string dir = std::string(REPO_ROOT) + "/build/tc_tmp_" + src_name; + std::filesystem::create_directories(dir); + const std::string toml_path = dir + "/project.toml"; + const std::string st_path = dir + "/main.st"; + std::FILE* f = std::fopen(toml_path.c_str(), "w"); + std::fprintf(f, "[project]\nname = \"tc\"\nentry = \"program MAIN\"\n" + "cycle_limit = 1000\ndt_ms = 10\n" + "[files]\nst = [\"main.st\"]\n"); + std::fclose(f); + f = std::fopen(st_path.c_str(), "w"); + std::fputs(st_content, f); + std::fclose(f); + + Project p; + if (!parse_project(toml_path, &p, err)) { + return false; + } + std::vector units; + for (const std::string& file : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + file, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + LinkResult r; + if (!link_project(p, units, &r, err)) { + return false; + } + return check_project(p, units, r, err); +} + +static bool expect_type_err(const char* src_name, const char* st_content, + const char* keyword) { + std::string err; + if (check_src(src_name, st_content, &err)) { + std::printf("FAIL %s: typecheck passed\n", src_name); + return false; + } + if (err.find("type error") != 0) { + std::printf("FAIL %s: want 'type error', got '%s'\n", src_name, err.c_str()); + return false; + } + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL %s: want '%s', got '%s'\n", src_name, keyword, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +// ---- 1. 用例 14:FUNCTION 写全局(经 VAR_EXTERNAL)---- + +static bool test_case14() { + std::string err; + compiler::LinkResult r; + if (check_case("14_function_write_global", &r, &err)) { + std::printf("FAIL case14: typecheck passed\n"); + return false; + } + CHECK(err.find("type error") == 0); + CHECK(err.find("function cannot write global") != std::string::npos); + return true; +} + +// ---- 2. 负例 ---- + +static bool test_negative() { + // AND 吃 INT + if (!expect_type_err("and_int", + "PROGRAM MAIN\nVAR\n a, b : INT;\n x : BOOL;\nEND_VAR\n" + " x := a AND b;\nEND_PROGRAM\n", + "AND operands must be BOOL")) { + return false; + } + // 赋值类型不匹配(INT 赋给 BOOL) + if (!expect_type_err("assign_mismatch", + "PROGRAM MAIN\nVAR\n b : BOOL;\nEND_VAR\n" + " b := 5;\nEND_PROGRAM\n", + "type mismatch in assignment to 'b'")) { + return false; + } + // INT 与 TIME 混用算术 + if (!expect_type_err("int_time", + "PROGRAM MAIN\nVAR\n x : INT;\n t : TIME;\n y : INT;\nEND_VAR\n" + " y := x + t;\nEND_PROGRAM\n", + "TIME has no arithmetic")) { + return false; + } + // NOT 吃 INT + if (!expect_type_err("not_int", + "PROGRAM MAIN\nVAR\n a : INT;\n b : BOOL;\nEND_VAR\n" + " b := NOT a;\nEND_PROGRAM\n", + "NOT operand must be BOOL")) { + return false; + } + // 比较异型(INT = TIME) + if (!expect_type_err("cmp_mismatch", + "PROGRAM MAIN\nVAR\n x : INT;\n t : TIME;\n b : BOOL;\nEND_VAR\n" + " b := x = t;\nEND_PROGRAM\n", + "comparison of mismatched types")) { + return false; + } + // FB 输入类型不匹配(TON pt 期望 TIME,给 TRUE) + if (!expect_type_err("fb_arg", + "PROGRAM MAIN\nVAR\n t : TON;\n q : BOOL;\nEND_VAR\n" + " t(in := TRUE, pt := TRUE);\n" + " q := t.Q;\nEND_PROGRAM\n", + "FB input 'pt' expects TIME")) { + return false; + } + // IF 条件非 BOOL + if (!expect_type_err("if_cond", + "PROGRAM MAIN\nVAR\n n : INT;\n x : INT;\nEND_VAR\n" + " IF n THEN\n x := 1;\n END_IF\nEND_PROGRAM\n", + "condition must be BOOL")) { + return false; + } + // FB 实例当值用 + if (!expect_type_err("fb_value", + "PROGRAM MAIN\nVAR\n t : TON;\n b : BOOL;\nEND_VAR\n" + " b := t;\nEND_PROGRAM\n", + "cannot be used as a value")) { + return false; + } + return true; +} + +// ---- 3. 正例 ---- + +static bool test_positive() { + std::string err; + compiler::LinkResult r; + CHECK(check_case("01_empty_main", &r, &err)); + CHECK(check_case("02_bool_assign", &r, &err)); + CHECK(check_case("03_short_circuit", &r, &err)); + CHECK(check_case("04_if_elsif_else", &r, &err)); + CHECK(check_case("05_while_normal", &r, &err)); + CHECK(check_case("07_int_arith", &r, &err)); + CHECK(check_case("08_time_literal", &r, &err)); + CHECK(check_case("09_gvl_external", &r, &err)); + CHECK(check_case("13_function_call", &r, &err)); + CHECK(check_case("15_fb_instance", &r, &err)); + CHECK(check_case("17_ton", &r, &err)); + CHECK(check_case("18_tof_ctu", &r, &err)); + CHECK(check_case("20_line1", &r, &err)); + return true; +} + +int main() { + if (!test_case14()) return 1; + if (!test_negative()) return 1; + if (!test_positive()) return 1; + std::printf("typecheck_test: %d checks passed\n", g_checks); + return 0; +} From d296a9215e3d26030f47f199574a655fa838bdc1 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:30:43 +0800 Subject: [PATCH 25/69] =?UTF-8?q?=E8=A7=84=E5=88=92=2012.8=20=E5=AF=84?= =?UTF-8?q?=E5=AD=98=E5=99=A8=E7=A0=81=EF=BC=9A=E5=AD=98=E5=82=A8=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E3=80=81=E6=8C=87=E4=BB=A4=E6=A8=A1=E5=BC=8F=E3=80=81?= =?UTF-8?q?8=20=E7=89=87=E6=89=A7=E8=A1=8C=E8=AE=A1=E5=88=92=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 寄存器码.md:帧/数据区布局(FB 实例块持久化)、短路编跳转、函数帧约定(结果 r0) - 用户 FB 调用内联展开定案;内建 CAL_TON/TOF/CTU - 8 片计划:骨架→I/Q/M→短路→IF→WHILE→算术→CALL→FB+line1 全链路 - 索引.md 登记 --- Doc/compiler/寄存器码.md | 121 +++++++++++++++++++++++++++++++++++++++ Doc/索引.md | 2 + 2 files changed, 123 insertions(+) create mode 100644 Doc/compiler/寄存器码.md diff --git a/Doc/compiler/寄存器码.md b/Doc/compiler/寄存器码.md new file mode 100644 index 0000000..7e9cb80 --- /dev/null +++ b/Doc/compiler/寄存器码.md @@ -0,0 +1,121 @@ +# 寄存器码(12.8) + +compiler 模块的代码生成。输入:`Project` + `Unit`(AST)+ `LinkResult`;输出:`.stb` 映像字节(`std::vector`)。全工程定案见 [`初步计划.md`](../初步计划.md) 12.8,指令/映像布局见 [`指令与映像.md`](../isa/指令与映像.md)。 + +## 做 / 不做 + +**做** + +- 寄存器分配:局部/变量固定寄存器,表达式临时值往后编号,`nregs` 写进函数头 +- 跳转先留洞、后回填;`AND` / `OR` **必须编成跳转**(短路),禁止两边都算 +- 常量表(`LOADK`)、全局/实例槽(`LOAD_GLOBAL` / `STORE_GLOBAL` 等) +- `CALL fn_id` 有界帧;用户 FB 调用**内联展开**;内建 `CAL_TON` / `CAL_TOF` / `CAL_CTU` +- 拼装映像:函数表 / 常量表 / 数据段 / 头(`cycle_limit`、`dt_ms`、工程哈希、入口) + +**不做(第一版)** + +- 图染色、SSA、生命周期分析、优化、异常表、闭包 +- 函数指针、间接跳转、`REAL` + +## 存储布局(v1 定案) + +| 数据 | 放哪 | 说明 | +|---|---|---| +| PROGRAM / FUNCTION 标量变量与临时 | **寄存器**(函数帧) | 每周期帧保留由 VM 负责(12.9) | +| FB 实例(任意 POU 内声明) | **数据区实例块**(绝对地址) | 状态跨周期持久 | +| 全局(含 I/Q/M) | 数据区(声明序) | 12.6 已定槽号 | + +- **数据区** = 全局块(声明序,按对齐规则:BOOL 1B / INT 2B / TIME 8B,起点对齐自身宽度)→ FB 实例块(按实例声明序,各块按类型布局大小、8 字节对齐) +- **指令 slot = 数据区字节偏移**(u16);符号表 `address` 保持逻辑槽号(12.6 冻结),编译时做 槽号 → 偏移 映射 +- 字段访问 `fb.Q`:`LOAD_GLOBAL rd, <实例基址+字段偏移>`(编译期算死);I/Q/M 用 `LOAD_I` / `STORE_Q` / `LOAD_M` / `STORE_M` 同槽号 +- 函数帧寄存器:`FUNCTION` 结果固定 **r0**,`VAR_INPUT` 从 r1 起按声明序,`VAR` 续后;PROGRAM / FB 变量从 r0 起按声明序 + +## 寄存器分配 + +- 局部变量固定占用前段(0..k-1) +- 表达式临时从 k 起,**语句内递增、语句结束复用基址**(不分析生命周期) +- `nregs` = 全函数临时峰值 + 1 + +## 表达式与语句的指令模式 + +```text +字面量 LOADK rd, const_id +变量读 LOAD_GLOBAL/LOAD_I/LOAD_M rd, slot (或帧内 MOVE) +字段读 LOAD_GLOBAL rd, <基址+偏移> +函数调用 <实参求值 → 参数寄存器> CALL fn_id MOVE rd, r0 + +NOT NOT rd, t +AND/OR 短路(必须跳转): + JF(AND)/JT(OR) rd, L_end + MOVE rd, t + L_end: +比较 CMP_xx rd, t1, t2 +算术 ADD/SUB/MUL/DIV rd, t1, t2 +负号 SUB rd, r0, t (0 - t,r0 恒 0) + +IF JF t, L_else + JMP L_end + L_else: (ELSIF 逐级) JF t, L_next ... + L_end: +WHILE L_loop: JF t, L_end JMP L_loop L_end: + +FB 调用(用户,内联展开): + <每个实参求值 → 临时> STORE_GLOBAL <实例基址+字段偏移>, 临时 + <内联体:读输入字段、算输出、写输出字段> +内建 实参同左 → CAL_TON/CAL_TOF/CAL_CTU <实例偏移> +``` + +- 常量统一进常量表(BOOL / INT / TIME),`const_id` = 首次出现序 +- 空条件寄存器约定:`r0` 恒 0(`enc_jmp` 用),`SUB rd, r0, t` 实现取负 + +## 用户 FB 调用内联(v1 定案) + +FB 体共享会与「绝对字段地址」冲突(不同实例基址不同),v1 选择**调用点内联展开**: + +- 调用点:实参写入该实例字段 → 内联该 FB 的函数体(字段地址按本实例基址算死)→ 字段读取自然可用 +- 代价:字节码随调用点数膨胀;v1 规模可接受;以后可升级为「基址寄存器寻址」指令 +- FB 无递归、无动态实例,内联安全 + +## 映像拼装 + +```text +函数表:收集序 fn_id → { nregs, code_offset(相对字节码段), code_len } +常量表:const_id → { tag, value } +数据段:全局 + 实例块(见上) +头:cycle_limit / dt_ms / 工程哈希(12.3)/ entry=MAIN fn_id / n_globals / n_i=n_q=n_m=0 +``` + +- 用 `isa` 的 `enc_*` 编码指令、`ImageView` 校验回读 +- 工程哈希用 12.3 的 `compute_project_hash` + +## 错误 + +稳定前缀 `codegen error`: + +```text +codegen error: register overflow (>256) in function 'f' +codegen error: constant table overflow +codegen error: slot out of range +``` + +## 完成标准 + +1. `line1` 能写出映像(函数字节码 + 全局/FB 槽 + 工程哈希),`ImageView` 校验通过 +2. 跳转回填无悬空(disasm 抽查) +3. 用例 1~9、13、15、17、18、20 编译出映像;负例(10/11/12/14/16/19)仍在前层被拒 +4. 全部构建 + `ctest` 无回归 + +--- + +## 执行计划(8 片,每片验证编译产物) + +1. **`Codegen.h`(新建)+ `Codegen.cpp` 骨架**:帧/槽/常量表/指令缓冲/回填基础设施;`MOVE` `LOADK` `RET` → 用例 1、2 出映像 +2. **全局与 I/Q/M**:`LOAD_I` / `STORE_Q` / `LOAD_M` / `STORE_M` / `LOAD_GLOBAL` / `STORE_GLOBAL`(用例 9) +3. **`NOT` + 短路 `AND` / `OR`**(用例 3,断言产物含跳转) +4. **`CMP_*` + `JMP` / `JT` / `JF` → `IF`**(用例 4) +5. **`WHILE`**(用例 5;`cycle_limit` 进映像头) +6. **`ADD/SUB/MUL/DIV` + TIME 常量**(用例 7、8) +7. **`CALL fn_id` + 有界帧**(用例 13) +8. **FB 实例块 + 字段偏移;内联用户 FB;`CAL_TON` / `CAL_TOF` / `CAL_CTU`**(用例 15、17、18、20 line1 全链路) + +每片配 `codegen_test` 断言(nregs / 指令序列 disasm / 回填 / 常量表 / 数据区);最终 `ctest` 9/9。 diff --git a/Doc/索引.md b/Doc/索引.md index 4eaf3af..44cd671 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -12,6 +12,7 @@ doc/ compiler/语法.md compiler/符号表与链接.md compiler/类型检查.md + compiler/寄存器码.md vm/扫描周期.md executor/执行器入口.md ``` @@ -25,6 +26,7 @@ doc/ | [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 | | [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 | | [`compiler/类型检查.md`](compiler/类型检查.md) | 12.7 类型检查:表达式类型、语句规则、错误类别 | +| [`compiler/寄存器码.md`](compiler/寄存器码.md) | 12.8 寄存器码:布局、指令模式、内联 FB、8 片计划 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | From 66359a0a26863c5aede7df00d38fb8a15ead6cbf Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:32:31 +0800 Subject: [PATCH 26/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=201=EF=BC=9A?= =?UTF-8?q?=E5=AF=84=E5=AD=98=E5=99=A8=E7=A0=81=E9=AA=A8=E6=9E=B6=EF=BC=88?= =?UTF-8?q?MOVE/LOADK/RET=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Codegen.h/cpp:帧寄存器(0 起声明序)、常量表、函数表、小端映像拼装 - 切片 1 范围:PROGRAM 标量 + 字面量 LOADK + 变量 MOVE + RET;其余构造报 codegen error - codegen_test:23 断言(用例 01 空 MAIN 出 RET、02 LOADK r0/r1 + 常量表),ctest 9/9 --- compiler/CMakeLists.txt | 3 +- compiler/include/compiler/Codegen.h | 25 +++ compiler/src/Codegen.cpp | 321 ++++++++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/codegen_test.cpp | 127 +++++++++++ 5 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 compiler/include/compiler/Codegen.h create mode 100644 compiler/src/Codegen.cpp create mode 100644 tests/src/codegen_test.cpp diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index efcffe0..eecfc8e 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -12,7 +12,8 @@ add_library(compiler STATIC ./src/Project.cpp ./src/Parser.cpp ./src/Linker.cpp - ./src/Typecheck.cpp) + ./src/Typecheck.cpp + ./src/Codegen.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) diff --git a/compiler/include/compiler/Codegen.h b/compiler/include/compiler/Codegen.h new file mode 100644 index 0000000..ee72f53 --- /dev/null +++ b/compiler/include/compiler/Codegen.h @@ -0,0 +1,25 @@ +/** + * @file Codegen.h + * @brief 寄存器码生成(12.8,切片 1:MOVE / LOADK / RET) + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + // 编译工程为 .stb 映像字节(在链接 + 类型检查成功后调用)。 + // 失败返回 false,err 前缀 "codegen error"。 + bool codegen_project(const Project& proj, const std::vector& units, + const LinkResult& link, std::vector* image, + std::string* err); +} diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp new file mode 100644 index 0000000..cc5edf3 --- /dev/null +++ b/compiler/src/Codegen.cpp @@ -0,0 +1,321 @@ +/** + * @file Codegen.cpp + * @brief 寄存器码生成(12.8,切片 1:MOVE / LOADK / RET) + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/compiler/寄存器码.md): + * - 切片 1 范围:PROGRAM 标量变量帧(r0 起按声明序)、字面量 LOADK、 + * 变量间 MOVE、RET;空 MAIN 出映像。其余构造报 codegen error(后续切片展开) + * - 常量统一进常量表(BOOL/INT/TIME),const_id = 首次出现序 + * - 映像:头(cycle_limit/dt_ms/工程哈希/入口)+ 常量表 + 函数表 + 字节码, + * 数据段为空(无全局/实例) + * + * 函数清单: + * - put_le32 / put_le64 小端写入映像缓冲 + * - Builder::Builder (构造)存工程/源文件/链接结果/输出 + * - Builder::run 逐 POU 建函数 → 拼映像 + * - Builder::fail 组装 "codegen error: " 返回 false + * - build_frame 给 POU 标量变量分配帧寄存器(0 起按声明序) + * - build_function 编译一个 POU 的语句体 + * - compile_stmt 语句编译(切片 1:仅赋值) + * - compile_expr 表达式编译(切片 1:仅字面量 / 变量读) + * - const_id 取常量表 id(无则追加) + * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + * - codegen_project 对外入口 + */ + +#include "compiler/Codegen.h" + +#include +#include +#include +#include + +#include "isa/Encode.h" +#include "isa/Image.h" +#include "isa/Instr.h" +#include "isa/Types.h" + +namespace compiler { +namespace { + + // ---- 小端写入(Image.cpp 内部实现不可见,这里自带最小版)---- + + void put_le32(std::vector& b, size_t off, uint32_t v) { + b[off + 0] = static_cast(v & 0xFFu); + b[off + 1] = static_cast((v >> 8) & 0xFFu); + b[off + 2] = static_cast((v >> 16) & 0xFFu); + b[off + 3] = static_cast((v >> 24) & 0xFFu); + } + + void put_le64(std::vector& b, size_t off, uint64_t v) { + for (int i = 0; i < 8; ++i) { + b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + } + + /** + * @brief 代码生成器(切片 1) + */ + class Builder { + public: + /** + * @brief 构造生成器 + * @param proj 工程定义(cycle_limit / dt_ms / 哈希用) + * @param units 全部源文件的 AST + * @param link 链接结果(POU 顺序 / 符号) + * @param image 输出映像字节 + * @param err 错误输出;可为 nullptr(静默) + */ + Builder(const Project& proj, const std::vector& units, + const LinkResult& link, std::vector* image, std::string* err) + : proj_(proj), units_(units), link_(link), image_(image), err_(err) {} + + /** + * @brief 逐 POU 建函数后拼映像 + * @return true 成功;false(err 已写,前缀 "codegen error") + */ + bool run() { + for (const LinkResult::PouScope& sc : link_.scopes) { + const POU* pou = find_pou(sc.name); + if (pou == nullptr) { + continue; + } + FuncCtx f; + f.name = sc.name; + if (!build_function(*pou, &f)) { + return false; + } + funcs_.push_back(std::move(f)); + } + return assemble_image(); + } + + private: + /** + * @brief 组装错误消息并返回 false + * @param msg 错误描述(不含前缀) + * @return 恒 false + */ + bool fail(const std::string& msg) { + if (err_) { + *err_ = "codegen error: " + msg; + } + return false; + } + + /** + * @brief 按名查 POU AST + * @param name POU 名(小写) + * @return POU 指针;未找到返回 nullptr + */ + const POU* find_pou(const std::string& name) const { + for (const SourceUnit& u : units_) { + for (const POU& p : u.ast.pous) { + if (p.name == name) { + return &p; + } + } + } + return nullptr; + } + + // 每函数的编译态 + struct FuncCtx { + std::string name; + std::vector code; // 字节码(函数表 code_offset 相对此段) + std::map regs; // 变量名 → 帧寄存器 + uint8_t nregs = 0; // 变量数(切片 1 无临时) + }; + + /** + * @brief 编译一个 POU + * @details 切片 1:仅 PROGRAM + 标量变量(FB 实例/函数留后续切片) + * @param pou POU AST + * @param f 输出函数编译态 + * @return true 成功;false(err 已写) + */ + bool build_function(const POU& pou, FuncCtx* f) { + if (pou.kind != PouKind::Program) { + return fail("only PROGRAM supported in slice 1 ('" + pou.name + "')"); + } + for (const VarBlock& b : pou.blocks) { + for (const VarDecl& d : b.vars) { + if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { + return fail("FB instance not supported in slice 1 ('" + d.name + "')"); + } + if (f->regs.size() > 255) { + return fail("register overflow in function '" + pou.name + "'"); + } + f->regs[d.name] = f->nregs++; + } + } + for (const Stmt& st : pou.body) { + if (!compile_stmt(*f, st)) { + return false; + } + } + f->code.push_back(isa::enc_ret()); + return true; + } + + /** + * @brief 编译一条语句(切片 1:仅赋值) + * @param f 当前函数 + * @param st 语句 AST + * @return true 成功;false(err 已写) + */ + bool compile_stmt(FuncCtx& f, const Stmt& st) { + if (st.kind != StmtKind::Assign) { + return fail("statement not supported in slice 1"); + } + const auto it = f.regs.find(st.target); + if (it == f.regs.end()) { + return fail("no register for '" + st.target + "'"); + } + const uint8_t rd = it->second; + uint8_t rs = 0; + const ExprKind k = st.value->kind; + if (k == ExprKind::LitBool || k == ExprKind::LitInt || + k == ExprKind::LitTime) { + const isa::types::TypeTag tag = + k == ExprKind::LitBool ? isa::types::Bool + : k == ExprKind::LitInt ? isa::types::Int + : isa::types::Time; + f.code.push_back( + isa::enc_imm(isa::Op::LOADK, rd, const_id(tag, st.value->int_value))); + return true; + } + if (k == ExprKind::VarRef) { + const auto sit = f.regs.find(st.value->name); + if (sit == f.regs.end()) { + return fail("no register for '" + st.value->name + "'"); + } + f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, sit->second)); + return true; + } + return fail("expression not supported in slice 1"); + } + + /** + * @brief 取常量表 id(无则追加) + * @param tag 类型标记(BOOL/INT/TIME) + * @param value 常量值 + * @return const_id(u16) + */ + uint16_t const_id(isa::types::TypeTag tag, int64_t value) { + for (size_t i = 0; i < consts_.size(); ++i) { + if (consts_[i].tag == tag && consts_[i].value == static_cast(value)) { + return static_cast(i); + } + } + consts_.push_back({tag, static_cast(value)}); + return static_cast(consts_.size() - 1); + } + + /** + * @brief 拼映像:头 + 常量表 + 函数表 + 字节码(数据段空) + * @return true 成功;false(err 已写) + */ + bool assemble_image() { + const uint32_t off_const = isa::kHeaderSize; + const uint32_t off_funcs = off_const + + static_cast(consts_.size()) * isa::kConstEntrySize; + uint32_t off_code = off_funcs + + static_cast(funcs_.size()) * isa::kFuncRowSize; + uint32_t code_total = 0; + for (const FuncCtx& f : funcs_) { + code_total += static_cast(f.code.size()) * 4; + } + const uint32_t off_end = off_code + code_total; + + std::vector& b = *image_; + b.assign(off_end, 0); + put_le32(b, 0, isa::kMagic); + put_le32(b, 4, isa::kVersion); + put_le32(b, 8, proj_.cycle_limit); + put_le32(b, 12, proj_.dt_ms); + + uint64_t hash = isa::kFnvBasis; + if (!compute_project_hash(proj_, &hash, err_)) { + return false; + } + put_le64(b, 16, hash); + + uint32_t entry = 0; + for (size_t i = 0; i < funcs_.size(); ++i) { + if (funcs_[i].name == "main") { + entry = static_cast(i); + } + } + put_le32(b, 24, entry); + put_le32(b, 28, 0); // n_globals(切片 1 无全局) + put_le32(b, 32, 0); // n_i + put_le32(b, 36, 0); // n_q + put_le32(b, 40, 0); // n_m + put_le32(b, 44, static_cast(consts_.size())); + put_le32(b, 48, static_cast(funcs_.size())); + put_le32(b, 52, off_const); + put_le32(b, 56, off_funcs); + put_le32(b, 60, off_code); + put_le32(b, 64, off_end); // offset_fb(空) + put_le32(b, 68, off_end); // offset_data(空) + + for (size_t i = 0; i < consts_.size(); ++i) { + const size_t o = off_const + i * isa::kConstEntrySize; + put_le32(b, o, static_cast(consts_[i].tag)); + put_le64(b, o + 4, consts_[i].value); + } + + uint32_t c = off_code; + for (size_t i = 0; i < funcs_.size(); ++i) { + const FuncCtx& f = funcs_[i]; + const size_t o = off_funcs + i * isa::kFuncRowSize; + put_le32(b, o, f.nregs); + put_le32(b, o + 4, 0); // code_offset 相对段起点 = 前序函数长度和 + put_le32(b, o + 8, static_cast(f.code.size())); + for (const isa::Instr in : f.code) { + put_le32(b, c, in); + c += 4; + } + } + // code_offset 回填(相对字节码段起点) + uint32_t acc = 0; + for (size_t i = 0; i < funcs_.size(); ++i) { + const size_t o = off_funcs + i * isa::kFuncRowSize; + put_le32(b, o + 4, acc); + acc += static_cast(funcs_[i].code.size()) * 4; + } + return true; + } + + // ---- 成员 ---- + const Project& proj_; + const std::vector& units_; + const LinkResult& link_; + std::vector* image_; + std::string* err_; + std::vector funcs_; + std::vector consts_; + }; + +} // namespace + +/** + * @brief 编译工程为 .stb 映像字节(对外入口) + * @param proj 工程定义 + * @param units 全部源文件的 AST + * @param link 链接结果(在类型检查成功后调用) + * @param image 输出映像字节 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "codegen error") + */ +bool codegen_project(const Project& proj, const std::vector& units, + const LinkResult& link, std::vector* image, + std::string* err) { + Builder b(proj, units, link, image, err); + return b.run(); +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 204615e..5ab5034 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -67,3 +67,14 @@ target_compile_definitions(typecheck_test PRIVATE add_test(NAME typecheck_types COMMAND typecheck_test) + +# 寄存器码测试(12.8 切片 1):用例 01/02 出映像(REPO_ROOT 注入源目录绝对路径) +add_executable(codegen_test + ./src/codegen_test.cpp) + +target_link_libraries(codegen_test PRIVATE compiler) +target_compile_definitions(codegen_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME codegen_slice1 + COMMAND codegen_test) diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp new file mode 100644 index 0000000..a0f6751 --- /dev/null +++ b/tests/src/codegen_test.cpp @@ -0,0 +1,127 @@ +/** + * @file codegen_test.cpp + * @brief 寄存器码测试(12.8 切片 1):用例 01/02 出映像 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include +#include + +#include "compiler/Codegen.h" +#include "compiler/Linker.h" +#include "compiler/Project.h" +#include "compiler/Typecheck.h" +#include "isa/Encode.h" +#include "isa/Image.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// 从用例目录走完整编译管线(解析 → 链接 → 类型检查 → 代码生成) +static bool compile_case(const char* dir, std::vector* image, std::string* err) { + using namespace compiler; + const std::string toml = std::string(REPO_ROOT) + "/tests/cases/" + dir + "/project.toml"; + Project p; + if (!parse_project(toml, &p, err)) { + return false; + } + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + f, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + LinkResult link; + if (!link_project(p, units, &link, err)) { + return false; + } + if (!check_project(p, units, link, err)) { + return false; + } + return codegen_project(p, units, link, image, err); +} + +// ---- 1. 用例 01:空 MAIN + RET ---- + +static bool test_case01() { + std::vector img; + std::string err; + CHECK(compile_case("01_empty_main", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().cycle_limit == 1000); + CHECK(v.header().dt_ms == 10); + CHECK(v.header().entry_fn_id == 0); + CHECK(v.header().n_funcs == 1); + CHECK(v.header().n_consts == 0); + CHECK(v.header().n_globals == 0); + CHECK(v.header().project_hash != isa::kFnvBasis); + + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 0); + CHECK(r.code_len == 1); + + char buf[32]; + isa::disasm(v.code_bytes()[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + +// ---- 2. 用例 02:BOOL 赋值(TRUE / FALSE → LOADK)---- + +static bool test_case02() { + std::vector img; + std::string err; + CHECK(compile_case("02_bool_assign", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_funcs == 1); + CHECK(v.header().n_consts == 2); // TRUE 与 FALSE + + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 2); // a, b + CHECK(r.code_len == 3); + + // 从字节码段取指令(按函数行 code_offset 定位) + const uint8_t* base = v.code_bytes(); + char buf[64]; + isa::disasm(reinterpret_cast(base)[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r0, 0") == 0); // a := TRUE(常量 0) + isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r1, 1") == 0); // b := FALSE(常量 1) + isa::disasm(reinterpret_cast(base)[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + + // 常量表:0 = BOOL 1,1 = BOOL 0 + const isa::ConstEntry c0 = v.const_entry(0); + CHECK(c0.tag == isa::types::Bool && c0.value == 1); + const isa::ConstEntry c1 = v.const_entry(1); + CHECK(c1.tag == isa::types::Bool && c1.value == 0); + return true; +} + +int main() { + if (!test_case01()) return 1; + if (!test_case02()) return 1; + std::printf("codegen_test: %d checks passed\n", g_checks); + return 0; +} From ac411e6a4a9e157bb664ad744088704c2641b5cc Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:38:57 +0800 Subject: [PATCH 27/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=202=EF=BC=9A?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E6=95=B0=E6=8D=AE=E5=8C=BA=E4=B8=8E=20I/Q/M?= =?UTF-8?q?=EF=BC=88LOAD/STORE=20=E6=8C=87=E4=BB=A4=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据区布局:声明序 + 对齐(BOOL 1B/INT 2B/TIME 8B),初值入数据段 - 读全局 LOAD_GLOBAL / io.input 绑定用 LOAD_I;写 STORE_GLOBAL / io.output 用 STORE_Q;写 io.input 拒绝 - 临时寄存器分配:语句内递增、语句结束复用基址 - codegen_test:51 断言(用例 09 初值+LOAD_GLOBAL、io 绑定 LOAD_I/STORE_Q、混合布局偏移、写输入负例),ctest 9/9 --- compiler/src/Codegen.cpp | 299 +++++++++++++++++++++++++++++++------ tests/src/codegen_test.cpp | 164 ++++++++++++++++++++ 2 files changed, 421 insertions(+), 42 deletions(-) diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index cc5edf3..da4f0c1 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,32 +1,43 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 1:MOVE / LOADK / RET) + * @brief 寄存器码生成(12.8,切片 2:+ 全局 / I/Q/M) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1 范围:PROGRAM 标量变量帧(r0 起按声明序)、字面量 LOADK、 - * 变量间 MOVE、RET;空 MAIN 出映像。其余构造报 codegen error(后续切片展开) + * - 切片 1:PROGRAM 标量变量帧(r0 起按声明序)、字面量 LOADK、 + * 变量间 MOVE、RET;空 MAIN 出映像。 + * - 切片 2:+ 全局数据区(声明序、对齐 BOOL 1B / INT 2B / TIME 8B、 + * 初值入数据段);读全局 LOAD_GLOBAL / io.input 绑定用 LOAD_I; + * 写全局 STORE_GLOBAL / io.output 绑定用 STORE_Q;写 io.input → 拒绝。 + * 其余构造报 codegen error(后续切片展开) * - 常量统一进常量表(BOOL/INT/TIME),const_id = 首次出现序 - * - 映像:头(cycle_limit/dt_ms/工程哈希/入口)+ 常量表 + 函数表 + 字节码, - * 数据段为空(无全局/实例) + * - 指令 slot = 数据区字节偏移;符号表 address 保持逻辑槽号(12.6 冻结) * * 函数清单: * - put_le32 / put_le64 小端写入映像缓冲 - * - Builder::Builder (构造)存工程/源文件/链接结果/输出 - * - Builder::run 逐 POU 建函数 → 拼映像 - * - Builder::fail 组装 "codegen error: " 返回 false - * - build_frame 给 POU 标量变量分配帧寄存器(0 起按声明序) - * - build_function 编译一个 POU 的语句体 - * - compile_stmt 语句编译(切片 1:仅赋值) - * - compile_expr 表达式编译(切片 1:仅字面量 / 变量读) - * - const_id 取常量表 id(无则追加) - * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 - * - codegen_project 对外入口 + * - align_up 按 2 的幂宽度向上对齐 + * - width_of 类型名 → 槽宽(bool 1 / int 2 / time 8) + * - Builder::Builder (构造)存工程/源文件/链接结果/输出,收集 io 绑定分类 + * - Builder::run 逐 POU 建函数 → 布局数据区 → 拼映像 + * - Builder::fail 组装 "codegen error: " 返回 false + * - find_pou 按名查 POU AST + * - build_function 编译一个 POU 的语句体(帧寄存器分配) + * - compile_stmt 语句编译(切片 2:仅赋值,左值可帧寄存器或全局) + * - compile_expr 表达式编译到寄存器(字面量 / 变量读,含全局 LOAD_*) + * - store_target 赋值左值:帧寄存器直写或 STORE_* 到全局槽 + * - load_op / store_op 按 io 绑定选读取/写入操作码 + * - global_offset 逻辑槽号 → 数据区字节偏移 + * - layout_data 布局全局数据区(对齐 + 初值字节) + * - init_of 查全局声明的初值(AST) + * - const_id 取常量表 id(无则追加) + * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + 数据段 + * - codegen_project 对外入口 */ #include "compiler/Codegen.h" +#include #include #include #include @@ -55,6 +66,27 @@ namespace { } } + /** + * @brief 按 2 的幂宽度向上对齐 + * @param v 当前偏移 + * @param w 对齐宽度(1/2/8) + * @return 对齐后的偏移 + */ + uint32_t align_up(uint32_t v, uint32_t w) { + return (v + w - 1) & ~(w - 1); + } + + /** + * @brief 类型名 → 槽宽(对齐规则见 Doc/isa/指令与映像.md) + * @param name "bool" / "int" / "time" + * @return 1 / 2 / 8;未知返回 1 + */ + uint32_t width_of(const std::string& name) { + if (name == "int") return 2; + if (name == "time") return 8; + return 1; // bool 及未知 + } + /** * @brief 代码生成器(切片 1) */ @@ -70,13 +102,30 @@ namespace { */ Builder(const Project& proj, const std::vector& units, const LinkResult& link, std::vector* image, std::string* err) - : proj_(proj), units_(units), link_(link), image_(image), err_(err) {} + : proj_(proj), units_(units), link_(link), image_(image), err_(err) { + // io 绑定分类(名已折小写;不创造变量,只影响操作码选择) + for (const isa::IoBinding& b : proj_.io) { + std::string key = b.var; + for (char& ch : key) { + ch = static_cast(std::tolower(static_cast(ch))); + } + if (b.is_input) { + io_input_[key] = true; + } else { + io_output_[key] = true; + } + } + } /** - * @brief 逐 POU 建函数后拼映像 + * @brief 布局数据区 → 逐 POU 建函数 → 拼映像 + * @details 数据区偏移先定(函数编译引用 global_offset) * @return true 成功;false(err 已写,前缀 "codegen error") */ bool run() { + if (!layout_data()) { + return false; + } for (const LinkResult::PouScope& sc : link_.scopes) { const POU* pou = find_pou(sc.name); if (pou == nullptr) { @@ -126,9 +175,31 @@ namespace { std::string name; std::vector code; // 字节码(函数表 code_offset 相对此段) std::map regs; // 变量名 → 帧寄存器 - uint8_t nregs = 0; // 变量数(切片 1 无临时) + uint8_t nlocals = 0; // 变量数 = 临时寄存器起始 + uint8_t nregs = 0; // 峰值(变量 + 临时) + uint8_t temp_used = 0; // 本语句已用临时数(语句结束清零) }; + /** + * @brief 语句开始:临时寄存器基址复用 + * @param f 当前函数 + */ + void begin_stmt(FuncCtx& f) { f.temp_used = 0; } + + /** + * @brief 分配一个临时寄存器(语句内递增) + * @param f 当前函数 + * @return 临时寄存器号(可能更新 nregs 峰值) + */ + uint8_t alloc_temp(FuncCtx& f) { + const uint8_t r = f.nlocals + f.temp_used; + ++f.temp_used; + if (static_cast(f.nlocals) + f.temp_used > f.nregs) { + f.nregs = f.nlocals + f.temp_used; + } + return r; + } + /** * @brief 编译一个 POU * @details 切片 1:仅 PROGRAM + 标量变量(FB 实例/函数留后续切片) @@ -141,6 +212,10 @@ namespace { return fail("only PROGRAM supported in slice 1 ('" + pou.name + "')"); } for (const VarBlock& b : pou.blocks) { + // External / Global 走数据区槽,不占帧寄存器 + if (b.section == VarSection::External || b.section == VarSection::Global) { + continue; + } for (const VarDecl& d : b.vars) { if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { return fail("FB instance not supported in slice 1 ('" + d.name + "')"); @@ -151,7 +226,9 @@ namespace { f->regs[d.name] = f->nregs++; } } + f->nlocals = f->nregs; for (const Stmt& st : pou.body) { + begin_stmt(*f); if (!compile_stmt(*f, st)) { return false; } @@ -161,41 +238,168 @@ namespace { } /** - * @brief 编译一条语句(切片 1:仅赋值) + * @brief 编译一条语句(切片 2:仅赋值,左值可为帧寄存器或全局) * @param f 当前函数 * @param st 语句 AST * @return true 成功;false(err 已写) */ bool compile_stmt(FuncCtx& f, const Stmt& st) { if (st.kind != StmtKind::Assign) { - return fail("statement not supported in slice 1"); + return fail("statement not supported in slice 2"); } const auto it = f.regs.find(st.target); if (it == f.regs.end()) { - return fail("no register for '" + st.target + "'"); + // 全局 / 外部左值 → STORE_* 到数据区 + return store_target(f, st.target, *st.value); } const uint8_t rd = it->second; - uint8_t rs = 0; - const ExprKind k = st.value->kind; - if (k == ExprKind::LitBool || k == ExprKind::LitInt || - k == ExprKind::LitTime) { + return compile_expr(f, *st.value, rd); + } + + /** + * @brief 赋值左值:帧寄存器直写或 STORE_* 到全局槽 + * @param f 当前函数 + * @param target 左值名(全局 / 外部) + * @param value 右值表达式 + * @return true 成功;false(err 已写) + */ + bool store_target(FuncCtx& f, const std::string& target, const Expr& value) { + const auto git = link_.global_index.find(target); + if (git == link_.global_index.end()) { + return fail("no storage for '" + target + "'"); + } + if (io_input_.count(target)) { + return fail("cannot write to input '" + target + "'"); + } + const uint8_t tmp = alloc_temp(f); + if (!compile_expr(f, value, tmp)) { + return false; + } + const uint16_t slot = global_offset(git->second); + f.code.push_back(isa::enc_slot(store_op(target), tmp, slot)); + return true; + } + + /** + * @brief 表达式编译到目标寄存器 + * @details 字面量 → LOADK;帧变量 → MOVE; + * 全局/外部 → LOAD_GLOBAL(io.input 绑定用 LOAD_I) + * @param f 当前函数 + * @param e 表达式 AST + * @param rd 目标寄存器 + * @return true 成功;false(err 已写) + */ + bool compile_expr(FuncCtx& f, const Expr& e, uint8_t rd) { + if (e.kind == ExprKind::LitBool || e.kind == ExprKind::LitInt || + e.kind == ExprKind::LitTime) { const isa::types::TypeTag tag = - k == ExprKind::LitBool ? isa::types::Bool - : k == ExprKind::LitInt ? isa::types::Int - : isa::types::Time; + e.kind == ExprKind::LitBool ? isa::types::Bool + : e.kind == ExprKind::LitInt ? isa::types::Int + : isa::types::Time; f.code.push_back( - isa::enc_imm(isa::Op::LOADK, rd, const_id(tag, st.value->int_value))); + isa::enc_imm(isa::Op::LOADK, rd, const_id(tag, e.int_value))); return true; } - if (k == ExprKind::VarRef) { - const auto sit = f.regs.find(st.value->name); - if (sit == f.regs.end()) { - return fail("no register for '" + st.value->name + "'"); + if (e.kind == ExprKind::VarRef) { + const auto sit = f.regs.find(e.name); + if (sit != f.regs.end()) { + f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, sit->second)); + return true; } - f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, sit->second)); - return true; + const auto git = link_.global_index.find(e.name); + if (git != link_.global_index.end()) { + const uint16_t slot = global_offset(git->second); + f.code.push_back(isa::enc_slot(load_op(e.name), rd, slot)); + return true; + } + return fail("no register or slot for '" + e.name + "'"); } - return fail("expression not supported in slice 1"); + return fail("expression not supported in slice 2"); + } + + /** + * @brief 按 io 绑定选读取操作码 + * @param name 变量名(小写) + * @return io.input 绑定 → LOAD_I;否则 LOAD_GLOBAL + */ + isa::Op load_op(const std::string& name) const { + return io_input_.count(name) ? isa::Op::LOAD_I : isa::Op::LOAD_GLOBAL; + } + + /** + * @brief 按 io 绑定选写入操作码 + * @param name 变量名(小写) + * @return io.output 绑定 → STORE_Q;否则 STORE_GLOBAL + */ + isa::Op store_op(const std::string& name) const { + return io_output_.count(name) ? isa::Op::STORE_Q : isa::Op::STORE_GLOBAL; + } + + /** + * @brief 逻辑槽号 → 数据区字节偏移(layout_data 先行) + * @param slot 全局逻辑槽号(符号表 address) + * @return 数据区字节偏移(u16 可容) + */ + uint16_t global_offset(uint32_t slot) const { + return static_cast(global_offsets_[slot]); + } + + /** + * @brief 布局全局数据区:对齐 + 初值字节 + * @details 槽序 = 声明序(12.6);对齐规则 BOOL 1B / INT 2B / TIME 8B, + * 起点对齐自身宽度;初值取自 GVL 声明的 has_init(无则 0) + * @return true 成功;false(槽溢出等,err 已写) + */ + bool layout_data() { + uint32_t cur = 0; + for (const Symbol& s : link_.globals) { + const uint32_t w = width_of(s.type_name); + cur = align_up(cur, w); + if (cur + w > 0xFFFF) { + return fail("data area exceeds slot range"); + } + global_offsets_.push_back(cur); + bool has_init = false; + int64_t init = 0; + init_of(s.name, &has_init, &init); + data_.resize(cur + w, 0); + if (s.type_name == "bool") { + data_[cur] = static_cast(has_init ? init : 0); + } else if (s.type_name == "int") { + const uint16_t v = static_cast(has_init ? init : 0); + data_[cur] = static_cast(v & 0xFFu); + data_[cur + 1] = static_cast((v >> 8) & 0xFFu); + } else if (s.type_name == "time") { + const uint64_t v = static_cast(has_init ? init : 0); + for (int i = 0; i < 8; ++i) { + data_[cur + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + } + cur += w; + } + return true; + } + + /** + * @brief 查全局声明的初值(AST;gvl 文件顶层段,声明序 = 槽号序) + * @param name 全局名(小写) + * @param has_init 输出是否有初值 + * @param init 输出初值 + */ + void init_of(const std::string& name, bool* has_init, int64_t* init) const { + for (const SourceUnit& u : units_) { + for (const VarBlock& b : u.ast.globals) { + for (const VarDecl& d : b.vars) { + if (d.name == name) { + *has_init = d.has_init; + *init = d.init_value; + return; + } + } + } + } + *has_init = false; + *init = 0; } /** @@ -215,7 +419,9 @@ namespace { } /** - * @brief 拼映像:头 + 常量表 + 函数表 + 字节码(数据段空) + * @brief 拼映像:头 + 常量表 + 函数表 + 字节码 + 数据段 + * @details 段序:const → funcs → code → fb(空)→ data; + * 数据段放全局初值(layout_data 已生成字节) * @return true 成功;false(err 已写) */ bool assemble_image() { @@ -228,7 +434,8 @@ namespace { for (const FuncCtx& f : funcs_) { code_total += static_cast(f.code.size()) * 4; } - const uint32_t off_end = off_code + code_total; + const uint32_t off_data = off_code + code_total; + const uint32_t off_end = off_data + static_cast(data_.size()); std::vector& b = *image_; b.assign(off_end, 0); @@ -250,7 +457,7 @@ namespace { } } put_le32(b, 24, entry); - put_le32(b, 28, 0); // n_globals(切片 1 无全局) + put_le32(b, 28, static_cast(link_.globals.size())); // n_globals put_le32(b, 32, 0); // n_i put_le32(b, 36, 0); // n_q put_le32(b, 40, 0); // n_m @@ -259,8 +466,8 @@ namespace { put_le32(b, 52, off_const); put_le32(b, 56, off_funcs); put_le32(b, 60, off_code); - put_le32(b, 64, off_end); // offset_fb(空) - put_le32(b, 68, off_end); // offset_data(空) + put_le32(b, 64, off_data); // offset_fb(空,与数据段起点相同) + put_le32(b, 68, off_data); // offset_data for (size_t i = 0; i < consts_.size(); ++i) { const size_t o = off_const + i * isa::kConstEntrySize; @@ -273,7 +480,6 @@ namespace { const FuncCtx& f = funcs_[i]; const size_t o = off_funcs + i * isa::kFuncRowSize; put_le32(b, o, f.nregs); - put_le32(b, o + 4, 0); // code_offset 相对段起点 = 前序函数长度和 put_le32(b, o + 8, static_cast(f.code.size())); for (const isa::Instr in : f.code) { put_le32(b, c, in); @@ -287,6 +493,11 @@ namespace { put_le32(b, o + 4, acc); acc += static_cast(funcs_[i].code.size()) * 4; } + + // 数据段:全局初值字节 + for (size_t i = 0; i < data_.size(); ++i) { + b[off_data + i] = data_[i]; + } return true; } @@ -298,6 +509,10 @@ namespace { std::string* err_; std::vector funcs_; std::vector consts_; + std::map io_input_; // io.input 绑定名(小写) + std::map io_output_; // io.output 绑定名(小写) + std::vector global_offsets_; // 逻辑槽号 → 数据区字节偏移 + std::vector data_; // 数据区(全局初值字节) }; } // namespace diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index a0f6751..820db67 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -58,6 +59,50 @@ static bool compile_case(const char* dir, std::vector* image, std::stri return codegen_project(p, units, link, image, err); } +// 从临时工程(globals.st + main.st + 可含 io 绑定)走完整管线 +static bool compile_src(const char* name, const char* io_extra, const char* globals_st, + const char* main_st, std::vector* image, + std::string* err) { + using namespace compiler; + const std::string dir = std::string(REPO_ROOT) + "/build/cg_tmp_" + name; + std::filesystem::remove_all(dir); + std::filesystem::create_directories(dir); + const std::string toml_path = dir + "/project.toml"; + std::FILE* f = std::fopen(toml_path.c_str(), "w"); + std::fprintf(f, "[project]\nname = \"cg\"\nentry = \"program MAIN\"\n" + "cycle_limit = 1000\ndt_ms = 10\n" + "[files]\nst = [\"globals.st\", \"main.st\"]\n" + "[gvl]\nfile = \"globals.st\"\n%s", io_extra); + std::fclose(f); + f = std::fopen((dir + "/globals.st").c_str(), "w"); + std::fputs(globals_st, f); + std::fclose(f); + f = std::fopen((dir + "/main.st").c_str(), "w"); + std::fputs(main_st, f); + std::fclose(f); + + Project p; + if (!parse_project(toml_path, &p, err)) { + return false; + } + std::vector units; + for (const std::string& file : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + file, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + LinkResult link; + if (!link_project(p, units, &link, err)) { + return false; + } + if (!check_project(p, units, link, err)) { + return false; + } + return codegen_project(p, units, link, image, err); +} + // ---- 1. 用例 01:空 MAIN + RET ---- static bool test_case01() { @@ -119,9 +164,128 @@ static bool test_case02() { return true; } +// ---- 3. 用例 09:GVL + VAR_EXTERNAL(全局读取 LOAD_GLOBAL + 初值数据段)---- + +static bool test_case09() { + std::vector img; + std::string err; + CHECK(compile_case("09_gvl_external", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_globals == 1); + CHECK(v.data_len() == 2); // INT 2 字节 + CHECK(v.data_bytes()[0] == 5); // G1 初值 5(小端低位) + CHECK(v.data_bytes()[1] == 0); + + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 1); // 仅局部 x + CHECK(r.code_len == 2); + + char buf[64]; + const uint8_t* base = v.code_bytes(); + isa::disasm(reinterpret_cast(base)[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r0, 0") == 0); // x := G1(槽 0 偏移 0) + isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + +// ---- 4. io 绑定:LOAD_I / STORE_Q ---- + +static bool test_io_ops() { + std::vector img; + std::string err; + const char* io_extra = + "[[io.input]]\nvar = \"I0_0\"\nchannel = 0\nbit = 0\n" + "[[io.output]]\nvar = \"Q0_0\"\nchannel = 0\nbit = 0\n"; + const char* globals_st = + "VAR_GLOBAL\n I0_0 : BOOL;\n Q0_0 : BOOL;\nEND_VAR\n"; + const char* main_st = + "PROGRAM MAIN\nVAR\n q : BOOL;\nEND_VAR\n" + " q := I0_0;\n" + " Q0_0 := q;\n" + "END_PROGRAM\n"; + CHECK(compile_src("io", io_extra, globals_st, main_st, &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_globals == 2); + + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 2); // q + 1 临时 + + char buf[64]; + const uint8_t* base = v.code_bytes(); + isa::disasm(reinterpret_cast(base)[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_I r0, 0") == 0); // q := I0_0(io.input → LOAD_I) + isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r1, r0") == 0); // 临时 ← q + isa::disasm(reinterpret_cast(base)[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_Q r1, 1") == 0); // Q0_0 := 临时(io.output → STORE_Q) + isa::disasm(reinterpret_cast(base)[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + +// ---- 5. 混合类型布局:BOOL@0 INT@2 TIME@8,初值入数据段 ---- + +static bool test_layout() { + std::vector img; + std::string err; + const char* globals_st = + "VAR_GLOBAL\n" + " B : BOOL;\n" + " I : INT := 300;\n" + " T : TIME := T#10ms;\n" + "END_VAR\n"; + const char* main_st = + "PROGRAM MAIN\nVAR\n x : INT;\nEND_VAR\n" + " x := I;\n" + "END_PROGRAM\n"; + CHECK(compile_src("layout", "", globals_st, main_st, &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_globals == 3); + CHECK(v.data_len() == 16); // 0 + 2 + 8(对齐)= 16 + CHECK(v.data_bytes()[0] == 0); // B 无初值 + CHECK(v.data_bytes()[2] == 0x2C && v.data_bytes()[3] == 0x01); // I = 300 + CHECK(v.data_bytes()[8] == 10); // T = 10ms + + char buf[64]; + isa::disasm(reinterpret_cast(v.code_bytes())[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r0, 2") == 0); // x := I(INT 偏移 2) + return true; +} + +// ---- 6. 写 io.input → codegen error ---- + +static bool test_write_input() { + std::vector img; + std::string err; + const char* io_extra = + "[[io.input]]\nvar = \"I0_0\"\nchannel = 0\nbit = 0\n"; + const char* globals_st = + "VAR_GLOBAL\n I0_0 : BOOL;\nEND_VAR\n"; + const char* main_st = + "PROGRAM MAIN\n I0_0 := TRUE;\nEND_PROGRAM\n"; + if (compile_src("winput", io_extra, globals_st, main_st, &img, &err)) { + std::printf("FAIL write_input: codegen passed\n"); + return false; + } + CHECK(err.find("codegen error") == 0); + CHECK(err.find("cannot write to input") != std::string::npos); + return true; +} + int main() { if (!test_case01()) return 1; if (!test_case02()) return 1; + if (!test_case09()) return 1; + if (!test_io_ops()) return 1; + if (!test_layout()) return 1; + if (!test_write_input()) return 1; std::printf("codegen_test: %d checks passed\n", g_checks); return 0; } From e7dee50cde56a7bb28093db5ae7f629c7cd3c674 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:40:53 +0800 Subject: [PATCH 28/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=203=EF=BC=9ANOT?= =?UTF-8?q?=20=E4=B8=8E=E7=9F=AD=E8=B7=AF=20AND/OR=EF=BC=88=E7=BC=96?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 短路模式: JF(AND)/JT(OR) rd, L_end MOVE rd, t L_end: - patch_jump 回填偏移;约定相对下一条指令(目标 = 当前 + 1 + off),冻结进 isa 规格 - codegen_test:71 断言(用例 03 跳转序列 JF/JT +2、NOT 序列),ctest 9/9 --- Doc/isa/指令与映像.md | 2 +- compiler/src/Codegen.cpp | 71 +++++++++++++++++++++++++++++--------- tests/src/codegen_test.cpp | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 17 deletions(-) diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 7d2c0fb..2256c8a 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -82,7 +82,7 @@ C++20 定宽: - 三寄存器:`a`/`b` 为 `ra`/`rb`(`MOVE`/`NOT` 只用 `rd`+`a`) - `LOADK`:`a|b` 为 `const_id`(u16) -- `JMP` / `JT` / `JF`:`a|b` 为有符号相对 offset(指令条数) +- `JMP` / `JT` / `JF`:`a|b` 为有符号相对 offset(指令条数),**相对下一条指令**:目标下标 = 当前下标 + 1 + off - `CALL`:`a|b` 为 `fn_id`(u16) 操作码一次列全(`CMP_xx` 冻下面六种): diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index da4f0c1..c37302e 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,33 +1,33 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 2:+ 全局 / I/Q/M) + * @brief 寄存器码生成(12.8,切片 3:+ NOT、短路 AND/OR) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1:PROGRAM 标量变量帧(r0 起按声明序)、字面量 LOADK、 - * 变量间 MOVE、RET;空 MAIN 出映像。 - * - 切片 2:+ 全局数据区(声明序、对齐 BOOL 1B / INT 2B / TIME 8B、 - * 初值入数据段);读全局 LOAD_GLOBAL / io.input 绑定用 LOAD_I; - * 写全局 STORE_GLOBAL / io.output 绑定用 STORE_Q;写 io.input → 拒绝。 - * 其余构造报 codegen error(后续切片展开) - * - 常量统一进常量表(BOOL/INT/TIME),const_id = 首次出现序 - * - 指令 slot = 数据区字节偏移;符号表 address 保持逻辑槽号(12.6 冻结) + * - 切片 1:PROGRAM 标量变量帧、字面量 LOADK、变量 MOVE、RET。 + * - 切片 2:全局数据区(对齐 + 初值)、LOAD_I/STORE_Q/LOAD_GLOBAL/STORE_GLOBAL。 + * - 切片 3:NOT(enc_rr);AND/OR **短路编跳转**(禁止两边都算): + * JF(AND)/JT(OR) rd, L_end MOVE rd, t L_end: + * 跳转偏移相对下一条指令(目标 = 当前 + 1 + off,见 Doc/isa/指令与映像.md) + * - 其余构造报 codegen error(后续切片展开) * * 函数清单: * - put_le32 / put_le64 小端写入映像缓冲 * - align_up 按 2 的幂宽度向上对齐 * - width_of 类型名 → 槽宽(bool 1 / int 2 / time 8) * - Builder::Builder (构造)存工程/源文件/链接结果/输出,收集 io 绑定分类 - * - Builder::run 逐 POU 建函数 → 布局数据区 → 拼映像 + * - Builder::run 布局数据区 → 逐 POU 建函数 → 拼映像 * - Builder::fail 组装 "codegen error: " 返回 false * - find_pou 按名查 POU AST * - build_function 编译一个 POU 的语句体(帧寄存器分配) - * - compile_stmt 语句编译(切片 2:仅赋值,左值可帧寄存器或全局) - * - compile_expr 表达式编译到寄存器(字面量 / 变量读,含全局 LOAD_*) - * - store_target 赋值左值:帧寄存器直写或 STORE_* 到全局槽 + * - begin_stmt / alloc_temp 临时寄存器:语句内递增、语句结束复用基址 + * - compile_stmt 语句编译(切片 3:仅赋值,左值可帧寄存器或全局) + * - store_target 赋值左值:STORE_* 到全局槽 + * - compile_expr 表达式编译到寄存器(字面量/变量读/NOT/短路 AND/OR) * - load_op / store_op 按 io 绑定选读取/写入操作码 * - global_offset 逻辑槽号 → 数据区字节偏移 + * - patch_jump 回填跳转偏移(相对下一条指令) * - layout_data 布局全局数据区(对齐 + 初值字节) * - init_of 查全局声明的初值(AST) * - const_id 取常量表 id(无则追加) @@ -282,8 +282,8 @@ namespace { /** * @brief 表达式编译到目标寄存器 - * @details 字面量 → LOADK;帧变量 → MOVE; - * 全局/外部 → LOAD_GLOBAL(io.input 绑定用 LOAD_I) + * @details 字面量 → LOADK;帧变量 → MOVE;全局/外部 → LOAD_*(io.input 用 LOAD_I); + * NOT → 求值后 NOT rd, t;AND/OR → 短路跳转(JF/JT 跳过右侧) * @param f 当前函数 * @param e 表达式 AST * @param rd 目标寄存器 @@ -314,7 +314,46 @@ namespace { } return fail("no register or slot for '" + e.name + "'"); } - return fail("expression not supported in slice 2"); + if (e.kind == ExprKind::Not) { + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *e.operand, t)) { + return false; + } + f.code.push_back(isa::enc_rr(isa::Op::NOT, rd, t)); + return true; + } + if (e.kind == ExprKind::And || e.kind == ExprKind::Or) { + // 短路:左侧结果在 rd;AND 为假 / OR 为真时跳过右侧 + if (!compile_expr(f, *e.lhs, rd)) { + return false; + } + const isa::Op jop = (e.kind == ExprKind::And) ? isa::Op::JF : isa::Op::JT; + const size_t jmp_idx = f.code.size(); + f.code.push_back(isa::enc_jc(jop, rd, 0)); // 偏移留洞,稍后回填 + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *e.rhs, t)) { + return false; + } + f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, t)); + patch_jump(f, jmp_idx, f.code.size()); + return true; + } + return fail("expression not supported in slice 3"); + } + + /** + * @brief 回填跳转偏移(相对下一条指令) + * @param f 当前函数 + * @param idx 跳转指令下标(占位) + * @param target_idx 目标指令下标 + */ + void patch_jump(FuncCtx& f, size_t idx, size_t target_idx) { + const int16_t off = static_cast( + static_cast(target_idx) - (static_cast(idx) + 1)); + const isa::Instr w = f.code[idx]; + f.code[idx] = isa::pack(isa::op(w), isa::rd(w), + static_cast(static_cast(off) & 0xFFu), + static_cast((static_cast(off) >> 8) & 0xFFu)); } /** diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 820db67..464a232 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -279,6 +279,74 @@ static bool test_write_input() { return true; } +// ---- 7. 用例 03:短路 AND / OR 必须编跳转 ---- + +static bool test_case03() { + std::vector img; + std::string err; + CHECK(compile_case("03_short_circuit", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 4); // a b x + 1 临时 + CHECK(r.code_len == 9); + + char buf[64]; + const uint8_t* base = v.code_bytes(); + const uint32_t* ins = reinterpret_cast(base); + // x := a AND b:MOVE r2,r0 / JF r2,+2 / MOVE r3,r1 / MOVE r2,r3 + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r2, +2") == 0); + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r3, r1") == 0); + isa::disasm(ins[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r3") == 0); + // x := a OR b:MOVE r2,r0 / JT r2,+2 / MOVE r3,r1 / MOVE r2,r3 + isa::disasm(ins[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); + isa::disasm(ins[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "JT r2, +2") == 0); + isa::disasm(ins[6], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r3, r1") == 0); + isa::disasm(ins[7], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r3") == 0); + isa::disasm(ins[8], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + +// ---- 8. NOT ---- + +static bool test_not() { + std::vector img; + std::string err; + const char* main_st = + "PROGRAM MAIN\nVAR\n a, x : BOOL;\nEND_VAR\n" + " x := NOT a;\n" + "END_PROGRAM\n"; + CHECK(compile_src("not", "", "", main_st, &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 3); // a x + 1 临时 + CHECK(r.code_len == 3); + + char buf[64]; + const uint8_t* base = v.code_bytes(); + const uint32_t* ins = reinterpret_cast(base); + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); // 临时 ← a + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "NOT r1, r2") == 0); // x := NOT 临时 + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + int main() { if (!test_case01()) return 1; if (!test_case02()) return 1; @@ -286,6 +354,8 @@ int main() { if (!test_io_ops()) return 1; if (!test_layout()) return 1; if (!test_write_input()) return 1; + if (!test_case03()) return 1; + if (!test_not()) return 1; std::printf("codegen_test: %d checks passed\n", g_checks); return 0; } From 6f1bf61c7f9d691bd95538116ffc8de64eea7b4a Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:43:23 +0800 Subject: [PATCH 29/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=204=EF=BC=9ACMP?= =?UTF-8?q?=5F*=20=E4=B8=8E=20IF/ELSIF/ELSE=20=E8=B7=B3=E8=BD=AC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CMP_xx:两侧求值到临时后比较;IF 编成 JF t, L_next JMP L_end 链 - 分支尾部 JMP 与条件 JF 双回填;无 else 时末分支 JF 直接落 L_end - codegen_test:89 断言(用例 04 完整跳转序列),ctest 9/9 --- compiler/src/Codegen.cpp | 110 ++++++++++++++++++++++++++++++++----- tests/src/codegen_test.cpp | 51 +++++++++++++++++ 2 files changed, 148 insertions(+), 13 deletions(-) diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index c37302e..7395d79 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,35 +1,34 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 3:+ NOT、短路 AND/OR) + * @brief 寄存器码生成(12.8,切片 4:+ CMP_*、IF/ELSIF/ELSE) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1:PROGRAM 标量变量帧、字面量 LOADK、变量 MOVE、RET。 - * - 切片 2:全局数据区(对齐 + 初值)、LOAD_I/STORE_Q/LOAD_GLOBAL/STORE_GLOBAL。 - * - 切片 3:NOT(enc_rr);AND/OR **短路编跳转**(禁止两边都算): - * JF(AND)/JT(OR) rd, L_end MOVE rd, t L_end: + * - 切片 1~3:帧/字面量/MOVE/RET;全局数据区;NOT 与短路 AND/OR。 + * - 切片 4:比较 CMP_xx(两侧求值到临时);IF 编成 + * JF t, L_next JMP L_end L_next: ... L_end: * 跳转偏移相对下一条指令(目标 = 当前 + 1 + off,见 Doc/isa/指令与映像.md) * - 其余构造报 codegen error(后续切片展开) * * 函数清单: * - put_le32 / put_le64 小端写入映像缓冲 - * - align_up 按 2 的幂宽度向上对齐 - * - width_of 类型名 → 槽宽(bool 1 / int 2 / time 8) + * - align_up / width_of 对齐与槽宽 * - Builder::Builder (构造)存工程/源文件/链接结果/输出,收集 io 绑定分类 * - Builder::run 布局数据区 → 逐 POU 建函数 → 拼映像 * - Builder::fail 组装 "codegen error: " 返回 false * - find_pou 按名查 POU AST * - build_function 编译一个 POU 的语句体(帧寄存器分配) * - begin_stmt / alloc_temp 临时寄存器:语句内递增、语句结束复用基址 - * - compile_stmt 语句编译(切片 3:仅赋值,左值可帧寄存器或全局) + * - compile_stmt 语句编译(切片 4:赋值 / IF) + * - compile_if IF/ELSIF/ELSE:条件链 + 分支尾部 JMP,JF/JMP 回填 * - store_target 赋值左值:STORE_* 到全局槽 - * - compile_expr 表达式编译到寄存器(字面量/变量读/NOT/短路 AND/OR) + * - compile_expr 表达式编译到寄存器(含 CMP_xx / NOT / 短路 AND/OR) + * - cmp_op 比较符 → CMP_xx 操作码 * - load_op / store_op 按 io 绑定选读取/写入操作码 * - global_offset 逻辑槽号 → 数据区字节偏移 * - patch_jump 回填跳转偏移(相对下一条指令) - * - layout_data 布局全局数据区(对齐 + 初值字节) - * - init_of 查全局声明的初值(AST) + * - layout_data / init_of 全局数据区布局与初值 * - const_id 取常量表 id(无则追加) * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + 数据段 * - codegen_project 对外入口 @@ -238,14 +237,17 @@ namespace { } /** - * @brief 编译一条语句(切片 2:仅赋值,左值可为帧寄存器或全局) + * @brief 编译一条语句(切片 4:赋值 / IF) * @param f 当前函数 * @param st 语句 AST * @return true 成功;false(err 已写) */ bool compile_stmt(FuncCtx& f, const Stmt& st) { + if (st.kind == StmtKind::If) { + return compile_if(f, st); + } if (st.kind != StmtKind::Assign) { - return fail("statement not supported in slice 2"); + return fail("statement not supported in slice 4"); } const auto it = f.regs.find(st.target); if (it == f.regs.end()) { @@ -256,6 +258,59 @@ namespace { return compile_expr(f, *st.value, rd); } + /** + * @brief 编译 IF / ELSIF / ELSE + * @details 每分支: JF t, L_next → → JMP L_end; + * 最后一个分支无 else 时 JF 直接落 L_end(不补 JMP) + * @param f 当前函数 + * @param st IF 语句 AST + * @return true 成功;false(err 已写) + */ + bool compile_if(FuncCtx& f, const Stmt& st) { + // 条件链:IF 体 + ELSIF 各体;ELSE 体可选 + std::vector*>> branches; + branches.push_back({st.cond.get(), &st.body}); + for (const IfBranch& b : st.elsifs) { + branches.push_back({b.cond.get(), &b.body}); + } + const bool has_else = !st.else_body.empty(); + std::vector end_jmps; // 各分支尾部 JMP(回填到 L_end) + + for (size_t i = 0; i < branches.size(); ++i) { + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *branches[i].first, t)) { + return false; + } + const size_t jf_idx = f.code.size(); + f.code.push_back(isa::enc_jc(isa::Op::JF, t, 0)); // 假 → 下一分支 + for (const Stmt& s : *branches[i].second) { + begin_stmt(f); + if (!compile_stmt(f, s)) { + return false; + } + } + if (i + 1 < branches.size() || has_else) { + const size_t jmp_idx = f.code.size(); + f.code.push_back(isa::enc_jmp(0)); // 分支尾部 → L_end + end_jmps.push_back(jmp_idx); + } + patch_jump(f, jf_idx, f.code.size()); // 回填 JF 到下一分支起点 + } + if (has_else) { + for (const Stmt& s : st.else_body) { + begin_stmt(f); + if (!compile_stmt(f, s)) { + return false; + } + } + } + const size_t end = f.code.size(); + for (const size_t j : end_jmps) { + patch_jump(f, j, end); + } + return true; + } + /** * @brief 赋值左值:帧寄存器直写或 STORE_* 到全局槽 * @param f 当前函数 @@ -314,6 +369,18 @@ namespace { } return fail("no register or slot for '" + e.name + "'"); } + if (e.kind == ExprKind::Cmp) { + const uint8_t l = alloc_temp(f); + if (!compile_expr(f, *e.lhs, l)) { + return false; + } + const uint8_t r = alloc_temp(f); + if (!compile_expr(f, *e.rhs, r)) { + return false; + } + f.code.push_back(isa::enc_rrr(cmp_op(e.op), rd, l, r)); + return true; + } if (e.kind == ExprKind::Not) { const uint8_t t = alloc_temp(f); if (!compile_expr(f, *e.operand, t)) { @@ -356,6 +423,23 @@ namespace { static_cast((static_cast(off) >> 8) & 0xFFu)); } + /** + * @brief 比较符 → CMP_xx 操作码 + * @param op 比较运算 + * @return 对应操作码(CMP_EQ / CMP_NE / CMP_LT / CMP_LE / CMP_GT / CMP_GE) + */ + isa::Op cmp_op(BinOp op) const { + switch (op) { + case BinOp::Eq: return isa::Op::CMP_EQ; + case BinOp::Ne: return isa::Op::CMP_NE; + case BinOp::Lt: return isa::Op::CMP_LT; + case BinOp::Le: return isa::Op::CMP_LE; + case BinOp::Gt: return isa::Op::CMP_GT; + case BinOp::Ge: return isa::Op::CMP_GE; + } + return isa::Op::CMP_EQ; + } + /** * @brief 按 io 绑定选读取操作码 * @param name 变量名(小写) diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 464a232..c1022bd 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -347,6 +347,56 @@ static bool test_not() { return true; } +// ---- 9. 用例 04:IF / ELSIF / ELSE(CMP + JF + JMP 回填)---- + +static bool test_case04() { + std::vector img; + std::string err; + CHECK(compile_case("04_if_elsif_else", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 5); // sel out + 3 临时(CMP 目的 + 两操作数) + CHECK(r.code_len == 14); + + char buf[64]; + const uint8_t* base = v.code_bytes(); + const uint32_t* ins = reinterpret_cast(base); + // IF sel = 0 THEN:sel→r3、0→r4、CMP_EQ r2(常量序:0、10、1、20、30) + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r3, r0") == 0); + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r4, 0") == 0); + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "CMP_EQ r2, r3, r4") == 0); + isa::disasm(ins[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r2, +2") == 0); // 假 → elsif([6]) + isa::disasm(ins[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r1, 1") == 0); // out := 10(常量 1) + isa::disasm(ins[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "JMP +7") == 0); // → end([13]) + // ELSIF sel = 1 + isa::disasm(ins[6], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r3, r0") == 0); + isa::disasm(ins[7], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r4, 2") == 0); + isa::disasm(ins[8], buf, sizeof buf); + CHECK(std::strcmp(buf, "CMP_EQ r2, r3, r4") == 0); + isa::disasm(ins[9], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r2, +2") == 0); // 假 → else([12]) + isa::disasm(ins[10], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r1, 3") == 0); + isa::disasm(ins[11], buf, sizeof buf); + CHECK(std::strcmp(buf, "JMP +1") == 0); // → end([13]) + // ELSE + isa::disasm(ins[12], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r1, 4") == 0); + isa::disasm(ins[13], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + int main() { if (!test_case01()) return 1; if (!test_case02()) return 1; @@ -356,6 +406,7 @@ int main() { if (!test_write_input()) return 1; if (!test_case03()) return 1; if (!test_not()) return 1; + if (!test_case04()) return 1; std::printf("codegen_test: %d checks passed\n", g_checks); return 0; } From 31ba557627e8314151ffd1c933c6b5b320b10d91 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:45:32 +0800 Subject: [PATCH 30/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=205=EF=BC=9AWHI?= =?UTF-8?q?LE=20=E5=9B=9E=E7=8E=AF=E4=B8=8E=E5=9B=9B=E5=88=99=E8=BF=90?= =?UTF-8?q?=E7=AE=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WHILE 编成 L_loop: JF t, L_end JMP L_loop L_end:,JF/JMP 双回填 - 四则 ADD/SUB/MUL/DIV(用例 5 循环体自增需要,同模式实现,用例 7 顺带点亮) - codegen_test:115 断言(用例 05 回环 JMP -8、07 四则+比较、08 TIME 常量表),ctest 9/9 --- compiler/src/Codegen.cpp | 80 +++++++++++++++++++++++++++---- tests/src/codegen_test.cpp | 97 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 9 deletions(-) diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 7395d79..796e036 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,14 +1,15 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 4:+ CMP_*、IF/ELSIF/ELSE) + * @brief 寄存器码生成(12.8,切片 5:+ WHILE、四则运算) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1~3:帧/字面量/MOVE/RET;全局数据区;NOT 与短路 AND/OR。 - * - 切片 4:比较 CMP_xx(两侧求值到临时);IF 编成 - * JF t, L_next JMP L_end L_next: ... L_end: - * 跳转偏移相对下一条指令(目标 = 当前 + 1 + off,见 Doc/isa/指令与映像.md) + * - 切片 1~4:帧/字面量/MOVE/RET;全局数据区;NOT 与短路 AND/OR;CMP 与 IF。 + * - 切片 5:WHILE 编成 + * L_loop: JF t, L_end JMP L_loop L_end: + * (用例 5 循环体含自增,四则 ADD/SUB/MUL/DIV 同模式一并实现,用例 7 可测) + * - 跳转偏移相对下一条指令(目标 = 当前 + 1 + off,见 Doc/isa/指令与映像.md) * - 其余构造报 codegen error(后续切片展开) * * 函数清单: @@ -20,11 +21,12 @@ * - find_pou 按名查 POU AST * - build_function 编译一个 POU 的语句体(帧寄存器分配) * - begin_stmt / alloc_temp 临时寄存器:语句内递增、语句结束复用基址 - * - compile_stmt 语句编译(切片 4:赋值 / IF) + * - compile_stmt 语句编译(切片 5:赋值 / IF / WHILE) * - compile_if IF/ELSIF/ELSE:条件链 + 分支尾部 JMP,JF/JMP 回填 + * - compile_while WHILE:L_loop + 条件 JF + 体 + JMP 回环 * - store_target 赋值左值:STORE_* 到全局槽 - * - compile_expr 表达式编译到寄存器(含 CMP_xx / NOT / 短路 AND/OR) - * - cmp_op 比较符 → CMP_xx 操作码 + * - compile_expr 表达式编译到寄存器(含 CMP/算术/NOT/短路 AND/OR) + * - cmp_op / arith_op 比较符/算术符 → 操作码 * - load_op / store_op 按 io 绑定选读取/写入操作码 * - global_offset 逻辑槽号 → 数据区字节偏移 * - patch_jump 回填跳转偏移(相对下一条指令) @@ -246,8 +248,11 @@ namespace { if (st.kind == StmtKind::If) { return compile_if(f, st); } + if (st.kind == StmtKind::While) { + return compile_while(f, st); + } if (st.kind != StmtKind::Assign) { - return fail("statement not supported in slice 4"); + return fail("statement not supported in slice 5"); } const auto it = f.regs.find(st.target); if (it == f.regs.end()) { @@ -258,6 +263,35 @@ namespace { return compile_expr(f, *st.value, rd); } + /** + * @brief 编译 WHILE + * @details L_loop: JF t, L_end JMP L_loop L_end: + * 条件 JF 与回环 JMP 双回填 + * @param f 当前函数 + * @param st WHILE 语句 AST + * @return true 成功;false(err 已写) + */ + bool compile_while(FuncCtx& f, const Stmt& st) { + const size_t loop = f.code.size(); + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *st.cond, t)) { + return false; + } + const size_t jf_idx = f.code.size(); + f.code.push_back(isa::enc_jc(isa::Op::JF, t, 0)); // 假 → L_end + for (const Stmt& s : st.body) { + begin_stmt(f); + if (!compile_stmt(f, s)) { + return false; + } + } + const size_t jmp_idx = f.code.size(); + f.code.push_back(isa::enc_jmp(0)); // 回环 + patch_jump(f, jmp_idx, loop); + patch_jump(f, jf_idx, f.code.size()); + return true; + } + /** * @brief 编译 IF / ELSIF / ELSE * @details 每分支: JF t, L_next → → JMP L_end; @@ -369,6 +403,19 @@ namespace { } return fail("no register or slot for '" + e.name + "'"); } + if (e.kind == ExprKind::Add || e.kind == ExprKind::Sub || + e.kind == ExprKind::Mul || e.kind == ExprKind::Div) { + const uint8_t l = alloc_temp(f); + if (!compile_expr(f, *e.lhs, l)) { + return false; + } + const uint8_t r = alloc_temp(f); + if (!compile_expr(f, *e.rhs, r)) { + return false; + } + f.code.push_back(isa::enc_rrr(arith_op(e.kind), rd, l, r)); + return true; + } if (e.kind == ExprKind::Cmp) { const uint8_t l = alloc_temp(f); if (!compile_expr(f, *e.lhs, l)) { @@ -423,6 +470,21 @@ namespace { static_cast((static_cast(off) >> 8) & 0xFFu)); } + /** + * @brief 算术运算 → 操作码 + * @param k 表达式种类(Add/Sub/Mul/Div) + * @return 对应操作码 + */ + isa::Op arith_op(ExprKind k) const { + switch (k) { + case ExprKind::Add: return isa::Op::ADD; + case ExprKind::Sub: return isa::Op::SUB; + case ExprKind::Mul: return isa::Op::MUL; + case ExprKind::Div: return isa::Op::DIV; + default: return isa::Op::ADD; + } + } + /** * @brief 比较符 → CMP_xx 操作码 * @param op 比较运算 diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index c1022bd..8dac32a 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -397,6 +397,100 @@ static bool test_case04() { return true; } +// ---- 10. 用例 05:WHILE 回环(JF 出口 + JMP 回填)---- + +static bool test_case05() { + std::vector img; + std::string err; + CHECK(compile_case("05_while_normal", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 4); // n + 3 临时(CMP 目的 + 两操作数) + CHECK(r.code_len == 10); + + char buf[64]; + const uint8_t* base = v.code_bytes(); + const uint32_t* ins = reinterpret_cast(base); + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r0, 0") == 0); // n := 0 + // L_loop [1]:n < 10 → r1 + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r3, 1") == 0); // 常量 1 = 10 + isa::disasm(ins[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "CMP_LT r1, r2, r3") == 0); + isa::disasm(ins[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r1, +4") == 0); // 假 → RET([9]) + // n := n + 1(体语句临时复用 r1/r2) + isa::disasm(ins[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r1, r0") == 0); + isa::disasm(ins[6], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r2, 2") == 0); // 常量 2 = 1 + isa::disasm(ins[7], buf, sizeof buf); + CHECK(std::strcmp(buf, "ADD r0, r1, r2") == 0); + isa::disasm(ins[8], buf, sizeof buf); + CHECK(std::strcmp(buf, "JMP -8") == 0); // 回 L_loop([1]) + isa::disasm(ins[9], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + +// ---- 11. 用例 07:四则 + 比较 ---- + +static bool test_case07() { + std::vector img; + std::string err; + CHECK(compile_case("07_int_arith", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 6); // a b c eq + 2 临时(语句间复用) + + char buf[64]; + const uint8_t* base = v.code_bytes(); + const uint32_t* ins = reinterpret_cast(base); + bool saw_mul = false, saw_div = false, saw_sub = false, saw_gt = false; + for (uint32_t i = 0; i < r.code_len; ++i) { + isa::disasm(ins[i], buf, sizeof buf); + if (std::strstr(buf, "MUL r0, ")) saw_mul = true; + if (std::strstr(buf, "DIV r1, ")) saw_div = true; + if (std::strstr(buf, "SUB r2, ")) saw_sub = true; + if (std::strstr(buf, "CMP_GT r3, ")) saw_gt = true; + } + CHECK(saw_mul && saw_div && saw_sub && saw_gt); + return true; +} + +// ---- 12. 用例 08:TIME 字面量进常量表 ---- + +static bool test_case08() { + std::vector img; + std::string err; + CHECK(compile_case("08_time_literal", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_consts == 2); + const isa::ConstEntry c0 = v.const_entry(0); + CHECK(c0.tag == isa::types::Time && c0.value == 10); // T#10ms + const isa::ConstEntry c1 = v.const_entry(1); + CHECK(c1.tag == isa::types::Time && c1.value == 1250); // T#1s250ms + + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 2); + char buf[64]; + const uint32_t* ins = reinterpret_cast(v.code_bytes()); + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r0, 0") == 0); + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r1, 1") == 0); + return true; +} + int main() { if (!test_case01()) return 1; if (!test_case02()) return 1; @@ -407,6 +501,9 @@ int main() { if (!test_case03()) return 1; if (!test_not()) return 1; if (!test_case04()) return 1; + if (!test_case05()) return 1; + if (!test_case07()) return 1; + if (!test_case08()) return 1; std::printf("codegen_test: %d checks passed\n", g_checks); return 0; } From 0a36e36b16fd3bdcdd0b34281840ababc52415c9 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:48:33 +0800 Subject: [PATCH 31/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=206=EF=BC=9AFUN?= =?UTF-8?q?CTION=20=E4=B8=8E=20CALL=EF=BC=8C=E5=86=BB=E7=BB=93=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E7=BA=A6=E5=AE=9A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调用约定(冻结进 isa 规格与 12.8 文档):结果 r0、参数 r1..r7(最多 7 输入、函数内只读)、 变量与临时全部从 r8 起;CALL/RET 时 VM 复制 r0..r7(12.9 实现) - 调用点:实参求值到 r8+ 临时 → MOVE r(1+i) → CALL fn_id → MOVE rd, r0 - 函数名赋值 = 写 r0(结果);写函数输入 → codegen error - codegen_test:134 断言(用例 13 两函数 CALL 序列;既有用例帧基址迁移 r8 起),ctest 9/9 --- Doc/compiler/寄存器码.md | 14 ++++ Doc/isa/指令与映像.md | 8 ++ compiler/src/Codegen.cpp | 104 +++++++++++++++++++----- tests/src/codegen_test.cpp | 159 ++++++++++++++++++++++++------------- 4 files changed, 213 insertions(+), 72 deletions(-) diff --git a/Doc/compiler/寄存器码.md b/Doc/compiler/寄存器码.md index 7e9cb80..a50d4ed 100644 --- a/Doc/compiler/寄存器码.md +++ b/Doc/compiler/寄存器码.md @@ -30,6 +30,20 @@ compiler 模块的代码生成。输入:`Project` + `Unit`(AST)+ `LinkResu - 字段访问 `fb.Q`:`LOAD_GLOBAL rd, <实例基址+字段偏移>`(编译期算死);I/Q/M 用 `LOAD_I` / `STORE_Q` / `LOAD_M` / `STORE_M` 同槽号 - 函数帧寄存器:`FUNCTION` 结果固定 **r0**,`VAR_INPUT` 从 r1 起按声明序,`VAR` 续后;PROGRAM / FB 变量从 r0 起按声明序 +### 调用约定(v1 冻结,VM 12.9 按此实现) + +| 项 | 约定 | +|---|---| +| 结果寄存器 | **r0**(FUNCTION 结果写入处) | +| 参数寄存器 | **r1..r7**(最多 7 个输入,按声明序) | +| 变量 / 临时 | **全部从 r8 起**(调用约定区不受覆盖) | +| `CALL` | VM 复制当前帧 **r0..r7 → 新帧 r0..r7** | +| `RET` | VM 复制当前帧 **r0..r7 → 调用方帧 r0..r7**(r0=结果,r1..r7 原样返回) | +| 函数输入 | 函数体内**只读**(写输入 → codegen error) | + +- 调用点:实参求值到 r8+ 临时 → `MOVE r(1+i), t` → `CALL fn_id` → `MOVE rd, r0` +- 所有 POU 的变量/临时统一 r8 起,故 r1..r7 在调用点可安全覆盖 + ## 寄存器分配 - 局部变量固定占用前段(0..k-1) diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 2256c8a..082d43f 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -47,6 +47,14 @@ CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/` | 寄存器 | 下标 0..255;每函数实际个数写在函数头 `nregs` | | 跳转 | 相对 offset,单位是**指令条数**,不是字节 | | `CALL` | 立即数 `fn_id`(u16),无函数指针 | + +### 调用约定(冻结,compiler 12.8 与 VM 12.9 共同遵守) + +- 结果寄存器 **r0**;参数寄存器 **r1..r7**(最多 7 个输入,按声明序) +- 变量与表达式临时**全部从 r8 起**(调用约定区 r0..r7 不受用户数据占用) +- `CALL`:VM 复制当前帧 r0..r7 到新帧 r0..r7 +- `RET`:VM 复制当前帧 r0..r7 回调用方帧 r0..r7(r0 = 结果;r1..r7 原样返回) +- 函数输入在函数体内只读(compiler 拒绝写入) | 映像魔数 | `STSC`,版本 `1` | | 工程哈希 | 源文件路径排序后对内容做 FNV-1a 64(非密码学) | | I/O 绑定 | **不进映像**;编译期把 `var` 收成槽号,channel/bit 由 STCompiler 写进 sidecar(`.runtime.toml`),BytecodeExecutor 读 sidecar 采样 | diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 796e036..8aed05f 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,15 +1,15 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 5:+ WHILE、四则运算) + * @brief 寄存器码生成(12.8,切片 6:+ FUNCTION 与 CALL) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1~4:帧/字面量/MOVE/RET;全局数据区;NOT 与短路 AND/OR;CMP 与 IF。 - * - 切片 5:WHILE 编成 - * L_loop: JF t, L_end JMP L_loop L_end: - * (用例 5 循环体含自增,四则 ADD/SUB/MUL/DIV 同模式一并实现,用例 7 可测) - * - 跳转偏移相对下一条指令(目标 = 当前 + 1 + off,见 Doc/isa/指令与映像.md) + * - 切片 1~5:帧/字面量/MOVE/RET;全局数据区;NOT 与短路 AND/OR;CMP 与 IF;WHILE 与四则。 + * - 切片 6:调用约定(冻结)——结果 r0、参数 r1..r7(最多 7 输入、函数内只读)、 + * 变量与临时全部从 r8 起(调用约定区不受覆盖); + * CALL 时 VM 复制 r0..r7 到新帧、RET 复制回调用方(r0=结果); + * 调用点:实参求值到 r8+ 临时 → MOVE r(1+i) → CALL fn_id → MOVE rd, r0 * - 其余构造报 codegen error(后续切片展开) * * 函数清单: @@ -19,13 +19,14 @@ * - Builder::run 布局数据区 → 逐 POU 建函数 → 拼映像 * - Builder::fail 组装 "codegen error: " 返回 false * - find_pou 按名查 POU AST - * - build_function 编译一个 POU 的语句体(帧寄存器分配) + * - find_fn_id 按名查 fn_id(POU 收集序) + * - build_function 编译一个 POU(PROGRAM/FUNCTION;帧约定分配) * - begin_stmt / alloc_temp 临时寄存器:语句内递增、语句结束复用基址 - * - compile_stmt 语句编译(切片 5:赋值 / IF / WHILE) + * - compile_stmt 语句编译(赋值 / IF / WHILE;函数输入只读) * - compile_if IF/ELSIF/ELSE:条件链 + 分支尾部 JMP,JF/JMP 回填 * - compile_while WHILE:L_loop + 条件 JF + 体 + JMP 回环 * - store_target 赋值左值:STORE_* 到全局槽 - * - compile_expr 表达式编译到寄存器(含 CMP/算术/NOT/短路 AND/OR) + * - compile_expr 表达式编译到寄存器(含 CMP/算术/NOT/短路/CALL) * - cmp_op / arith_op 比较符/算术符 → 操作码 * - load_op / store_op 按 io 绑定选读取/写入操作码 * - global_offset 逻辑槽号 → 数据区字节偏移 @@ -176,9 +177,11 @@ namespace { std::string name; std::vector code; // 字节码(函数表 code_offset 相对此段) std::map regs; // 变量名 → 帧寄存器 - uint8_t nlocals = 0; // 变量数 = 临时寄存器起始 + uint8_t nlocals = 0; // 变量区终点 = 临时寄存器起始 uint8_t nregs = 0; // 峰值(变量 + 临时) uint8_t temp_used = 0; // 本语句已用临时数(语句结束清零) + bool is_function = false; // FUNCTION(结果 r0 / 输入只读) + std::string result_name; // FUNCTION 名(结果寄存器映射) }; /** @@ -203,31 +206,53 @@ namespace { /** * @brief 编译一个 POU - * @details 切片 1:仅 PROGRAM + 标量变量(FB 实例/函数留后续切片) + * @details 切片 6:支持 PROGRAM 与 FUNCTION(FB 留切片 8) + * 帧约定(见 Doc/compiler/寄存器码.md):结果 r0、输入 r1..r7(只读)、 + * 变量与临时全部从 r8 起 * @param pou POU AST * @param f 输出函数编译态 * @return true 成功;false(err 已写) */ bool build_function(const POU& pou, FuncCtx* f) { - if (pou.kind != PouKind::Program) { - return fail("only PROGRAM supported in slice 1 ('" + pou.name + "')"); + if (pou.kind == PouKind::FunctionBlock) { + return fail("FB not supported in slice 6 ('" + pou.name + "')"); } + f->is_function = pou.kind == PouKind::Function; + f->result_name = pou.name; + if (f->is_function) { + f->regs[pou.name] = 0; // 结果寄存器 r0 + uint8_t idx = 1; + for (const VarBlock& b : pou.blocks) { + if (b.section != VarSection::Input) { + continue; + } + for (const VarDecl& d : b.vars) { + f->regs[d.name] = idx++; // 输入 r1..r7 + } + } + } + uint8_t r = 8; // 变量/临时基址(调用约定区 r0..r7 不占用) for (const VarBlock& b : pou.blocks) { // External / Global 走数据区槽,不占帧寄存器 if (b.section == VarSection::External || b.section == VarSection::Global) { continue; } + // FUNCTION 输入已分配(r1..r7) + if (f->is_function && b.section == VarSection::Input) { + continue; + } for (const VarDecl& d : b.vars) { if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { - return fail("FB instance not supported in slice 1 ('" + d.name + "')"); + return fail("FB instance not supported in slice 6 ('" + d.name + "')"); } - if (f->regs.size() > 255) { + if (r > 255) { return fail("register overflow in function '" + pou.name + "'"); } - f->regs[d.name] = f->nregs++; + f->regs[d.name] = r++; } } - f->nlocals = f->nregs; + f->nlocals = r; + f->nregs = r; for (const Stmt& st : pou.body) { begin_stmt(*f); if (!compile_stmt(*f, st)) { @@ -259,6 +284,10 @@ namespace { // 全局 / 外部左值 → STORE_* 到数据区 return store_target(f, st.target, *st.value); } + // 函数输入只读(调用约定,见 Doc/compiler/寄存器码.md) + if (f.is_function && it->second >= 1 && it->second <= 7) { + return fail("cannot write function input '" + st.target + "'"); + } const uint8_t rd = it->second; return compile_expr(f, *st.value, rd); } @@ -452,7 +481,46 @@ namespace { patch_jump(f, jmp_idx, f.code.size()); return true; } - return fail("expression not supported in slice 3"); + if (e.kind == ExprKind::Call) { + // 调用约定:实参求值到 r8+ 临时 → MOVE 到 r1..r7 → CALL → 结果 r0 + if (e.args.size() > 7) { + return fail("too many arguments (max 7)"); + } + std::vector args; + for (const auto& a : e.args) { + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *a, t)) { + return false; + } + args.push_back(t); + } + const int fn_id = find_fn_id(e.name); + if (fn_id < 0) { + return fail("no fn_id for '" + e.name + "'"); + } + for (size_t i = 0; i < args.size(); ++i) { + f.code.push_back( + isa::enc_rr(isa::Op::MOVE, static_cast(1 + i), args[i])); + } + f.code.push_back(isa::enc_call(static_cast(fn_id))); + f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, 0)); + return true; + } + return fail("expression not supported in slice 6"); + } + + /** + * @brief 按名查 fn_id(POU 收集序下标) + * @param name POU 名(小写) + * @return fn_id;未找到返回 -1 + */ + int find_fn_id(const std::string& name) const { + for (size_t i = 0; i < link_.scopes.size(); ++i) { + if (link_.scopes[i].name == name) { + return static_cast(i); + } + } + return -1; } /** diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 8dac32a..2418128 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -121,7 +121,7 @@ static bool test_case01() { CHECK(v.header().project_hash != isa::kFnvBasis); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 0); + CHECK(r.nregs == 8); // 帧基址(调用约定区) CHECK(r.code_len == 1); char buf[32]; @@ -143,16 +143,16 @@ static bool test_case02() { CHECK(v.header().n_consts == 2); // TRUE 与 FALSE const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 2); // a, b + CHECK(r.nregs == 10); // a, b(r8 起) CHECK(r.code_len == 3); // 从字节码段取指令(按函数行 code_offset 定位) const uint8_t* base = v.code_bytes(); char buf[64]; isa::disasm(reinterpret_cast(base)[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r0, 0") == 0); // a := TRUE(常量 0) + CHECK(std::strcmp(buf, "LOADK r8, 0") == 0); // a := TRUE(常量 0) isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r1, 1") == 0); // b := FALSE(常量 1) + CHECK(std::strcmp(buf, "LOADK r9, 1") == 0); // b := FALSE(常量 1) isa::disasm(reinterpret_cast(base)[2], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); @@ -179,13 +179,13 @@ static bool test_case09() { CHECK(v.data_bytes()[1] == 0); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 1); // 仅局部 x + CHECK(r.nregs == 9); // r8 起:仅局部 x CHECK(r.code_len == 2); char buf[64]; const uint8_t* base = v.code_bytes(); isa::disasm(reinterpret_cast(base)[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r0, 0") == 0); // x := G1(槽 0 偏移 0) + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 0") == 0); // x := G1(槽 0 偏移 0) isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); return true; @@ -213,16 +213,16 @@ static bool test_io_ops() { CHECK(v.header().n_globals == 2); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 2); // q + 1 临时 + CHECK(r.nregs == 10); // r8 起:q + 1 临时 char buf[64]; const uint8_t* base = v.code_bytes(); isa::disasm(reinterpret_cast(base)[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_I r0, 0") == 0); // q := I0_0(io.input → LOAD_I) + CHECK(std::strcmp(buf, "LOAD_I r8, 0") == 0); // q := I0_0(io.input → LOAD_I) isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r1, r0") == 0); // 临时 ← q + CHECK(std::strcmp(buf, "MOVE r9, r8") == 0); // 临时 ← q isa::disasm(reinterpret_cast(base)[2], buf, sizeof buf); - CHECK(std::strcmp(buf, "STORE_Q r1, 1") == 0); // Q0_0 := 临时(io.output → STORE_Q) + CHECK(std::strcmp(buf, "STORE_Q r9, 1") == 0); // Q0_0 := 临时(io.output → STORE_Q) isa::disasm(reinterpret_cast(base)[3], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); return true; @@ -255,7 +255,7 @@ static bool test_layout() { char buf[64]; isa::disasm(reinterpret_cast(v.code_bytes())[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r0, 2") == 0); // x := I(INT 偏移 2) + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 2") == 0); // x := I(INT 偏移 2) return true; } @@ -289,7 +289,7 @@ static bool test_case03() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 4); // a b x + 1 临时 + CHECK(r.nregs == 12); // r8 起:a b x + 1 临时 CHECK(r.code_len == 9); char buf[64]; @@ -297,22 +297,22 @@ static bool test_case03() { const uint32_t* ins = reinterpret_cast(base); // x := a AND b:MOVE r2,r0 / JF r2,+2 / MOVE r3,r1 / MOVE r2,r3 isa::disasm(ins[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); + CHECK(std::strcmp(buf, "MOVE r10, r8") == 0); isa::disasm(ins[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "JF r2, +2") == 0); + CHECK(std::strcmp(buf, "JF r10, +2") == 0); isa::disasm(ins[2], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r3, r1") == 0); + CHECK(std::strcmp(buf, "MOVE r11, r9") == 0); isa::disasm(ins[3], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r2, r3") == 0); + CHECK(std::strcmp(buf, "MOVE r10, r11") == 0); // x := a OR b:MOVE r2,r0 / JT r2,+2 / MOVE r3,r1 / MOVE r2,r3 isa::disasm(ins[4], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); + CHECK(std::strcmp(buf, "MOVE r10, r8") == 0); isa::disasm(ins[5], buf, sizeof buf); - CHECK(std::strcmp(buf, "JT r2, +2") == 0); + CHECK(std::strcmp(buf, "JT r10, +2") == 0); isa::disasm(ins[6], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r3, r1") == 0); + CHECK(std::strcmp(buf, "MOVE r11, r9") == 0); isa::disasm(ins[7], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r2, r3") == 0); + CHECK(std::strcmp(buf, "MOVE r10, r11") == 0); isa::disasm(ins[8], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); return true; @@ -332,16 +332,16 @@ static bool test_not() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 3); // a x + 1 临时 + CHECK(r.nregs == 11); // r8 起:a x + 1 临时 CHECK(r.code_len == 3); char buf[64]; const uint8_t* base = v.code_bytes(); const uint32_t* ins = reinterpret_cast(base); isa::disasm(ins[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); // 临时 ← a + CHECK(std::strcmp(buf, "MOVE r10, r8") == 0); // 临时 ← a isa::disasm(ins[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "NOT r1, r2") == 0); // x := NOT 临时 + CHECK(std::strcmp(buf, "NOT r9, r10") == 0); // x := NOT 临时 isa::disasm(ins[2], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); return true; @@ -357,7 +357,7 @@ static bool test_case04() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 5); // sel out + 3 临时(CMP 目的 + 两操作数) + CHECK(r.nregs == 13); // r8 起:sel out + 3 临时 CHECK(r.code_len == 14); char buf[64]; @@ -365,33 +365,33 @@ static bool test_case04() { const uint32_t* ins = reinterpret_cast(base); // IF sel = 0 THEN:sel→r3、0→r4、CMP_EQ r2(常量序:0、10、1、20、30) isa::disasm(ins[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r3, r0") == 0); + CHECK(std::strcmp(buf, "MOVE r11, r8") == 0); isa::disasm(ins[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r4, 0") == 0); + CHECK(std::strcmp(buf, "LOADK r12, 0") == 0); isa::disasm(ins[2], buf, sizeof buf); - CHECK(std::strcmp(buf, "CMP_EQ r2, r3, r4") == 0); + CHECK(std::strcmp(buf, "CMP_EQ r10, r11, r12") == 0); isa::disasm(ins[3], buf, sizeof buf); - CHECK(std::strcmp(buf, "JF r2, +2") == 0); // 假 → elsif([6]) + CHECK(std::strcmp(buf, "JF r10, +2") == 0); // 假 → elsif([6]) isa::disasm(ins[4], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r1, 1") == 0); // out := 10(常量 1) + CHECK(std::strcmp(buf, "LOADK r9, 1") == 0); // out := 10(常量 1) isa::disasm(ins[5], buf, sizeof buf); CHECK(std::strcmp(buf, "JMP +7") == 0); // → end([13]) // ELSIF sel = 1 isa::disasm(ins[6], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r3, r0") == 0); + CHECK(std::strcmp(buf, "MOVE r11, r8") == 0); isa::disasm(ins[7], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r4, 2") == 0); + CHECK(std::strcmp(buf, "LOADK r12, 2") == 0); isa::disasm(ins[8], buf, sizeof buf); - CHECK(std::strcmp(buf, "CMP_EQ r2, r3, r4") == 0); + CHECK(std::strcmp(buf, "CMP_EQ r10, r11, r12") == 0); isa::disasm(ins[9], buf, sizeof buf); - CHECK(std::strcmp(buf, "JF r2, +2") == 0); // 假 → else([12]) + CHECK(std::strcmp(buf, "JF r10, +2") == 0); // 假 → else([12]) isa::disasm(ins[10], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r1, 3") == 0); + CHECK(std::strcmp(buf, "LOADK r9, 3") == 0); isa::disasm(ins[11], buf, sizeof buf); CHECK(std::strcmp(buf, "JMP +1") == 0); // → end([13]) // ELSE isa::disasm(ins[12], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r1, 4") == 0); + CHECK(std::strcmp(buf, "LOADK r9, 4") == 0); isa::disasm(ins[13], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); return true; @@ -407,30 +407,30 @@ static bool test_case05() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 4); // n + 3 临时(CMP 目的 + 两操作数) + CHECK(r.nregs == 12); // r8 起:n + 3 临时(条件结果 r9 + 两操作数) CHECK(r.code_len == 10); char buf[64]; const uint8_t* base = v.code_bytes(); const uint32_t* ins = reinterpret_cast(base); isa::disasm(ins[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r0, 0") == 0); // n := 0 - // L_loop [1]:n < 10 → r1 + CHECK(std::strcmp(buf, "LOADK r8, 0") == 0); // n := 0 + // L_loop [1]:n < 10 → r9 isa::disasm(ins[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r2, r0") == 0); + CHECK(std::strcmp(buf, "MOVE r10, r8") == 0); isa::disasm(ins[2], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r3, 1") == 0); // 常量 1 = 10 + CHECK(std::strcmp(buf, "LOADK r11, 1") == 0); // 常量 1 = 10 isa::disasm(ins[3], buf, sizeof buf); - CHECK(std::strcmp(buf, "CMP_LT r1, r2, r3") == 0); + CHECK(std::strcmp(buf, "CMP_LT r9, r10, r11") == 0); isa::disasm(ins[4], buf, sizeof buf); - CHECK(std::strcmp(buf, "JF r1, +4") == 0); // 假 → RET([9]) - // n := n + 1(体语句临时复用 r1/r2) + CHECK(std::strcmp(buf, "JF r9, +4") == 0); // 假 → RET([9]) + // n := n + 1(体语句临时复用 r9/r10) isa::disasm(ins[5], buf, sizeof buf); - CHECK(std::strcmp(buf, "MOVE r1, r0") == 0); + CHECK(std::strcmp(buf, "MOVE r9, r8") == 0); isa::disasm(ins[6], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r2, 2") == 0); // 常量 2 = 1 + CHECK(std::strcmp(buf, "LOADK r10, 2") == 0); // 常量 2 = 1 isa::disasm(ins[7], buf, sizeof buf); - CHECK(std::strcmp(buf, "ADD r0, r1, r2") == 0); + CHECK(std::strcmp(buf, "ADD r8, r9, r10") == 0); isa::disasm(ins[8], buf, sizeof buf); CHECK(std::strcmp(buf, "JMP -8") == 0); // 回 L_loop([1]) isa::disasm(ins[9], buf, sizeof buf); @@ -448,7 +448,7 @@ static bool test_case07() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 6); // a b c eq + 2 临时(语句间复用) + CHECK(r.nregs == 14); // r8 起:a b c eq + 2 临时 char buf[64]; const uint8_t* base = v.code_bytes(); @@ -456,10 +456,10 @@ static bool test_case07() { bool saw_mul = false, saw_div = false, saw_sub = false, saw_gt = false; for (uint32_t i = 0; i < r.code_len; ++i) { isa::disasm(ins[i], buf, sizeof buf); - if (std::strstr(buf, "MUL r0, ")) saw_mul = true; - if (std::strstr(buf, "DIV r1, ")) saw_div = true; - if (std::strstr(buf, "SUB r2, ")) saw_sub = true; - if (std::strstr(buf, "CMP_GT r3, ")) saw_gt = true; + if (std::strstr(buf, "MUL r8, ")) saw_mul = true; + if (std::strstr(buf, "DIV r9, ")) saw_div = true; + if (std::strstr(buf, "SUB r10, ")) saw_sub = true; + if (std::strstr(buf, "CMP_GT r11, ")) saw_gt = true; } CHECK(saw_mul && saw_div && saw_sub && saw_gt); return true; @@ -481,13 +481,63 @@ static bool test_case08() { CHECK(c1.tag == isa::types::Time && c1.value == 1250); // T#1s250ms const isa::FuncRow r = v.func_row(0); - CHECK(r.nregs == 2); + CHECK(r.nregs == 10); // r8 起 char buf[64]; const uint32_t* ins = reinterpret_cast(v.code_bytes()); isa::disasm(ins[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r0, 0") == 0); + CHECK(std::strcmp(buf, "LOADK r8, 0") == 0); isa::disasm(ins[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOADK r1, 1") == 0); + CHECK(std::strcmp(buf, "LOADK r9, 1") == 0); + return true; +} + +// ---- 13. 用例 13:FUNCTION + CALL(调用约定 r0 结果 / r1.. 参数)---- + +static bool test_case13() { + std::vector img; + std::string err; + CHECK(compile_case("13_function_call", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_funcs == 2); + CHECK(v.header().entry_fn_id == 1); // MAIN 是第二个 POU + + char buf[64]; + const uint8_t* base = v.code_bytes(); + // Add(fn_id 0):结果 r0、输入 r1(a) r2(b)、临时 r8/r9 + const isa::FuncRow fa = v.func_row(0); + CHECK(fa.nregs == 10); + CHECK(fa.code_len == 4); + const uint32_t* ia = reinterpret_cast(base); + isa::disasm(ia[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r8, r1") == 0); // t ← a + isa::disasm(ia[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r9, r2") == 0); // t ← b + isa::disasm(ia[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "ADD r0, r8, r9") == 0); // 结果 r0 + isa::disasm(ia[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + + // MAIN(fn_id 1):x 在 r8,实参临时 r9/r10 + const isa::FuncRow fm = v.func_row(1); + CHECK(fm.nregs == 11); + CHECK(fm.code_len == 7); + const uint32_t* im = reinterpret_cast(base) + fm.code_offset / 4; + isa::disasm(im[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r9, 0") == 0); // 3 + isa::disasm(im[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r10, 1") == 0); // 4 + isa::disasm(im[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r1, r9") == 0); // 参数 1 ← 3 + isa::disasm(im[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r2, r10") == 0); // 参数 2 ← 4 + isa::disasm(im[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "CALL 0") == 0); + isa::disasm(im[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r8, r0") == 0); // x ← 结果 + isa::disasm(im[6], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); return true; } @@ -504,6 +554,7 @@ int main() { if (!test_case05()) return 1; if (!test_case07()) return 1; if (!test_case08()) return 1; + if (!test_case13()) return 1; std::printf("codegen_test: %d checks passed\n", g_checks); return 0; } From 0263813a8f451046c45440e550d0a1684bc338e3 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:52:25 +0800 Subject: [PATCH 32/69] =?UTF-8?q?12.8=20=E5=88=87=E7=89=87=207=EF=BC=9AFB?= =?UTF-8?q?=20=E5=AE=9E=E4=BE=8B=E5=9D=97=E3=80=81=E7=94=A8=E6=88=B7=20FB?= =?UTF-8?q?=20=E5=86=85=E8=81=94=E3=80=81CAL=5FTON/TOF/CTU=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FB 实例 → 数据区实例块(字段地址 = 基址+偏移,编译期算死;跨周期持久) - 用户 FB 调用点内联展开(实参写字段 → FB 体以实例字段为变量编译) - 内建 TON/TOF/CTU:实参写字段 → CAL_* <实例偏移>;FB 生成空函数占位保持 fn_id - codegen_test:189 断言(用例 15 内联序列、17 CAL_TON 字段偏移、18 TOF/CTU、line1 全链路 LOAD_I/STORE_Q),ctest 9/9 - 20 用例扫描:14 正例全部出映像,6 负例按期望在前层拒绝 --- compiler/src/Codegen.cpp | 237 ++++++++++++++++++++++++++++++++----- tests/src/codegen_test.cpp | 149 +++++++++++++++++++++++ 2 files changed, 359 insertions(+), 27 deletions(-) diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 8aed05f..29bb880 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,37 +1,37 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 6:+ FUNCTION 与 CALL) + * @brief 寄存器码生成(12.8,切片 7:+ FB 实例、内联展开、CAL_*) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1~5:帧/字面量/MOVE/RET;全局数据区;NOT 与短路 AND/OR;CMP 与 IF;WHILE 与四则。 - * - 切片 6:调用约定(冻结)——结果 r0、参数 r1..r7(最多 7 输入、函数内只读)、 - * 变量与临时全部从 r8 起(调用约定区不受覆盖); - * CALL 时 VM 复制 r0..r7 到新帧、RET 复制回调用方(r0=结果); - * 调用点:实参求值到 r8+ 临时 → MOVE r(1+i) → CALL fn_id → MOVE rd, r0 - * - 其余构造报 codegen error(后续切片展开) + * - 切片 1~6:帧/字面量/MOVE/RET;全局数据区;短路 AND/OR;CMP 与 IF;WHILE 与四则; + * FUNCTION 与 CALL(调用约定 r0/r1..r7/r8+)。 + * - 切片 7:FB 实例 → 数据区实例块(跨周期持久);字段地址 = 实例基址 + 字段偏移 + * (编译期算死);字段读写走 LOAD_GLOBAL / STORE_GLOBAL; + * 内建 TON/TOF/CTU → 实参写字段后 CAL_* <实例偏移>; + * 用户 FB → 调用点内联展开(实参写字段 → FB 体以实例字段为变量编译) + * - 其余构造报 codegen error * * 函数清单: * - put_le32 / put_le64 小端写入映像缓冲 * - align_up / width_of 对齐与槽宽 * - Builder::Builder (构造)存工程/源文件/链接结果/输出,收集 io 绑定分类 - * - Builder::run 布局数据区 → 逐 POU 建函数 → 拼映像 + * - Builder::run 布局数据区(全局 + 实例)→ 逐 POU 建函数 → 拼映像 * - Builder::fail 组装 "codegen error: " 返回 false - * - find_pou 按名查 POU AST - * - find_fn_id 按名查 fn_id(POU 收集序) - * - build_function 编译一个 POU(PROGRAM/FUNCTION;帧约定分配) + * - find_pou / find_fn_id 按名查 POU AST / fn_id + * - build_function 编译一个 POU(PROGRAM/FUNCTION/FB 骨架;帧约定分配) * - begin_stmt / alloc_temp 临时寄存器:语句内递增、语句结束复用基址 - * - compile_stmt 语句编译(赋值 / IF / WHILE;函数输入只读) - * - compile_if IF/ELSIF/ELSE:条件链 + 分支尾部 JMP,JF/JMP 回填 - * - compile_while WHILE:L_loop + 条件 JF + 体 + JMP 回环 - * - store_target 赋值左值:STORE_* 到全局槽 - * - compile_expr 表达式编译到寄存器(含 CMP/算术/NOT/短路/CALL) - * - cmp_op / arith_op 比较符/算术符 → 操作码 - * - load_op / store_op 按 io 绑定选读取/写入操作码 - * - global_offset 逻辑槽号 → 数据区字节偏移 + * - compile_stmt 语句编译(赋值 / IF / WHILE / FB 调用) + * - compile_if / compile_while IF 链 / WHILE 回环 + * - compile_fb_call FB 调用:内建 CAL_* 或用户内联展开 + * - compile_fb_inline 内联编译用户 FB 体(实例字段为变量) + * - store_target 赋值左值:STORE_* 到全局槽(或内联模式实例字段) + * - compile_expr 表达式编译到寄存器(含 Field 字段读) + * - cmp_op / arith_op / load_op / store_op 操作码选择 + * - global_offset / instance_of 槽号 → 偏移 / 实例 → 字段地址 * - patch_jump 回填跳转偏移(相对下一条指令) - * - layout_data / init_of 全局数据区布局与初值 + * - layout_data / layout_fb_instances / init_of 数据区布局 * - const_id 取常量表 id(无则追加) * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + 数据段 * - codegen_project 对外入口 @@ -89,6 +89,17 @@ namespace { return 1; // bool 及未知 } + /** + * @brief TypeKind → 槽宽(FB 字段布局用) + * @param k 类型种类 + * @return 1 / 2 / 8;未知返回 1 + */ + uint32_t width_of(TypeKind k) { + if (k == TypeKind::Int) return 2; + if (k == TypeKind::Time) return 8; + return 1; + } + /** * @brief 代码生成器(切片 1) */ @@ -120,14 +131,17 @@ namespace { } /** - * @brief 布局数据区 → 逐 POU 建函数 → 拼映像 - * @details 数据区偏移先定(函数编译引用 global_offset) + * @brief 布局数据区 → 布局 FB 实例 → 逐 POU 建函数 → 拼映像 + * @details 偏移先定(函数编译引用 global_offset / instance_of) * @return true 成功;false(err 已写,前缀 "codegen error") */ bool run() { if (!layout_data()) { return false; } + if (!layout_fb_instances()) { + return false; + } for (const LinkResult::PouScope& sc : link_.scopes) { const POU* pou = find_pou(sc.name); if (pou == nullptr) { @@ -135,6 +149,7 @@ namespace { } FuncCtx f; f.name = sc.name; + f.pou_name = sc.name; if (!build_function(*pou, &f)) { return false; } @@ -175,6 +190,7 @@ namespace { // 每函数的编译态 struct FuncCtx { std::string name; + std::string pou_name; // 所属 POU 名(实例查找键) std::vector code; // 字节码(函数表 code_offset 相对此段) std::map regs; // 变量名 → 帧寄存器 uint8_t nlocals = 0; // 变量区终点 = 临时寄存器起始 @@ -184,6 +200,13 @@ namespace { std::string result_name; // FUNCTION 名(结果寄存器映射) }; + // FB 实例:字段名 → 数据区字节地址 + struct InstFields { + std::map field_addr; + uint32_t base = 0; + std::string type_name; // 实例的 FB 类型名(内建 / 用户) + }; + /** * @brief 语句开始:临时寄存器基址复用 * @param f 当前函数 @@ -206,7 +229,8 @@ namespace { /** * @brief 编译一个 POU - * @details 切片 6:支持 PROGRAM 与 FUNCTION(FB 留切片 8) + * @details 支持 PROGRAM / FUNCTION / FUNCTION_BLOCK: + * FB 不生成业务字节码(调用点内联),只出空函数占位保持 fn_id 一致; * 帧约定(见 Doc/compiler/寄存器码.md):结果 r0、输入 r1..r7(只读)、 * 变量与临时全部从 r8 起 * @param pou POU AST @@ -215,7 +239,11 @@ namespace { */ bool build_function(const POU& pou, FuncCtx* f) { if (pou.kind == PouKind::FunctionBlock) { - return fail("FB not supported in slice 6 ('" + pou.name + "')"); + // FB 内联展开(见 compile_fb_call),此处只占 fn_id + f->nlocals = 8; + f->nregs = 8; + f->code.push_back(isa::enc_ret()); + return true; } f->is_function = pou.kind == PouKind::Function; f->result_name = pou.name; @@ -242,8 +270,9 @@ namespace { continue; } for (const VarDecl& d : b.vars) { + // FB 实例走数据区(layout_fb_instances),不占帧寄存器 if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { - return fail("FB instance not supported in slice 6 ('" + d.name + "')"); + continue; } if (r > 255) { return fail("register overflow in function '" + pou.name + "'"); @@ -276,8 +305,23 @@ namespace { if (st.kind == StmtKind::While) { return compile_while(f, st); } + if (st.kind == StmtKind::FbCall) { + return compile_fb_call(f, st); + } if (st.kind != StmtKind::Assign) { - return fail("statement not supported in slice 5"); + return fail("statement not supported in slice 7"); + } + // 内联 FB 体内:左值可能是实例字段(STORE_GLOBAL) + if (inline_fields_) { + const auto fit = inline_fields_->field_addr.find(st.target); + if (fit != inline_fields_->field_addr.end()) { + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *st.value, t)) { + return false; + } + f.code.push_back(isa::enc_slot(isa::Op::STORE_GLOBAL, t, fit->second)); + return true; + } } const auto it = f.regs.find(st.target); if (it == f.regs.end()) { @@ -292,6 +336,68 @@ namespace { return compile_expr(f, *st.value, rd); } + /** + * @brief 编译 FB 调用 + * @details 实参求值到临时 → STORE_GLOBAL 到实例字段; + * 内建 TON/TOF/CTU → CAL_* <实例偏移>; + * 用户 FB → compile_fb_inline 内联展开 + * @param f 当前函数 + * @param st FbCall 语句 AST + * @return true 成功;false(err 已写) + */ + bool compile_fb_call(FuncCtx& f, const Stmt& st) { + const InstFields* inst = instance_of(f.pou_name, st.instance); + if (inst == nullptr) { + return fail("no instance '" + st.instance + "' in '" + f.pou_name + "'"); + } + for (const FbArg& a : st.args) { + const auto it = inst->field_addr.find(a.name); + if (it == inst->field_addr.end()) { + return fail("unknown input '" + a.name + "' for '" + st.instance + "'"); + } + const uint8_t t = alloc_temp(f); + if (!compile_expr(f, *a.value, t)) { + return false; + } + f.code.push_back(isa::enc_slot(isa::Op::STORE_GLOBAL, t, it->second)); + } + if (inst->type_name == "ton" || inst->type_name == "tof" || + inst->type_name == "ctu") { + const isa::Op op = inst->type_name == "ton" ? isa::Op::CAL_TON + : inst->type_name == "tof" ? isa::Op::CAL_TOF + : isa::Op::CAL_CTU; + f.code.push_back(isa::enc_slot(op, 0, inst->base)); + return true; + } + const POU* fb = find_pou(inst->type_name); + if (fb == nullptr) { + return fail("no FB type '" + inst->type_name + "'"); + } + return compile_fb_inline(f, *fb, *inst); + } + + /** + * @brief 内联编译用户 FB 体 + * @details 切到实例字段上下文(VarRef/赋值左值映射到字段地址); + * 表达式临时与调用方共用 r8+;字段读写全走 LOAD/STORE_GLOBAL + * @param f 当前函数 + * @param fb FB 类型 POU + * @param inst 本实例字段表 + * @return true 成功;false(err 已写) + */ + bool compile_fb_inline(FuncCtx& f, const POU& fb, const InstFields& inst) { + const InstFields* saved = inline_fields_; + inline_fields_ = &inst; + for (const Stmt& s : fb.body) { + begin_stmt(f); + if (!compile_stmt(f, s)) { + return false; + } + } + inline_fields_ = saved; + return true; + } + /** * @brief 编译 WHILE * @details L_loop: JF t, L_end JMP L_loop L_end: @@ -419,6 +525,14 @@ namespace { return true; } if (e.kind == ExprKind::VarRef) { + // 内联 FB 体内:字段优先(实例字段为变量) + if (inline_fields_) { + const auto fit = inline_fields_->field_addr.find(e.name); + if (fit != inline_fields_->field_addr.end()) { + f.code.push_back(isa::enc_slot(isa::Op::LOAD_GLOBAL, rd, fit->second)); + return true; + } + } const auto sit = f.regs.find(e.name); if (sit != f.regs.end()) { f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, sit->second)); @@ -432,6 +546,18 @@ namespace { } return fail("no register or slot for '" + e.name + "'"); } + if (e.kind == ExprKind::Field) { + const InstFields* inst = instance_of(f.pou_name, e.name); + if (inst == nullptr) { + return fail("no instance '" + e.name + "' in '" + f.pou_name + "'"); + } + const auto fit = inst->field_addr.find(e.field); + if (fit == inst->field_addr.end()) { + return fail("unknown field '" + e.field + "' for '" + e.name + "'"); + } + f.code.push_back(isa::enc_slot(isa::Op::LOAD_GLOBAL, rd, fit->second)); + return true; + } if (e.kind == ExprKind::Add || e.kind == ExprKind::Sub || e.kind == ExprKind::Mul || e.kind == ExprKind::Div) { const uint8_t l = alloc_temp(f); @@ -633,6 +759,61 @@ namespace { return true; } + /** + * @brief 布局 FB 实例块:数据区全局之后,按 POU 收集序 / 实例声明序 + * @details 每实例:字段按布局段序对齐排布(BOOL 1 / INT 2 / TIME 8), + * 字段地址 = 实例基址 + 字段偏移;实例块间 8 字节对齐,块内容初值 0 + * @return true 成功;false(err 已写) + */ + bool layout_fb_instances() { + uint32_t cur = align_up(static_cast(data_.size()), 8); + bool any = false; + for (const LinkResult::PouScope& sc : link_.scopes) { + for (const Symbol& s : sc.syms) { + if (s.kind != SymbolKind::FbInstance) { + continue; + } + const auto lay = sc.fb_instances.find(s.name); + if (lay == sc.fb_instances.end()) { + return fail("no layout for instance '" + s.name + "'"); + } + cur = align_up(cur, 8); + InstFields inst; + inst.base = cur; + inst.type_name = s.type_name; + uint32_t off = 0; + for (const FbField& fd : lay->second.fields) { + const uint32_t w = width_of(fd.type); + off = align_up(off, w); + inst.field_addr[fd.name] = cur + off; + off += w; + } + cur += align_up(off, 8); + instances_[sc.name + "/" + s.name] = inst; + any = true; + } + } + // 仅在有实例时把数据区补到实例区终点(无实例则保持全局区原样) + if (any) { + while (data_.size() < cur) { + data_.push_back(0); + } + } + return true; + } + + /** + * @brief 按 POU + 实例名查实例字段表 + * @param pou POU 名 + * @param name 实例名 + * @return 实例字段表指针;未找到返回 nullptr + */ + const InstFields* instance_of(const std::string& pou, + const std::string& name) const { + const auto it = instances_.find(pou + "/" + name); + return it == instances_.end() ? nullptr : &it->second; + } + /** * @brief 查全局声明的初值(AST;gvl 文件顶层段,声明序 = 槽号序) * @param name 全局名(小写) @@ -765,7 +946,9 @@ namespace { std::map io_input_; // io.input 绑定名(小写) std::map io_output_; // io.output 绑定名(小写) std::vector global_offsets_; // 逻辑槽号 → 数据区字节偏移 - std::vector data_; // 数据区(全局初值字节) + std::vector data_; // 数据区(全局初值 + FB 实例块) + std::map instances_; // "POU/实例名" → 字段地址 + const InstFields* inline_fields_ = nullptr; // 内联 FB 体字段上下文(可空) }; } // namespace diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 2418128..aab5a73 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -541,6 +541,151 @@ static bool test_case13() { return true; } +// ---- 14. 用例 15:用户 FB 内联展开 ---- + +static bool test_case15() { + std::vector img; + std::string err; + CHECK(compile_case("15_fb_instance", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_funcs == 2); // FB 占位 + MAIN + CHECK(v.header().entry_fn_id == 1); + CHECK(v.data_len() == 8); // 实例 3B(start@0 stop@1 q@2),8 对齐 + + const isa::FuncRow fm = v.func_row(1); + CHECK(fm.nregs == 12); + CHECK(fm.code_len == 12); + const uint32_t* im = reinterpret_cast(v.code_bytes()) + fm.code_offset / 4; + char buf[64]; + // 实参写字段:start := TRUE / stop := FALSE + isa::disasm(im[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r9, 0") == 0); + isa::disasm(im[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r9, 0") == 0); + isa::disasm(im[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r10, 1") == 0); + isa::disasm(im[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r10, 1") == 0); + // 内联体:Q := start AND NOT stop(短路跳转) + isa::disasm(im[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r9, 0") == 0); + isa::disasm(im[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r9, +3") == 0); + isa::disasm(im[9], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r9, 2") == 0); // Q 字段 + // q := starter.Q + isa::disasm(im[10], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 2") == 0); + return true; +} + +// ---- 15. 用例 17:TON(CAL_TON + 字段偏移)---- + +static bool test_case17() { + std::vector img; + std::string err; + CHECK(compile_case("17_ton", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.data_len() == 32); // in@0 pt@8 q@16 et@24 + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 11); + const uint32_t* ins = reinterpret_cast(v.code_bytes()); + char buf[64]; + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r9, 0") == 0); + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r9, 0") == 0); // in + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r10, 1") == 0); + isa::disasm(ins[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r10, 8") == 0); // pt(TIME 8 对齐) + isa::disasm(ins[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "CAL_TON 0") == 0); + isa::disasm(ins[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 16") == 0); // t.Q + return true; +} + +// ---- 16. 用例 18:TOF / CTU ---- + +static bool test_case18() { + std::vector img; + std::string err; + CHECK(compile_case("18_tof_ctu", &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.data_len() == 40); // tf 块 0..31(in@0 pt@8 q@16 et@24)、c 块 32.. + const isa::FuncRow r = v.func_row(0); + CHECK(r.nregs == 14); + const uint32_t* ins = reinterpret_cast(v.code_bytes()); + char buf[64]; + isa::disasm(ins[4], buf, sizeof buf); + CHECK(std::strcmp(buf, "CAL_TOF 0") == 0); + isa::disasm(ins[12], buf, sizeof buf); + CHECK(std::strcmp(buf, "CAL_CTU 32") == 0); + isa::disasm(ins[13], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r9, 36") == 0); // c.Q + isa::disasm(ins[14], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r10, 38") == 0); // c.CV(INT 2 对齐) + return true; +} + +// ---- 17. line1 全链路(MAIN + Motor FB + I/O + 全局)---- + +static bool test_line1() { + std::vector img; + std::string err; + using namespace compiler; + const std::string toml = std::string(REPO_ROOT) + "/examples/line1/project.toml"; + Project p; + CHECK(parse_project(toml, &p, &err)); + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + CHECK(load_unit(p.base_dir + "/" + f, &u, &err)); + units.push_back(std::move(u)); + } + LinkResult link; + CHECK(link_project(p, units, &link, &err)); + CHECK(check_project(p, units, link, &err)); + CHECK(codegen_project(p, units, link, &img, &err)); + + const isa::ImageView v = isa::ImageView::from(img); + CHECK(v.ok()); + CHECK(v.header().n_globals == 4); + CHECK(v.header().n_funcs == 2); + CHECK(v.header().entry_fn_id == 1); + CHECK(v.data_len() == 16); // 全局 4B + 实例块 8..10(8 对齐) + + const isa::FuncRow fm = v.func_row(1); + CHECK(fm.nregs == 13); + CHECK(fm.code_len == 17); + const uint32_t* im = reinterpret_cast(v.code_bytes()) + fm.code_offset / 4; + char buf[64]; + isa::disasm(im[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_I r8, 1") == 0); // I0_0(io.input) + isa::disasm(im[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r8, 8") == 0); // starter.start + isa::disasm(im[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r9, 2") == 0); // I0_1 + isa::disasm(im[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r9, 9") == 0); // starter.stop + isa::disasm(im[13], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_GLOBAL r8, 10") == 0); // starter.Q + isa::disasm(im[14], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 10") == 0); // starter.Q 读回 + isa::disasm(im[15], buf, sizeof buf); + CHECK(std::strcmp(buf, "STORE_Q r8, 3") == 0); // Q0_0(io.output) + isa::disasm(im[16], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + return true; +} + int main() { if (!test_case01()) return 1; if (!test_case02()) return 1; @@ -555,6 +700,10 @@ int main() { if (!test_case07()) return 1; if (!test_case08()) return 1; if (!test_case13()) return 1; + if (!test_case15()) return 1; + if (!test_case17()) return 1; + if (!test_case18()) return 1; + if (!test_line1()) return 1; std::printf("codegen_test: %d checks passed\n", g_checks); return 0; } From 0a42d5876e9cadb62f2f55164004c8620a8581a2 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:54:02 +0800 Subject: [PATCH 33/69] =?UTF-8?q?STCompiler=20=E6=8E=A5=E7=BA=BF=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E7=BC=96=E8=AF=91=E7=AE=A1=E7=BA=BF=EF=BC=9A-o=20?= =?UTF-8?q?=E4=BA=A7=E5=87=BA=20.stb=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STCompiler -o .stb:读 .st → 链接 → 类型检查 → 寄存器码 → 写映像 - 无 -o 保留 12.3 行为(打印文件集合与哈希) - 验证:line1 编译出 184 字节 .stb(2 函数 4 全局);负例 10/14/19 按期望报错退出码 1 --- compiler/src/main.cpp | 72 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index a45da05..e7bccc1 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -10,12 +10,18 @@ #include #include +#include "compiler/Codegen.h" +#include "compiler/Linker.h" #include "compiler/Project.h" +#include "compiler/Typecheck.h" +#include "isa/Image.h" namespace { void usage() { - std::printf("usage: STCompiler \n" - " 解析工程并打印文件集合与工程哈希(12.3 阶段)\n" + std::printf("usage: STCompiler [-o .stb]\n" + " 解析工程并编译(词法 → 语法 → 链接 → 类型 → 寄存器码)\n" + " -o .stb 编译并写出映像文件\n" + " 无 -o 只打印文件集合与工程哈希(12.3 阶段)\n" " --help 打印本帮助\n"); } } @@ -31,24 +37,68 @@ int main(int argc, char** argv) { return 0; } + const std::string toml_path = argv[1]; + std::string out_path; + for (int i = 2; i + 1 < argc; ++i) { + if (std::strcmp(argv[i], "-o") == 0) { + out_path = argv[i + 1]; + } + } + compiler::Project proj; std::string err; - if (!compiler::parse_project(argv[1], &proj, &err)) { + if (!compiler::parse_project(toml_path, &proj, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } const std::vector files = compiler::compile_files(proj); - uint64_t hash = 0; - if (!compiler::compute_project_hash(proj, &hash, &err)) { + + if (out_path.empty()) { + uint64_t hash = 0; + if (!compiler::compute_project_hash(proj, &hash, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + std::printf("project: %s\n", proj.name.c_str()); + std::printf("files:\n"); + for (const std::string& f : files) { + std::printf(" %s\n", f.c_str()); + } + std::printf("hash: 0x%016llx\n", static_cast(hash)); + return 0; + } + + // 完整管线:读 .st → 链接 → 类型检查 → 寄存器码 + std::vector units; + for (const std::string& f : files) { + compiler::SourceUnit u; + if (!compiler::load_unit(proj.base_dir + "/" + f, &u, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + units.push_back(std::move(u)); + } + compiler::LinkResult link; + if (!compiler::link_project(proj, units, &link, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + if (!compiler::check_project(proj, units, link, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + std::vector image; + if (!compiler::codegen_project(proj, units, link, &image, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + if (!isa::write_stb_file(out_path.c_str(), image, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } - std::printf("project: %s\n", proj.name.c_str()); - std::printf("files:\n"); - for (const std::string& f : files) { - std::printf(" %s\n", f.c_str()); - } - std::printf("hash: 0x%016llx\n", static_cast(hash)); + const isa::ImageView v = isa::ImageView::from(image); + std::printf("compiled: %s (%zu bytes, %u functions, %u globals)\n", + out_path.c_str(), image.size(), v.header().n_funcs, v.header().n_globals); return 0; } From eb6829b9b9b45a1ef7f1ba2d91a24c0f54c9d11a Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:55:36 +0800 Subject: [PATCH 34/69] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20STCompiler=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 构建 / 用法(-o 产出 .stb)/ 退出码 / line1 示例 - project.toml 字段表、ST 子集支持范围与 v1 限制清单 - 编译管线五类错误前缀与示例;样例与测试索引;当前状态 - compiler/README 与 Doc/索引 登记 --- Doc/compiler/STCompiler使用说明.md | 123 +++++++++++++++++++++++++++++ Doc/索引.md | 2 + compiler/README.md | 6 +- 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 Doc/compiler/STCompiler使用说明.md diff --git a/Doc/compiler/STCompiler使用说明.md b/Doc/compiler/STCompiler使用说明.md new file mode 100644 index 0000000..f29900e --- /dev/null +++ b/Doc/compiler/STCompiler使用说明.md @@ -0,0 +1,123 @@ +# STCompiler 使用说明 + +把 ST 工程(`project.toml` + 若干 `.st`)编译成单一映像 `.stb`。执行由 `BytecodeExecutor`(12.9/12.10 实现)负责。 + +## 构建 + +```bash +cmake --preset gcc-debug # 配置 +cmake --build --preset gcc-debug # 编译 +ctest --test-dir build/gcc-debug # 测试(当前 9 个用例) +``` + +可执行文件:`build/gcc-debug/compiler/STCompiler` + +## 用法 + +```text +STCompiler [-o .stb] +``` + +| 参数 | 作用 | +|---|---| +| `` | 工程文件(必填) | +| `-o .stb` | 编译并写出映像文件;**缺省时只打印文件集合与工程哈希**(12.3 阶段行为) | +| `--help` | 打印帮助 | + +退出码:`0` 成功;`1` 编译失败(错误信息打到 stderr)。 + +## 示例(line1) + +```bash +$ STCompiler examples/line1/project.toml +project: line1 +files: + globals.st + motor.st + main.st +hash: 0xc3fe4ae74ad45900 + +$ STCompiler examples/line1/project.toml -o line1.stb +compiled: line1.stb (184 bytes, 2 functions, 4 globals) +``` + +产物 `line1.stb`:魔数 `STSC` + 版本 1 的单一映像(指令见 `Doc/isa/指令与映像.md`)。 + +## 工程文件(project.toml) + +只做三件事:**列出源文件、指定唯一 GVL、可选地把已声明全局接到硬件**。不声明变量。 + +| 字段 | 必填 | 说明 | +|---|---|---| +| `project.name` | 是 | 工程名 | +| `project.entry` | 是 | 第一版必须 `"program MAIN"` | +| `project.cycle_limit` | 是 | 每周期指令上限(>0) | +| `project.dt_ms` | 是 | 本周期 Δt 毫秒(>0),给 TON/TOF/CTU | +| `files.st` | 是 | 源文件列表(非空数组) | +| `gvl.file` | 否 | 唯一允许 `VAR_GLOBAL` 的文件;已在 `files.st` 则不重复收录 | +| `[[io.input]]` / `[[io.output]]` | 否 | `var` + `channel` + `bit`;`var` 必须已在 GVL 声明 | + +```toml +[project] +name = "line1" +entry = "program MAIN" +cycle_limit = 100000 +dt_ms = 10 + +[files] +st = ["globals.st", "motor.st", "main.st"] + +[gvl] +file = "globals.st" + +[[io.input]] +var = "EmergencyStop" +channel = 0 +bit = 2 +``` + +## ST 子集(第一版) + +**POU**:`PROGRAM` / `FUNCTION` / `FUNCTION_BLOCK`(含对应 `END_*`)。 + +**变量段**:`VAR` / `VAR_INPUT` / `VAR_OUTPUT` / `VAR_GLOBAL`(仅 `gvl.file` 顶层)/ `VAR_EXTERNAL`。 + +**类型**:`BOOL` / `INT` / `TIME`;内建 FB 类型 `TON` / `TOF` / `CTU`(关键字,不可作变量名)。 + +**语句**:赋值 `:=`、`IF / ELSIF / ELSE / END_IF`、`WHILE / END_WHILE`、FB 调用 `fb(in := ..., ...);`、字段读 `fb.Q`。 + +**表达式**:字面量(`TRUE`/`FALSE`、整数、`T#10ms` 式 TIME)、`NOT`、`AND`/`OR`(**短路**)、比较 `= <> < <= > >=`(不连锁)、算术 `+ - * /`、一元负号、函数调用 `Add(3, 4)`。 + +**函数**:结果 = 函数名赋值(`Add := a + b;`);返回类型 `FUNCTION Add : INT`;调用约定:结果 r0、参数 r1..r7(最多 7 个输入,函数内只读)、变量与临时从 r8 起(见 `Doc/compiler/寄存器码.md`)。 + +**限制(v1 明确不做)**:指针 / `REF` / `CLASS` / `ANY` / `VAR_IN_OUT`(语法层直接拒绝)、链式比较、FB 字段赋值、`REAL`、字符串、用户类型套娃、隐式类型转换、函数循环调用(链接层拒绝)、`FUNCTION` 写全局(类型检查层拒绝,含经 `VAR_EXTERNAL`)。 + +## 编译管线与错误类别 + +``` +project.toml + .st + → 词法(lex error)→ 语法(syntax error)→ 链接(link error) + → 类型检查(type error)→ 寄存器码(codegen error)→ .stb +``` + +错误消息前缀即阶段:`lex error` / `syntax error` / `link error` / `type error` / `codegen error`,均带文件与行列(`type error` 仅带文件)。 + +| 类别 | 示例 | +|---|---| +| `lex error` | 非法字符、未闭合注释、坏 `T#` 字面量 | +| `syntax error` | 缺 `END_*`、`VAR_IN_OUT` 等保留名、链式比较 | +| `link error` | 非 GVL 文件写 `VAR_GLOBAL`、同名全局、io.var 未在 GVL、未声明实例、循环调用 | +| `type error` | `FUNCTION` 写全局、AND 吃 INT、赋值类型不匹配、FB 输入类型不匹配 | +| `codegen error` | 寄存器溢出、写 io.input、写函数输入 | + +## 样例与测试 + +- `examples/line1/`:最小闭环(MAIN + MotorStarter FB + I/O + 全局),给人看 +- `tests/cases/01~20/`:20 个用例(每份 `project.toml` + `.st` + `EXPECTED.md`),编译结果按表 +- `tests/`:CTest(`isa_roundtrip` / `compiler_toml` / `lexer_tokens` / `parser_syntax` / `linker_links` / `typecheck_types` / `codegen_slice1`) + +## 当前状态 + +- ✅ 可编译:14 个正例用例全部产出 `.stb`;6 个负例按期望拒绝 +- ⏳ 执行:`BytecodeExecutor`(加载 `.stb` 跑扫描周期)在 12.9/12.10 +- ⏳ sidecar(`.runtime.toml`,I/O 绑定)在 12.10 随执行器一起接线 diff --git a/Doc/索引.md b/Doc/索引.md index 44cd671..74da622 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -8,6 +8,7 @@ doc/ 初步计划.md isa/指令与映像.md compiler/编译管线.md + compiler/STCompiler使用说明.md compiler/词法.md compiler/语法.md compiler/符号表与链接.md @@ -22,6 +23,7 @@ doc/ | [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | +| [`compiler/STCompiler使用说明.md`](compiler/STCompiler使用说明.md) | 构建、用法、工程文件、ST 子集、错误类别 | | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | | [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 | | [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 | diff --git a/compiler/README.md b/compiler/README.md index eebf619..e1fafc1 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -1,5 +1,7 @@ # compiler -ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 `isa`。 +ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 `isa`;可执行 `STCompiler`。 -边界与管线见 [`doc/compiler/编译管线.md`](../doc/compiler/编译管线.md)。语言规则见 [`doc/初步计划.md`](../doc/初步计划.md)。 +- **使用说明**(构建 / 用法 / 工程文件 / ST 子集 / 错误类别):[`doc/compiler/STCompiler使用说明.md`](../doc/compiler/STCompiler使用说明.md) +- 边界与管线:[`doc/compiler/编译管线.md`](../doc/compiler/编译管线.md) +- 语言规则:[`doc/初步计划.md`](../doc/初步计划.md) From 17c62f0a1f31ccc5e1fdcdfbb95f929aba1cc3c4 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 13:40:20 +0800 Subject: [PATCH 35/69] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20.stb=20=E6=98=A0?= =?UTF-8?q?=E5=83=8F=E6=96=87=E4=BB=B6=E7=BB=93=E6=9E=84=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=EF=BC=88line1=20=E7=9C=9F=E5=AE=9E=E5=AD=97=E8=8A=82=E6=8B=86?= =?UTF-8?q?=E8=A7=A3=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Doc/isa/stb文件格式.md:段布局图、17 字段头表、常量/函数/字节码/FB/数据各段 - 用 line1.stb(184 字节)逐段核对实际值:函数表 2 行、MAIN 指令字节、数据段全局+实例块 - 标注与执行器的关系(sidecar 不含在映像);索引与 isa README 登记 --- Doc/isa/stb文件格式.md | 114 +++++++++++++++++++++++++++++++++++++++++ Doc/索引.md | 2 + isa/README.md | 2 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 Doc/isa/stb文件格式.md diff --git a/Doc/isa/stb文件格式.md b/Doc/isa/stb文件格式.md new file mode 100644 index 0000000..7474d44 --- /dev/null +++ b/Doc/isa/stb文件格式.md @@ -0,0 +1,114 @@ +# .stb 映像文件结构 + +编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(184 字节)做真实拆解。 + +## 概览 + +- 魔数 `STSC`,版本 `1`,**全部整数小端** +- 文件 = 头 + 五个段,段序:**常量表 → 函数表 → 字节码 → FB 布局 → 数据** +- 段偏移记在头里;`offset_const ≤ offset_funcs ≤ offset_code ≤ offset_fb ≤ offset_data ≤ 文件长度` + +``` +┌──────────────────────────────────────────────────┐ +│ 头(72 字节,偏移 0) │ +├──────────────────────────────────────────────────┤ +│ 常量表段 offset_const .. offset_funcs │ +├──────────────────────────────────────────────────┤ +│ 函数表段 offset_funcs .. offset_code │ +├──────────────────────────────────────────────────┤ +│ 字节码段 offset_code .. offset_fb │ +├──────────────────────────────────────────────────┤ +│ FB 布局段 offset_fb .. offset_data(v1 为空)│ +├──────────────────────────────────────────────────┤ +│ 数据段 offset_data .. 文件末尾 │ +└──────────────────────────────────────────────────┘ +``` + +## 头(72 字节) + +| 偏移 | 宽 | 字段 | line1 实际值 | +|---|---|---|---| +| 0 | 4 | 魔数 `STSC` | `53 53 54 43` | +| 4 | 4 | 版本 `1` | 1 | +| 8 | 4 | `cycle_limit`(每周期指令上限) | 100000 | +| 12 | 4 | `dt_ms`(本周期 Δt) | 10 | +| 16 | 8 | 工程哈希(FNV-1a 64) | `0xc3fe4ae74ad45900` | +| 24 | 4 | 入口 `entry_fn_id` | 1(MAIN) | +| 28 | 4 | 全局槽数 `n_globals` | 4 | +| 32 | 4 | I 槽数 `n_i` | 0(v1 不拆) | +| 36 | 4 | Q 槽数 `n_q` | 0 | +| 40 | 4 | M 槽数 `n_m` | 0 | +| 44 | 4 | 常量数 `n_consts` | 0 | +| 48 | 4 | 函数数 `n_funcs` | 2 | +| 52 | 4 | `offset_const` | 72 | +| 56 | 4 | `offset_funcs` | 72 | +| 60 | 4 | `offset_code` | 96 | +| 64 | 4 | `offset_fb` | 168 | +| 68 | 4 | `offset_data` | 168 | + +## 常量表段(`offset_const` 起) + +一行 12 字节:`[tag:u32][value:u64]`;`tag`:0=BOOL、1=INT、2=TIME。 + +line1 无字面量常量 → 段为空(`offset_const == offset_funcs`)。 + +## 函数表段(`offset_funcs` 起) + +一行 12 字节:`[nregs:u32][code_offset:u32][code_len:u32]` + +- **行下标即 `fn_id`**;`code_offset` 相对字节码段起点(**字节**);`code_len` 为**指令条数** +- 入口 = 头的 `entry_fn_id` 指向的行 + +line1(2 行): + +| fn_id | nregs | code_offset | code_len | 内容 | +|---|---|---|---|---| +| 0 | 8 | 0 | 1 | `MotorStarter` 占位(内联展开,仅 RET) | +| 1 | 13 | 4 | 17 | `MAIN` | + +## 字节码段(`offset_code` 起) + +指令 4 字节一条:`[op:8 | rd:8 | a:8 | b:8]`(小端 u32)。 + +- `rd` 目的寄存器;`a|b` 视操作码为寄存器、槽号(u16)或跳转偏移(有符号 int16,**相对下一条指令**:目标 = 当前 + 1 + off) +- `CALL` 的 `fn_id` = 函数表行下标 +- 调用约定:结果 r0、参数 r1..r7、变量与临时 r8 起 + +line1 `MAIN` 前 5 条(`0x60` 起): + +| 偏移 | 字节 | 反汇编 | 含义 | +|---|---|---|---| +| 0x60 | `12 08 01 00` | `LOAD_I r8, 1` | I0_0(io.input 绑定 → LOAD_I) | +| 0x64 | `17 08 08 00` | `STORE_GLOBAL r8, 8` | starter.start ← I0_0 | +| 0x68 | `16 09 02 00` | `LOAD_GLOBAL r9, 2` | I0_1 | +| 0x6C | `17 09 09 00` | `STORE_GLOBAL r9, 9` | starter.stop ← I0_1 | +| 0x70 | `16 08 08 00` | `LOAD_GLOBAL r8, 8` | 内联体:读 starter.start | + +## FB 布局段(`offset_fb` 起) + +v1 为空(用户 FB 内联展开,字段偏移已算进数据段;内建 TON/TOF/CTU 布局冻结在编译器中)。行格式约定:`[field_count:u32][field_count × (tag:u32, offset:u32)]`,offset 相对实例基址。 + +## 数据段(`offset_data` 起) + +**全局块**(`n_globals` 个,声明序)→ **FB 实例块**(按 POU 收集序、实例声明序,块间 8 字节对齐)。 + +槽宽与对齐:`BOOL` 1 字节 / `INT` 2 字节 / `TIME` 8 字节,每个槽起点对齐自身宽度。 + +line1 数据段(16 字节,`0xA8` 起): + +| 偏移 | 内容 | +|---|---| +| 0xA8..0xAB | 全局:EmergencyStop、I0_0、I0_1、Q0_0(BOOL,初值 0) | +| 0xAC..0xAF | 填充(实例块 8 对齐) | +| 0xB0..0xB2 | starter 实例:start、stop、q(BOOL) | +| 0xB3..0xB7 | 填充 | + +## 用工具验证 + +- `ctest` 的 `isa_roundtrip` / `codegen_slice1` 覆盖编解码往返与映像校验 +- `isa::ImageView` 提供只读视图(校验魔数/版本/段边界/入口 fn_id) +- 手工检查:`xxd line1.stb` 对照上表逐段核对 + +## 与执行器的关系 + +`BytecodeExecutor`(12.9/12.10 实现)只读本格式:头(`cycle_limit`/`dt_ms`)、函数表(帧 `nregs` + 代码定位)、数据段(全局 + FB 实例状态)。I/O 绑定**不在**映像里,在 sidecar `.runtime.toml`(var → 槽号 → channel/bit)。 diff --git a/Doc/索引.md b/Doc/索引.md index 74da622..e45b0c3 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -7,6 +7,7 @@ doc/ 索引.md 初步计划.md isa/指令与映像.md + isa/stb文件格式.md compiler/编译管线.md compiler/STCompiler使用说明.md compiler/词法.md @@ -22,6 +23,7 @@ doc/ |---|---| | [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | +| [`isa/stb文件格式.md`](isa/stb文件格式.md) | `.stb` 结构说明(用 line1 真实字节拆解) | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | | [`compiler/STCompiler使用说明.md`](compiler/STCompiler使用说明.md) | 构建、用法、工程文件、ST 子集、错误类别 | | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | diff --git a/isa/README.md b/isa/README.md index f17b372..608797c 100644 --- a/isa/README.md +++ b/isa/README.md @@ -9,7 +9,7 @@ STCompiler → compiler → isa BytecodeExecutor → vm → isa ``` -**类型、操作码、指令字、映像布局以** [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md) **为准。** 头文件实现该文,本 README 不另当规范。全工程阶段见 [`doc/初步计划.md`](../doc/初步计划.md) 12.0~12.1。 +**类型、操作码、指令字、映像布局以** [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md) **为准。** 头文件实现该文,本 README 不另当规范。`.stb` 结构面向使用者的拆解见 [`doc/isa/stb文件格式.md`](../doc/isa/stb文件格式.md)。全工程阶段见 [`doc/初步计划.md`](../doc/初步计划.md) 12.0~12.1。 公开头:`isa/include/isa/`。 From b18fd6014cd5719b31d469d04c81908edad4d100 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 13:49:06 +0800 Subject: [PATCH 36/69] =?UTF-8?q?STCompiler=20=E5=8A=A0=20--disasm?= =?UTF-8?q?=EF=BC=9A=E5=8F=8D=E6=B1=87=E7=BC=96=E5=B7=B2=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E7=9A=84=20.stb=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STCompiler .stb --disasm:读映像 → 校验 → 打印段摘要 + 函数表逐条反汇编(文件绝对偏移)+ 常量表 + 数据段 hex - 映像无符号表,按 fn_id/槽号显示;损坏/缺失文件报错退出码 1 - 验证:line1.stb 完整反汇编(17 条 MAIN 指令)与损坏文件拒绝 --- compiler/src/main.cpp | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index e7bccc1..30d0a8f 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -14,16 +14,79 @@ #include "compiler/Linker.h" #include "compiler/Project.h" #include "compiler/Typecheck.h" +#include "isa/Encode.h" #include "isa/Image.h" namespace { void usage() { std::printf("usage: STCompiler [-o .stb]\n" + " STCompiler .stb --disasm\n" " 解析工程并编译(词法 → 语法 → 链接 → 类型 → 寄存器码)\n" " -o .stb 编译并写出映像文件\n" " 无 -o 只打印文件集合与工程哈希(12.3 阶段)\n" + " --disasm 反汇编一个已编译的 .stb 映像\n" " --help 打印本帮助\n"); } + + // 反汇编一个已编译映像(函数表逐条 + 常量表 + 数据段摘要)。 + // 映像无符号表,按 fn_id / 槽号显示;指令偏移为文件绝对字节偏移。 + int dump_image(const char* path) { + std::vector bytes; + std::string err; + if (!isa::read_stb_file(path, &bytes, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + const isa::ImageView v = isa::ImageView::from(bytes); + if (!v.ok()) { + std::fprintf(stderr, "error: %s\n", v.error().c_str()); + return 1; + } + const isa::ImageHeader& h = v.header(); + std::printf("image: %s (%zu bytes, %u functions, %u globals, entry fn %u)\n", + path, bytes.size(), h.n_funcs, h.n_globals, h.entry_fn_id); + std::printf(" dt_ms=%u cycle_limit=%u hash=0x%016llx\n", h.dt_ms, + h.cycle_limit, static_cast(h.project_hash)); + + if (h.n_consts) { + std::printf("constants:\n"); + for (uint32_t i = 0; i < h.n_consts; ++i) { + const isa::ConstEntry c = v.const_entry(i); + const char* tag = c.tag == isa::types::Bool ? "BOOL" + : c.tag == isa::types::Int ? "INT" + : "TIME"; + std::printf(" [%u] %s %llu\n", i, tag, + static_cast(c.value)); + } + } + + std::printf("functions:\n"); + for (uint32_t i = 0; i < h.n_funcs; ++i) { + const isa::FuncRow r = v.func_row(i); + std::printf(" fn %u: nregs=%u, offset=%u, len=%u\n", i, r.nregs, + r.code_offset, r.code_len); + const uint8_t* base = v.code_bytes() + r.code_offset; + for (uint32_t j = 0; j < r.code_len; ++j) { + char buf[64]; + isa::disasm(reinterpret_cast(base)[j], buf, sizeof buf); + std::printf(" 0x%04x %s\n", + h.offset_code + r.code_offset + j * 4, buf); + } + } + + if (v.data_len()) { + std::printf("data (%zu bytes):\n", v.data_len()); + const uint8_t* d = v.data_bytes(); + for (size_t i = 0; i < v.data_len(); i += 16) { + std::printf(" 0x%04x:", static_cast(i)); + for (size_t j = 0; j < 16 && i + j < v.data_len(); ++j) { + std::printf(" %02x", d[i + j]); + } + std::printf("\n"); + } + } + return 0; + } } int main(int argc, char** argv) { @@ -37,6 +100,13 @@ int main(int argc, char** argv) { return 0; } + // --disasm:第一参数是 .stb 路径,不重新编译 + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--disasm") == 0) { + return dump_image(argv[1]); + } + } + const std::string toml_path = argv[1]; std::string out_path; for (int i = 2; i + 1 < argc; ++i) { From ffb64d2063e0a76d7276c4dfc7234a5ef268f177 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 13:55:47 +0800 Subject: [PATCH 37/69] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E8=A1=A5=E5=85=85=20--disasm=20=E5=8F=8D=E6=B1=87=E7=BC=96?= =?UTF-8?q?=E7=94=A8=E6=B3=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/compiler/STCompiler使用说明.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Doc/compiler/STCompiler使用说明.md b/Doc/compiler/STCompiler使用说明.md index f29900e..927e497 100644 --- a/Doc/compiler/STCompiler使用说明.md +++ b/Doc/compiler/STCompiler使用说明.md @@ -16,16 +16,44 @@ ctest --test-dir build/gcc-debug # 测试(当前 9 个用例) ```text STCompiler [-o .stb] +STCompiler .stb --disasm ``` | 参数 | 作用 | |---|---| | `` | 工程文件(必填) | | `-o .stb` | 编译并写出映像文件;**缺省时只打印文件集合与工程哈希**(12.3 阶段行为) | +| `.stb --disasm` | **反汇编一个已编译的映像**(不重新编译,见下) | | `--help` | 打印帮助 | 退出码:`0` 成功;`1` 编译失败(错误信息打到 stderr)。 +## 反汇编(--disasm) + +对已编译的 `.stb` 逐段查看:段摘要、函数表逐条指令(文件绝对偏移)、常量表、数据段 hex。 + +```bash +$ STCompiler line1.stb --disasm +image: line1.stb (184 bytes, 2 functions, 4 globals, entry fn 1) + dt_ms=10 cycle_limit=100000 hash=0xc3fe4ae74ad45900 +functions: + fn 0: nregs=8, offset=0, len=1 + 0x0060 RET + fn 1: nregs=13, offset=4, len=17 + 0x0064 LOAD_I r8, 1 + 0x0068 STORE_GLOBAL r8, 8 + 0x006c LOAD_GLOBAL r9, 2 + ... +data (16 bytes): + 0x0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +``` + +说明: + +- 指令偏移是**文件绝对字节偏移**,可直接对照 `xxd line1.stb` +- 映像里**没有符号名**(函数表只有 fn_id,数据段无名字映射),因此按 `fn 0` / `fn 1`、槽号显示 +- 文件损坏或打不开 → `error: ...` 退出码 1 + ## 示例(line1) ```bash From 3c42a72e866e29762ac777cd42e26d35bec13619 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 19:50:39 +0800 Subject: [PATCH 38/69] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3=EF=BC=88?= =?UTF-8?q?12.9=20=E5=89=8D=E7=BD=AE=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Doc/vm/指令执行.md:执行模型、帧/调用栈(CALL/RET 复制 r0..r7)、值表示约定、 8 字节定宽槽访问、29 条指令执行表、跳转语义、cycle_limit 计数、 CAL_TON/TOF/CTU Δt 推进、五类故障 - line1 MAIN 逐指令推演(I0_0=1、stop=0、急停=0 → Q0_0=1,对照真值表) - 索引登记 --- Doc/vm/指令执行.md | 160 +++++++++++++++++++++++++++++++++++++++++++++ Doc/索引.md | 2 + 2 files changed, 162 insertions(+) create mode 100644 Doc/vm/指令执行.md diff --git a/Doc/vm/指令执行.md b/Doc/vm/指令执行.md new file mode 100644 index 0000000..11b72e8 --- /dev/null +++ b/Doc/vm/指令执行.md @@ -0,0 +1,160 @@ +# 指令执行流程(12.9) + +`vm` 模块的指令执行语义:取指、译码、执行、故障。规范以 [`指令与映像.md`](../isa/指令与映像.md) 为准,本文描述**执行器怎么跑一条指令**。执行模型见 [`初步计划.md`](../初步计划.md) 第 2 节。 + +## 执行模型 + +```text +每个扫描周期(run_cycle): + 1. pc = 0,从入口函数(MAIN)开始逐条执行 + 2. 执行到 MAIN 的 RET → 周期结束 + 3. (I 采样 / Q 写回由 executor 按 sidecar 做,VM 只管数据区) + 4. 定时器在 CAL_* 指令执行时用映像 dt_ms 推进 +``` + +- **pc**:当前指令在函数字节码段内的下标(指令条数单位,从 0 起) +- **取指**:函数表 `func_row(fn_id)` → `code_offset`(字节)→ `code_bytes[code_offset/4 + pc]` 读 u32(小端) +- **译码**:拆 `op / rd / a / b` 四字段,`a|b` 按操作码解释为寄存器对、槽号(u16)、或跳转偏移(int16) + +## 帧与调用栈 + +```cpp +struct Frame { + uint32_t fn_id; // 当前函数 + std::vector regs; // 寄存器文件,大小 = 函数头 nregs + uint32_t ret_pc; // CALL 后应恢复的 pc(仅非 MAIN 帧有意义) + uint32_t ret_fn_id; // 返回目标函数(调用方) +}; +``` + +- 调用栈 `std::vector`,**深度上限 64**;超限 → `StackOverflow` 故障 +- **MAIN 帧跨周期保留**(PROGRAM 变量状态持久);周期结束只清调用栈(保留栈底 MAIN 帧) +- 帧寄存器初值 0 + +### CALL / RET(调用约定,冻结于 12.8) + +```text +CALL fn_id: + 1. 新帧:regs 大小 = callee 的 nregs,全部 0 + 2. 复制当前帧 r0..r7 → 新帧 r0..r7(实参已在调用点 MOVE 进 r1..r7) + 3. 新帧 ret_pc = 当前 pc + 1(CALL 的下一条)、ret_fn_id = 当前函数 + 4. 压栈,pc = 0,fn_id = callee + +RET: + 1. 若当前是 MAIN(栈深 0)→ 周期结束 + 2. 否则:复制当前帧 r0..r7 → 调用方帧 r0..r7(r0 = 结果,r1..r7 原样返回) + 3. 弹栈,pc = ret_pc,fn_id = ret_fn_id +``` + +## 值表示(寄存器 / 槽统一约定) + +| 类型 | 表示 | 说明 | +|---|---|---| +| BOOL | `0` / `1` | 逻辑指令产出、字面量 TRUE=1 | +| INT | `int16_t` 符号扩展 | `sat_*` 返回 int16,赋给 int64 寄存器自动符号扩展 | +| TIME | 全 64 位 | 毫秒 | + +推论:**全宽 int64 比较等价于各类型自身比较**(INT 符号扩展后 -5 < 3 正确)。类型语义由编译期类型检查保证,VM 不查类型。 + +## 数据区访问(8 字节定宽槽,方案 a) + +```text +槽号 slot(指令操作数)→ 数据偏移 = slot × 8 +LOAD_* rd, slot → rd = 读 data[slot*8 .. slot*8+8)(8 字节原样) +STORE_* slot, rs → 写 data[slot*8 .. slot*8+8) = rs(8 字节原样) +``` + +`LOAD_I` / `LOAD_M` / `LOAD_GLOBAL` / `STORE_Q` / `STORE_M` / `STORE_GLOBAL` 执行逻辑完全相同(v1 单一数据区,操作码只作语义标签)。 + +## 指令执行表 + +| 指令 | 流程 | +|---|---| +| `MOVE rd, rs` | `reg[rd] = reg[rs]` | +| `LOADK rd, cid` | 查常量表 `const_entry(cid)`:BOOL → 0/1;INT → `(int16_t)value` 符号扩展;TIME → 全宽 | +| `NOT rd, rs` | `reg[rd] = (reg[rs] == 0) ? 1 : 0` | +| `AND/OR rd, ra, rb` | `reg[rd] = (reg[ra] != 0 && reg[rb] != 0) ? 1 : 0`(或 OR) | +| `ADD/SUB/MUL/DIV rd, ra, rb` | `sat_add((int16)ra, (int16)rb)` 等,结果符号扩展入 rd | +| `CMP_xx rd, ra, rb` | 全宽 int64 比较 → `1` / `0` | +| `JMP off` | `pc += off`(见跳转语义) | +| `JT rd, off` / `JF rd, off` | `reg[rd] != 0`(JT)/ `== 0`(JF)时 `pc += off`,否则正常 +1 | +| `LOAD_* rd, slot` | 见数据区访问 | +| `STORE_* slot, rs` | 见数据区访问 | +| `CAL_TON/TOF/CTU slot` | 见定时器推进 | +| `CALL fn_id` / `RET` | 见帧与调用栈 | + +### 跳转语义 + +```text +指令执行后 pc 先 +1(指向下一条),跳转指令再 pc += off +⇒ 目标下标 = 当前 + 1 + off(与编译端 patch_jump 一致) +``` + +### 周期指令计数 + +- 每条指令执行后 `cycle_count++`;超过映像头 `cycle_limit` → `CycleLimit` 故障(周期中止) +- `JMP` / `JT` / `JF` / `CALL` 只算 1 条(cycle_limit 以"执行的指令数"计) + +## 定时器推进(CAL_TON / CAL_TOF / CAL_CTU) + +`CAL_* slot` 的 `slot` 是实例**基槽号**;字段偏移 = 实例布局字段序号 × 8(内建布局冻结:TON/TOF = in/pt/q/et,CTU = cu/r/pv/q/cv)。每次执行 = 推进一次,Δt = 映像头 `dt_ms`(不读系统时钟): + +```text +CAL_TON: + in = 槽[in] 非零;pt = 槽[pt];et = 槽[et];q = 槽[q] + in 为真:et += dt(et ≥ pt 时停在 pt);q = (et ≥ pt) ? 1 : 0 + in 为假:et = 0;q = 0 + +CAL_TOF: + in 为真:q = 1;et = 0 + in 掉电:et += dt;et ≥ pt 时 q = 0 + (in 再次为真 → 重新 q = 1, et = 0) + +CAL_CTU: + 上升沿检测:本周期 cu 为真且上次 cu 为假 → cv += 1 + (上次 cu 存 VM 侧 vector,按实例基槽索引,跨周期保留) + r 为真 → cv = 0 + q = (cv ≥ pv) ? 1 : 0 +``` + +## 故障 + +| 故障 | 触发 | 周期行为 | +|---|---|---| +| `None` | — | 正常完成 | +| `CycleLimit` | 周期指令数 > `cycle_limit`(用例 6) | 中止,返回故障 | +| `StackOverflow` | 调用栈深度 > 64 | 中止 | +| `BadOp` | op ≥ 29 | 中止 | +| `BadSlot` | slot × 8 + 8 > 数据区长度 | 中止 | +| `BadConst` | const_id ≥ n_consts | 中止 | + +## 示例:line1 MAIN 逐指令推演 + +映像:`dt_ms=10`,`cycle_limit=100000`,数据区 56 字节(7 槽 × 8B,方案 a)。 + +```text +初始:MAIN 帧 regs[13] 全 0;槽 0..6 全 0(初值 0) +假定 executor 已采样:槽 0 = EmergencyStop = 0、槽 1 = I0_0 = 1、槽 2 = I0_1 = 0 + +pc 指令 执行后 +── ───────────────────── ────────────────────────────── +0 LOAD_I r8, 1 r8 = data[8..16) 低字节 = 1(I0_0) +1 STORE_GLOBAL r8, 4 槽 4(starter.start)= 1 +2 LOAD_GLOBAL r9, 2 r9 = 0(I0_1) +3 STORE_GLOBAL r9, 5 槽 5(starter.stop)= 0 +4 LOAD_GLOBAL r8, 8 内联体:r8 = 槽 4 = 1(start) +5 JF r8, +3 r8≠0 不跳;pc = 6 +6 LOAD_GLOBAL r10, 9 r10 = 槽 5 = 0(stop) +7 NOT r9, r10 r9 = 1 +8 MOVE r8, r9 r8 = 1 +9 JF r8, +3 r8≠0 不跳;pc = 10 +10 LOAD_I r12, 0 r12 = 槽 0 = 0(EmergencyStop) +11 NOT r11, r12 r11 = 1 +12 MOVE r8, r11 r8 = 1 +13 STORE_GLOBAL r8, 10 槽 6(starter.q)= 1 +14 LOAD_GLOBAL r8, 10 r8 = 槽 6 = 1 +15 STORE_Q r8, 3 槽 3(Q0_0)= 1 +16 RET 周期结束;executor 读槽 3 → 硬件 +``` + +结果:`Q0_0 = 1`(start=1、stop=0、急停=0 → 电机启动)——与 `EXPECTED.md` 真值表一致。 diff --git a/Doc/索引.md b/Doc/索引.md index e45b0c3..a02bd95 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -16,6 +16,7 @@ doc/ compiler/类型检查.md compiler/寄存器码.md vm/扫描周期.md + vm/指令执行.md executor/执行器入口.md ``` @@ -32,6 +33,7 @@ doc/ | [`compiler/类型检查.md`](compiler/类型检查.md) | 12.7 类型检查:表达式类型、语句规则、错误类别 | | [`compiler/寄存器码.md`](compiler/寄存器码.md) | 12.8 寄存器码:布局、指令模式、内联 FB、8 片计划 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | +| [`vm/指令执行.md`](vm/指令执行.md) | 12.9 指令执行流程:帧/调用栈、指令表、定时器、故障 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | 代码目录里的 README 只作入口;类型、指令、映像以 `isa/指令与映像.md` 为准。 From cd417cd27baf68da5297d268a77ee6d2e05eaf4a Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 19:58:03 +0800 Subject: [PATCH 39/69] =?UTF-8?q?12.9=20=E6=89=A7=E8=A1=8C=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E5=86=99=E8=BF=9B=E6=89=AB=E6=8F=8F=E5=91=A8=E6=9C=9F?= =?UTF-8?q?.md=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 步骤 0:数据段 8 字节定宽槽(方案 a)——文档/codegen/测试三处落地 - 步骤 1~7:Machine.h API、译码 switch、CALL/RET 帧栈与故障、 定时器 Δt 推进、可观察性基础、vm_test(用例 06/17/18/20 + 确定性)、验证提交 - 指令执行语义指向指令执行.md --- Doc/vm/扫描周期.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md index f0d93d3..0af8f6b 100644 --- a/Doc/vm/扫描周期.md +++ b/Doc/vm/扫描周期.md @@ -48,3 +48,74 @@ const Snapshot& snapshot() const; - 寄存器 ≤256、映像很小,快照拷贝成本可忽略。 - 单步、回放、dump 都建立在同一套观察 API 上。 + +--- + +## 12.9 执行计划 + +指令执行语义(帧/调用栈、指令表、定时器、故障、line1 推演)见 [`指令执行.md`](指令执行.md)。 + +### 步骤 0:数据段改 8 字节定宽槽(方案 a,前置) + +VM 需要无歧义地访问数据区,当前变宽布局(BOOL 1B / INT 2B / TIME 8B 对齐)无法从槽号推断宽度。定案:**每槽 8 字节定宽**,指令 slot 语义 = **槽号**(数据偏移 = slot × 8)。 + +- 文档修订:`Doc/isa/指令与映像.md`(数据段规则 + slot 语义)、`Doc/compiler/寄存器码.md`(存储布局,`global_offset` 映射取消)、`Doc/isa/stb文件格式.md`(line1 数据段 16B → 56B) +- `Codegen.cpp` 简化:`width_of`/`align_up` 删除、`layout_data`/`layout_fb_instances` 按槽号布局、指令 slot = 符号表槽号 +- `codegen_test` 断言更新(slot 从字节偏移变槽号、数据区字节数变化) + +### 步骤 1:`Machine.h`(新建 `vm/include/vm/Machine.h`) + +```cpp +namespace vm { + enum class Fault { None, CycleLimit, StackOverflow, BadOp, BadSlot, BadConst }; + + class Machine { + public: + static bool create(const std::vector& image, Machine* out, std::string* err); + Fault run_cycle(); // MAIN pc=0 → RET(MAIN 帧跨周期保留) + // 可观察性(v1 预留,见上节): + bool step(); // 执行一条指令,停在指令边界 + uint32_t pc() / fn_id() / cycle_count() / call_depth(); + uint8_t* data(); // 数据区(I 采样 / Q 读回由外部做) + int64_t reg(uint8_t i); // 当前帧寄存器 + }; +} +``` + +- `Fault` 枚举 + 帧结构 `{ fn_id, regs[nregs], ret_pc, ret_fn_id }`;调用栈深度上限 64 +- 只依赖 `isa`(`ImageView` + 类型/饱和函数) + +### 步骤 2:`Machine.cpp` 译码 switch + +- 取指:函数表 → `code_offset`(字节)→ 读 u32 → 拆 `op/rd/a/b` +- 标量:`MOVE` / `LOADK`(常量表 tag 定值表示)/ `NOT` / `AND` / `OR`(布尔)/ `ADD/SUB/MUL/DIV`(`sat_*`)/ `CMP_xx`(全宽比较) +- 跳转:`JMP` / `JT` / `JF`(`pc += off`,相对下一条语义) +- 数据区:`LOAD_I/LOAD_M/LOAD_GLOBAL`、`STORE_Q/STORE_M/STORE_GLOBAL`(同一实现,slot × 8) + +### 步骤 3:CALL / RET 帧栈 + 故障 + +- `CALL fn_id`:压新帧(复制 r0..r7)→ pc=0;`RET`:复制 r0..r7 回调用方 → 弹栈(MAIN 的 RET = 周期结束) +- `cycle_limit` 超限(用例 6)、栈深 > 64、`BadOp` / `BadSlot` / `BadConst` → 周期中止返回故障 + +### 步骤 4:定时器(CAL_TON / CAL_TOF / CAL_CTU) + +- 执行时用映像 `dt_ms` 推进(每周期一次调用 = 推进一次),不读系统时钟 +- TON:in 真 → et += dt(到 pt 停)、q = et ≥ pt;in 假 → et = 0 +- TOF:in 真 → q=1、et=0;掉电 → et += dt、et ≥ pt → q=0 +- CTU:cu 上升沿(**上次 cu 存 VM 侧 vector**,按实例基槽索引)→ cv+1;r → 复位;q = cv ≥ pv + +### 步骤 5:可观察性基础 + +- `step()` / `pc` / `fn_id` / `cycle_count` / `call_depth` / `reg` 落基础实现;`snapshot()` 留 12.11 补全 + +### 步骤 6:`vm_test`(`tests/src/vm_test.cpp` + CMake,链 `vm` 库) + +- 手工拼小映像(`isa` 编码函数):赋值、跳转、CALL/RET 帧复制、cycle_limit +- 编译器产物全链路:用例 01/02/04/05/07/09/13/15/17/18/20 跑周期断言寄存器与数据区 +- 用例 06:`cycle_limit` 打满 → `CycleLimit` 故障 +- TON:dt=10、pt=30 → 第 3 周期 q=1;CTU:上升沿计数 +- **确定性**:同一 I 序列跑两遍,数据区完全一致 + +### 步骤 7:验证 + 提交 + +- `cmake --build` + `ctest`(新增 `vm_cycles` 后 10/10 全绿);20 用例扫描:14 正例执行通过 From ce948451e02e2cfaf2595811b223231eff64c414 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:01:21 +0800 Subject: [PATCH 40/69] =?UTF-8?q?12.9=20=E6=AD=A5=E9=AA=A4=200=EF=BC=9A?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=AE=B5=E6=94=B9=208=20=E5=AD=97=E8=8A=82?= =?UTF-8?q?=E5=AE=9A=E5=AE=BD=E6=A7=BD=EF=BC=88=E6=96=B9=E6=A1=88=20a?= =?UTF-8?q?=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 指令与映像.md / 寄存器码.md:slot 语义 = 槽号(数据偏移 = 槽号 × 8),取消变宽对齐与 global_offset 映射 - Codegen.cpp:删除 width_of/align_up/global_offsets_;layout_data/layout_fb_instances 按槽号布局(字段 = 槽序号) - stb文件格式.md:line1 真实字节更新(224B、56B 数据段、starter 槽 4/5/6) - codegen_test:断言更新(槽号化 + 数据区字节数),189 断言 9/9 全绿 --- Doc/compiler/寄存器码.md | 6 +- Doc/isa/stb文件格式.md | 35 ++++++----- Doc/isa/指令与映像.md | 4 +- compiler/src/Codegen.cpp | 116 ++++++++++++------------------------- tests/src/codegen_test.cpp | 34 +++++------ 5 files changed, 78 insertions(+), 117 deletions(-) diff --git a/Doc/compiler/寄存器码.md b/Doc/compiler/寄存器码.md index a50d4ed..687f402 100644 --- a/Doc/compiler/寄存器码.md +++ b/Doc/compiler/寄存器码.md @@ -25,9 +25,9 @@ compiler 模块的代码生成。输入:`Project` + `Unit`(AST)+ `LinkResu | FB 实例(任意 POU 内声明) | **数据区实例块**(绝对地址) | 状态跨周期持久 | | 全局(含 I/Q/M) | 数据区(声明序) | 12.6 已定槽号 | -- **数据区** = 全局块(声明序,按对齐规则:BOOL 1B / INT 2B / TIME 8B,起点对齐自身宽度)→ FB 实例块(按实例声明序,各块按类型布局大小、8 字节对齐) -- **指令 slot = 数据区字节偏移**(u16);符号表 `address` 保持逻辑槽号(12.6 冻结),编译时做 槽号 → 偏移 映射 -- 字段访问 `fb.Q`:`LOAD_GLOBAL rd, <实例基址+字段偏移>`(编译期算死);I/Q/M 用 `LOAD_I` / `STORE_Q` / `LOAD_M` / `STORE_M` 同槽号 +- **数据区** = 全局块(声明序,**每槽 8 字节定宽**,见 [`指令与映像.md`](../isa/指令与映像.md))→ FB 实例块(按实例声明序,每字段一槽) +- **指令 slot = 槽号**(u16,0..65535);数据偏移 = 槽号 × 8;符号表 `address` 即指令 slot,无映射 +- 字段访问 `fb.Q`:`LOAD_GLOBAL rd, <实例基槽+字段序号>`(编译期算死);I/Q/M 用 `LOAD_I` / `STORE_Q` / `LOAD_M` / `STORE_M` 同槽号 - 函数帧寄存器:`FUNCTION` 结果固定 **r0**,`VAR_INPUT` 从 r1 起按声明序,`VAR` 续后;PROGRAM / FB 变量从 r0 起按声明序 ### 调用约定(v1 冻结,VM 12.9 按此实现) diff --git a/Doc/isa/stb文件格式.md b/Doc/isa/stb文件格式.md index 7474d44..7a32781 100644 --- a/Doc/isa/stb文件格式.md +++ b/Doc/isa/stb文件格式.md @@ -1,6 +1,6 @@ # .stb 映像文件结构 -编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(184 字节)做真实拆解。 +编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(224 字节)做真实拆解。 ## 概览 @@ -74,15 +74,15 @@ line1(2 行): - `CALL` 的 `fn_id` = 函数表行下标 - 调用约定:结果 r0、参数 r1..r7、变量与临时 r8 起 -line1 `MAIN` 前 5 条(`0x60` 起): +line1 `MAIN` 前 5 条(`0x60` 起,slot 为 8 字节定宽槽号): | 偏移 | 字节 | 反汇编 | 含义 | |---|---|---|---| -| 0x60 | `12 08 01 00` | `LOAD_I r8, 1` | I0_0(io.input 绑定 → LOAD_I) | -| 0x64 | `17 08 08 00` | `STORE_GLOBAL r8, 8` | starter.start ← I0_0 | -| 0x68 | `16 09 02 00` | `LOAD_GLOBAL r9, 2` | I0_1 | -| 0x6C | `17 09 09 00` | `STORE_GLOBAL r9, 9` | starter.stop ← I0_1 | -| 0x70 | `16 08 08 00` | `LOAD_GLOBAL r8, 8` | 内联体:读 starter.start | +| 0x64 | `12 08 01 00` | `LOAD_I r8, 1` | I0_0(io.input 绑定 → LOAD_I,槽 1) | +| 0x68 | `17 08 04 00` | `STORE_GLOBAL r8, 4` | starter.start(槽 4)← I0_0 | +| 0x6C | `16 09 02 00` | `LOAD_GLOBAL r9, 2` | I0_1(槽 2) | +| 0x70 | `17 09 05 00` | `STORE_GLOBAL r9, 5` | starter.stop(槽 5)← I0_1 | +| 0x74 | `16 08 04 00` | `LOAD_GLOBAL r8, 4` | 内联体:读 starter.start(槽 4) | ## FB 布局段(`offset_fb` 起) @@ -90,18 +90,21 @@ v1 为空(用户 FB 内联展开,字段偏移已算进数据段;内建 TON ## 数据段(`offset_data` 起) -**全局块**(`n_globals` 个,声明序)→ **FB 实例块**(按 POU 收集序、实例声明序,块间 8 字节对齐)。 +**每槽 8 字节定宽**(12.9 方案 a):`BOOL` 用低 1 字节、`INT` 用低 2 字节(小端)、`TIME` 全 8 字节。指令 `slot` 操作数即**槽号**,数据偏移 = 槽号 × 8。 -槽宽与对齐:`BOOL` 1 字节 / `INT` 2 字节 / `TIME` 8 字节,每个槽起点对齐自身宽度。 +槽序:**全局块**(`n_globals` 个,声明序)→ **FB 实例块**(POU 收集序、实例声明序,每字段一槽)。 -line1 数据段(16 字节,`0xA8` 起): +line1 数据段(56 字节 = 7 槽 × 8B,`0xA8` 起): -| 偏移 | 内容 | -|---|---| -| 0xA8..0xAB | 全局:EmergencyStop、I0_0、I0_1、Q0_0(BOOL,初值 0) | -| 0xAC..0xAF | 填充(实例块 8 对齐) | -| 0xB0..0xB2 | starter 实例:start、stop、q(BOOL) | -| 0xB3..0xB7 | 填充 | +| 槽号 | 偏移 | 内容 | +|---|---|---| +| 0 | 0xA8 | EmergencyStop(BOOL,初值 0) | +| 1 | 0xB0 | I0_0(BOOL) | +| 2 | 0xB8 | I0_1(BOOL) | +| 3 | 0xC0 | Q0_0(BOOL) | +| 4 | 0xC8 | starter.start(BOOL) | +| 5 | 0xD0 | starter.stop(BOOL) | +| 6 | 0xD8 | starter.q(BOOL) | ## 用工具验证 diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 082d43f..a196a7d 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -76,7 +76,7 @@ C++20 定宽: - 有符号:`TOF` 剩余时间、两个时间点相减会出负值,免去无符号边界处理 - 64 位:为将来 `DATE_AND_TIME`(毫秒时间戳)与 `TON` / `TOF` / `TP` 的 64 位定时器预留;毫秒量程约 2.9 亿年,扫描周期场景永不溢出 -- 代价:槽位 8 字节对齐。**槽位对齐规则**:`BOOL` 1 字节、`INT` 2 字节、`TIME` 8 字节;连续排列按类型对齐,需要时插 padding,偏移以本规则为准。 +- 代价:槽位 8 字节对齐。**槽位规则**(12.9 方案 a):每槽 8 字节定宽,`BOOL` 用低 1 字节、`INT` 用低 2 字节(小端)、`TIME` 全 8 字节;槽号 × 8 = 数据偏移,无 padding。 --- @@ -180,7 +180,7 @@ FB 实例固定布局,字段偏移编译期算死。 ### 数据段 -槽初值按序排:全局(`n_globals`)→ I → Q → M。槽宽与对齐:`BOOL` 1 字节 / `INT` 2 字节 / `TIME` 8 字节,每个槽起点对齐到自身宽度(需要时插 padding)。 +槽初值按序排:全局(`n_globals`,v1 不拆 I/Q/M)→ FB 实例块(每字段一槽)。**每槽 8 字节定宽**(12.9 方案 a):`BOOL` 用低 1 字节、`INT` 用低 2 字节(小端)、`TIME` 全 8 字节。指令 `slot` 操作数即**槽号**,数据偏移 = 槽号 × 8;符号表 `address` 与指令 slot 一致,无映射。 工程哈希:收录源文件路径排序后对内容做 **FNV-1a 64**,只保证回放槽位一致,不是密码学哈希。FNV-1a 64:basis `0xcbf29ce484222325`,prime `0x100000001b3`。 diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 29bb880..9d63b6e 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -7,15 +7,14 @@ * @details 设计说明(详见 Doc/compiler/寄存器码.md): * - 切片 1~6:帧/字面量/MOVE/RET;全局数据区;短路 AND/OR;CMP 与 IF;WHILE 与四则; * FUNCTION 与 CALL(调用约定 r0/r1..r7/r8+)。 - * - 切片 7:FB 实例 → 数据区实例块(跨周期持久);字段地址 = 实例基址 + 字段偏移 + * - 切片 7:FB 实例 → 数据区实例块(跨周期持久);字段槽号 = 实例基槽 + 字段序号 * (编译期算死);字段读写走 LOAD_GLOBAL / STORE_GLOBAL; - * 内建 TON/TOF/CTU → 实参写字段后 CAL_* <实例偏移>; + * 内建 TON/TOF/CTU → 实参写字段后 CAL_* <实例基槽>; * 用户 FB → 调用点内联展开(实参写字段 → FB 体以实例字段为变量编译) - * - 其余构造报 codegen error + * - 方案 a(12.9 前置):数据段**每槽 8 字节定宽**,指令 slot = 槽号(无映射) * * 函数清单: * - put_le32 / put_le64 小端写入映像缓冲 - * - align_up / width_of 对齐与槽宽 * - Builder::Builder (构造)存工程/源文件/链接结果/输出,收集 io 绑定分类 * - Builder::run 布局数据区(全局 + 实例)→ 逐 POU 建函数 → 拼映像 * - Builder::fail 组装 "codegen error: " 返回 false @@ -29,9 +28,10 @@ * - store_target 赋值左值:STORE_* 到全局槽(或内联模式实例字段) * - compile_expr 表达式编译到寄存器(含 Field 字段读) * - cmp_op / arith_op / load_op / store_op 操作码选择 - * - global_offset / instance_of 槽号 → 偏移 / 实例 → 字段地址 + * - global_offset 槽号恒等(方案 a 无映射) + * - instance_of 实例 → 字段槽号表 * - patch_jump 回填跳转偏移(相对下一条指令) - * - layout_data / layout_fb_instances / init_of 数据区布局 + * - layout_data / layout_fb_instances / init_of 数据区布局(8 字节定宽槽) * - const_id 取常量表 id(无则追加) * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + 数据段 * - codegen_project 对外入口 @@ -68,38 +68,6 @@ namespace { } } - /** - * @brief 按 2 的幂宽度向上对齐 - * @param v 当前偏移 - * @param w 对齐宽度(1/2/8) - * @return 对齐后的偏移 - */ - uint32_t align_up(uint32_t v, uint32_t w) { - return (v + w - 1) & ~(w - 1); - } - - /** - * @brief 类型名 → 槽宽(对齐规则见 Doc/isa/指令与映像.md) - * @param name "bool" / "int" / "time" - * @return 1 / 2 / 8;未知返回 1 - */ - uint32_t width_of(const std::string& name) { - if (name == "int") return 2; - if (name == "time") return 8; - return 1; // bool 及未知 - } - - /** - * @brief TypeKind → 槽宽(FB 字段布局用) - * @param k 类型种类 - * @return 1 / 2 / 8;未知返回 1 - */ - uint32_t width_of(TypeKind k) { - if (k == TypeKind::Int) return 2; - if (k == TypeKind::Time) return 8; - return 1; - } - /** * @brief 代码生成器(切片 1) */ @@ -715,58 +683,56 @@ namespace { } /** - * @brief 逻辑槽号 → 数据区字节偏移(layout_data 先行) - * @param slot 全局逻辑槽号(符号表 address) - * @return 数据区字节偏移(u16 可容) + * @brief 逻辑槽号(符号表 address)即指令 slot(方案 a:8 字节定宽,无映射) + * @param slot 全局逻辑槽号 + * @return 同值(u16 可容,layout_data 已校验) */ uint16_t global_offset(uint32_t slot) const { - return static_cast(global_offsets_[slot]); + return static_cast(slot); } /** - * @brief 布局全局数据区:对齐 + 初值字节 - * @details 槽序 = 声明序(12.6);对齐规则 BOOL 1B / INT 2B / TIME 8B, - * 起点对齐自身宽度;初值取自 GVL 声明的 has_init(无则 0) - * @return true 成功;false(槽溢出等,err 已写) + * @brief 布局全局数据区:每槽 8 字节定宽(方案 a) + * @details 槽号 = 符号表 address(声明序,12.6); + * BOOL 用低 1 字节、INT 用低 2 字节(小端)、TIME 全 8 字节; + * 初值取自 GVL 声明的 has_init(无则 0) + * @return true 成功;false(槽号越界,err 已写) */ bool layout_data() { - uint32_t cur = 0; for (const Symbol& s : link_.globals) { - const uint32_t w = width_of(s.type_name); - cur = align_up(cur, w); - if (cur + w > 0xFFFF) { + if (s.address > 0xFFFF) { return fail("data area exceeds slot range"); } - global_offsets_.push_back(cur); bool has_init = false; int64_t init = 0; init_of(s.name, &has_init, &init); - data_.resize(cur + w, 0); - if (s.type_name == "bool") { - data_[cur] = static_cast(has_init ? init : 0); - } else if (s.type_name == "int") { - const uint16_t v = static_cast(has_init ? init : 0); - data_[cur] = static_cast(v & 0xFFu); - data_[cur + 1] = static_cast((v >> 8) & 0xFFu); + const int64_t v = has_init ? init : 0; + const size_t off = static_cast(s.address) * 8; + data_.resize(off + 8, 0); + if (s.type_name == "int") { + const uint16_t iv = static_cast(v); + data_[off] = static_cast(iv & 0xFFu); + data_[off + 1] = static_cast((iv >> 8) & 0xFFu); } else if (s.type_name == "time") { - const uint64_t v = static_cast(has_init ? init : 0); + const uint64_t tv = static_cast(v); for (int i = 0; i < 8; ++i) { - data_[cur + i] = static_cast((v >> (8 * i)) & 0xFFu); + data_[off + i] = static_cast((tv >> (8 * i)) & 0xFFu); } + } else { // bool + data_[off] = v ? 1 : 0; } - cur += w; } return true; } /** * @brief 布局 FB 实例块:数据区全局之后,按 POU 收集序 / 实例声明序 - * @details 每实例:字段按布局段序对齐排布(BOOL 1 / INT 2 / TIME 8), - * 字段地址 = 实例基址 + 字段偏移;实例块间 8 字节对齐,块内容初值 0 + * @details 每实例:字段按布局段序各占一槽(8 字节定宽), + * 字段地址 = 实例基槽 + 字段序号;实例基槽 = 累计槽数 * @return true 成功;false(err 已写) */ bool layout_fb_instances() { - uint32_t cur = align_up(static_cast(data_.size()), 8); + uint32_t cur = static_cast(data_.size() / 8); // 槽号起点 bool any = false; for (const LinkResult::PouScope& sc : link_.scopes) { for (const Symbol& s : sc.syms) { @@ -777,27 +743,20 @@ namespace { if (lay == sc.fb_instances.end()) { return fail("no layout for instance '" + s.name + "'"); } - cur = align_up(cur, 8); InstFields inst; - inst.base = cur; + inst.base = cur; // 实例基槽(CAL_* 操作数) inst.type_name = s.type_name; - uint32_t off = 0; - for (const FbField& fd : lay->second.fields) { - const uint32_t w = width_of(fd.type); - off = align_up(off, w); - inst.field_addr[fd.name] = cur + off; - off += w; + for (size_t i = 0; i < lay->second.fields.size(); ++i) { + inst.field_addr[lay->second.fields[i].name] = cur + i; } - cur += align_up(off, 8); + cur += static_cast(lay->second.fields.size()); instances_[sc.name + "/" + s.name] = inst; any = true; } } // 仅在有实例时把数据区补到实例区终点(无实例则保持全局区原样) if (any) { - while (data_.size() < cur) { - data_.push_back(0); - } + data_.resize(static_cast(cur) * 8, 0); } return true; } @@ -945,9 +904,8 @@ namespace { std::vector consts_; std::map io_input_; // io.input 绑定名(小写) std::map io_output_; // io.output 绑定名(小写) - std::vector global_offsets_; // 逻辑槽号 → 数据区字节偏移 - std::vector data_; // 数据区(全局初值 + FB 实例块) - std::map instances_; // "POU/实例名" → 字段地址 + std::vector data_; // 数据区(8 字节定宽槽:全局 + FB 实例块) + std::map instances_; // "POU/实例名" → 字段槽号 const InstFields* inline_fields_ = nullptr; // 内联 FB 体字段上下文(可空) }; diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index aab5a73..5090cab 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -174,7 +174,7 @@ static bool test_case09() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); CHECK(v.header().n_globals == 1); - CHECK(v.data_len() == 2); // INT 2 字节 + CHECK(v.data_len() == 8); // 8 字节定宽槽 CHECK(v.data_bytes()[0] == 5); // G1 初值 5(小端低位) CHECK(v.data_bytes()[1] == 0); @@ -248,14 +248,14 @@ static bool test_layout() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); CHECK(v.header().n_globals == 3); - CHECK(v.data_len() == 16); // 0 + 2 + 8(对齐)= 16 + CHECK(v.data_len() == 24); // 3 槽 × 8 字节定宽 CHECK(v.data_bytes()[0] == 0); // B 无初值 - CHECK(v.data_bytes()[2] == 0x2C && v.data_bytes()[3] == 0x01); // I = 300 - CHECK(v.data_bytes()[8] == 10); // T = 10ms + CHECK(v.data_bytes()[8] == 0x2C && v.data_bytes()[9] == 0x01); // I = 300(槽 1) + CHECK(v.data_bytes()[16] == 10); // T = 10ms(槽 2) char buf[64]; isa::disasm(reinterpret_cast(v.code_bytes())[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 2") == 0); // x := I(INT 偏移 2) + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 1") == 0); // x := I(槽 1) return true; } @@ -552,7 +552,7 @@ static bool test_case15() { CHECK(v.ok()); CHECK(v.header().n_funcs == 2); // FB 占位 + MAIN CHECK(v.header().entry_fn_id == 1); - CHECK(v.data_len() == 8); // 实例 3B(start@0 stop@1 q@2),8 对齐 + CHECK(v.data_len() == 24); // 实例 3 槽 × 8 字节定宽 const isa::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 12); @@ -602,11 +602,11 @@ static bool test_case17() { isa::disasm(ins[2], buf, sizeof buf); CHECK(std::strcmp(buf, "LOADK r10, 1") == 0); isa::disasm(ins[3], buf, sizeof buf); - CHECK(std::strcmp(buf, "STORE_GLOBAL r10, 8") == 0); // pt(TIME 8 对齐) + CHECK(std::strcmp(buf, "STORE_GLOBAL r10, 1") == 0); // pt(槽 1) isa::disasm(ins[4], buf, sizeof buf); CHECK(std::strcmp(buf, "CAL_TON 0") == 0); isa::disasm(ins[5], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 16") == 0); // t.Q + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 2") == 0); // t.Q(槽 2) return true; } @@ -619,7 +619,7 @@ static bool test_case18() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); - CHECK(v.data_len() == 40); // tf 块 0..31(in@0 pt@8 q@16 et@24)、c 块 32.. + CHECK(v.data_len() == 72); // tf 4 槽 + c 5 槽 = 9 槽 × 8 const isa::FuncRow r = v.func_row(0); CHECK(r.nregs == 14); const uint32_t* ins = reinterpret_cast(v.code_bytes()); @@ -627,11 +627,11 @@ static bool test_case18() { isa::disasm(ins[4], buf, sizeof buf); CHECK(std::strcmp(buf, "CAL_TOF 0") == 0); isa::disasm(ins[12], buf, sizeof buf); - CHECK(std::strcmp(buf, "CAL_CTU 32") == 0); + CHECK(std::strcmp(buf, "CAL_CTU 4") == 0); isa::disasm(ins[13], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r9, 36") == 0); // c.Q + CHECK(std::strcmp(buf, "LOAD_GLOBAL r9, 7") == 0); // c.Q(槽 7) isa::disasm(ins[14], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r10, 38") == 0); // c.CV(INT 2 对齐) + CHECK(std::strcmp(buf, "LOAD_GLOBAL r10, 8") == 0); // c.CV(槽 8) return true; } @@ -660,7 +660,7 @@ static bool test_line1() { CHECK(v.header().n_globals == 4); CHECK(v.header().n_funcs == 2); CHECK(v.header().entry_fn_id == 1); - CHECK(v.data_len() == 16); // 全局 4B + 实例块 8..10(8 对齐) + CHECK(v.data_len() == 56); // 7 槽 × 8 字节定宽 const isa::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 13); @@ -670,15 +670,15 @@ static bool test_line1() { isa::disasm(im[0], buf, sizeof buf); CHECK(std::strcmp(buf, "LOAD_I r8, 1") == 0); // I0_0(io.input) isa::disasm(im[1], buf, sizeof buf); - CHECK(std::strcmp(buf, "STORE_GLOBAL r8, 8") == 0); // starter.start + CHECK(std::strcmp(buf, "STORE_GLOBAL r8, 4") == 0); // starter.start(槽 4) isa::disasm(im[2], buf, sizeof buf); CHECK(std::strcmp(buf, "LOAD_GLOBAL r9, 2") == 0); // I0_1 isa::disasm(im[3], buf, sizeof buf); - CHECK(std::strcmp(buf, "STORE_GLOBAL r9, 9") == 0); // starter.stop + CHECK(std::strcmp(buf, "STORE_GLOBAL r9, 5") == 0); // starter.stop(槽 5) isa::disasm(im[13], buf, sizeof buf); - CHECK(std::strcmp(buf, "STORE_GLOBAL r8, 10") == 0); // starter.Q + CHECK(std::strcmp(buf, "STORE_GLOBAL r8, 6") == 0); // starter.Q(槽 6) isa::disasm(im[14], buf, sizeof buf); - CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 10") == 0); // starter.Q 读回 + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 6") == 0); // starter.Q 读回 isa::disasm(im[15], buf, sizeof buf); CHECK(std::strcmp(buf, "STORE_Q r8, 3") == 0); // Q0_0(io.output) isa::disasm(im[16], buf, sizeof buf); From 815eb87a464a54745415864a6106980532a6a349 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:12:43 +0800 Subject: [PATCH 41/69] =?UTF-8?q?12.9=20=E6=AD=A5=E9=AA=A4=201=EF=BC=9AVM?= =?UTF-8?q?=20=E5=A4=B4=E6=96=87=E4=BB=B6=EF=BC=88API=E3=80=81Fault=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E3=80=81=E5=B8=A7=E7=BB=93=E6=9E=84=EF=BC=89?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Machine.h:create/run_cycle + 可观察性(step/pc/fn_id/cycle_count/call_depth/data/reg) - Fault:None/CycleLimit/StackOverflow/BadOp/BadSlot/BadConst - 帧 { fn_id, regs[nregs], ret_pc, ret_fn_id },调用栈深度 64,MAIN 帧跨周期保留 - ctu_prev_ 预留(步骤 4);exec_one/do_call/do_ret/do_cal 桩 --- vm/include/vm/Machine.h | 111 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 vm/include/vm/Machine.h diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h new file mode 100644 index 0000000..ee3a238 --- /dev/null +++ b/vm/include/vm/Machine.h @@ -0,0 +1,111 @@ +/** + * @file Machine.h + * @brief 寄存器虚拟机(12.9) + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/vm/扫描周期.md 与 Doc/vm/指令执行.md): + * - 只认 isa 映像(ImageView);无 GC、无堆、无线程 + * - 扫描周期:MAIN pc=0 → RET;MAIN 帧跨周期保留,调用栈深度上限 64 + * - 帧:{ fn_id, regs[nregs], ret_pc, ret_fn_id };CALL 压帧复制 r0..r7, + * RET 复制回调用方(r0=结果) + * - 数据区:8 字节定宽槽(方案 a),指令 slot = 槽号,偏移 = slot × 8 + * - 可观察性(v1 预留):step / pc / fn_id / cycle_count / call_depth / reg + */ + +#pragma once + +#include +#include +#include + +#include "isa/Image.h" + +namespace vm { + + // 周期/指令故障 + enum class Fault { + None, // 正常 + CycleLimit, // 本周期指令数超过映像头 cycle_limit(用例 6) + StackOverflow, // 调用栈深度超过 64 + BadOp, // 非法操作码(op ≥ 29) + BadSlot, // slot × 8 + 8 越出数据区 + BadConst, // const_id 越出常量表 + }; + + class Machine { + public: + /** + * @brief 从映像字节构建机器(校验 + 拷贝数据段 + 建 MAIN 帧) + * @param image .stb 映像字节 + * @param out 输出 Machine(未初始化) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(映像非法,err 已写) + */ + static bool create(const std::vector& image, Machine* out, + std::string* err); + + /** + * @brief 跑一个扫描周期:MAIN pc=0 执行到 RET + * @return 故障(None = 正常完成);调用栈清空但 MAIN 帧保留 + */ + Fault run_cycle(); + + // ---- 可观察性(v1 预留,12.11 供单步/回放/TUI)---- + + /** + * @brief 执行一条指令,停在指令边界 + * @return true 继续;false 周期结束(MAIN 的 RET)或发生故障 + */ + bool step(); + + /** @brief 最近一次故障(step 返回 false 后查询) */ + Fault fault() const; + + /** @brief 当前指令下标(相对当前函数字节码段) */ + uint32_t pc() const; + + /** @brief 当前函数 fn_id */ + uint32_t fn_id() const; + + /** @brief 本周期已执行指令数 */ + uint32_t cycle_count() const; + + /** @brief 调用栈深度(MAIN 帧 = 1) */ + uint32_t call_depth() const; + + /** @brief 数据区(I 采样写入 / Q 读回由外部做;8 字节定宽槽) */ + uint8_t* data(); + const uint8_t* data() const; + + /** @brief 数据区字节数 */ + size_t data_len() const; + + /** @brief 当前帧寄存器值(i < nregs) */ + int64_t reg(uint8_t i) const; + + private: + struct Frame { + uint32_t fn_id = 0; // 本帧函数 + std::vector regs; // 寄存器文件(大小 = 函数头 nregs) + uint32_t ret_pc = 0; // CALL 的下一条(非 MAIN 帧有意义) + uint32_t ret_fn_id = 0; // 返回目标函数 + }; + + isa::ImageView image_; // 映像只读视图 + std::vector data_; // 数据区工作副本(8 字节定宽槽) + std::vector frames_; // 调用栈([0] = MAIN,跨周期保留) + std::vector ctu_prev_; // CTU 上次 cu(按实例基槽索引,12.9 步骤 4) + uint32_t pc_ = 0; // 当前 pc(相对本帧函数字节码段) + uint32_t cycle_count_ = 0; // 本周期指令数 + Fault fault_ = Fault::None; // 最近一次故障 + bool ended_ = false; // step 后周期是否已结束 + + Frame& cur_frame(); + const Frame& cur_frame() const; + bool exec_one(); // 译码执行一条(12.9 步骤 2/3/4 填充) + Fault do_call(uint32_t fn_id); // 压帧;BadFn 用 BadOp 表示 + Fault do_ret(); // 弹帧 / 周期结束 + Fault do_cal(uint8_t op, uint16_t slot); // TON/TOF/CTU(12.9 步骤 4) + }; +} From efe4dd1a2c88fb6d2d1c120a27a43b603860fc5d Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:16:28 +0800 Subject: [PATCH 42/69] =?UTF-8?q?12.9=20=E6=AD=A5=E9=AA=A4=202=EF=BC=9AVM?= =?UTF-8?q?=20=E8=AF=91=E7=A0=81=20switch=EF=BC=88=E6=A0=87=E9=87=8F/?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC/LOAD-STORE=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Machine.cpp:create(拷贝数据段+建 MAIN 帧)、run_cycle、step、exec_one - 标量:MOVE/LOADK(常量表 tag 定值表示)/NOT/AND/OR/四则饱和/CMP 全宽比较 - 跳转:JMP/JT/JF(相对下一条);LOAD/STORE:slot×8 定宽槽,BadSlot 校验 - 寄存器越界检查按指令形态(a|b 作偏移/槽号/常量 id 时不查) - 修:ImageView 默认构造公开(无效态)、Codegen 两个编译警告 - 冒烟:用例 02/04/07/05 跑通、06 触发 CycleLimit、step 单步 --- compiler/src/Codegen.cpp | 5 +- isa/include/isa/Image.h | 4 +- vm/src/Machine.cpp | 258 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 260 insertions(+), 7 deletions(-) diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 9d63b6e..4e911bf 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -242,9 +242,6 @@ namespace { if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { continue; } - if (r > 255) { - return fail("register overflow in function '" + pou.name + "'"); - } f->regs[d.name] = r++; } } @@ -660,8 +657,8 @@ namespace { case BinOp::Le: return isa::Op::CMP_LE; case BinOp::Gt: return isa::Op::CMP_GT; case BinOp::Ge: return isa::Op::CMP_GE; + default: return isa::Op::CMP_EQ; // Add/Sub/Mul/Div 不经此函数 } - return isa::Op::CMP_EQ; } /** diff --git a/isa/include/isa/Image.h b/isa/include/isa/Image.h index d1f1289..fd1cc47 100644 --- a/isa/include/isa/Image.h +++ b/isa/include/isa/Image.h @@ -77,6 +77,8 @@ namespace isa { // 只读视图:校验过的映像 class ImageView { public: + // 默认构造为无效态(ok() == false,error() == "uninitialized") + ImageView(); static ImageView from(const uint8_t* buf, size_t len); static ImageView from(const std::vector& buf); @@ -96,7 +98,7 @@ namespace isa { size_t data_len() const; // 字节数 private: - ImageView(); + // 私有成员与内部构造辅助(from 使用) const uint8_t* buf_; size_t len_; diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index c337c8e..e81f9b5 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -1,8 +1,262 @@ /** * @file Machine.cpp - * @brief 寄存器虚拟机(12.9 实现) - * @date 2026-08-19 + * @brief 寄存器虚拟机(12.9,步骤 2:译码 switch) + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/vm/指令执行.md): + * - 步骤 2 范围:create / run_cycle / step + 标量指令(MOVE/LOADK/NOT/AND/OR/算术/CMP)、 + * 跳转(JMP/JT/JF)、LOAD/STORE(slot × 8);RET 仅处理 MAIN(周期结束) + * - 步骤 3 补 CALL/RET 帧栈与栈深/坏操作码故障;步骤 4 补 CAL_* 定时器 + * - 取指:函数表 → code_offset(字节)→ u32;执行后 pc 先 +1,跳转再 pc += off + * - 数据区 8 字节定宽槽:偏移 = slot × 8 */ +#include "vm/Machine.h" + +#include "isa/Encode.h" +#include "isa/Instr.h" +#include "isa/Op.h" +#include "isa/Types.h" + namespace vm { + +bool Machine::create(const std::vector& image, Machine* out, + std::string* err) { + out->image_ = isa::ImageView::from(image); + if (!out->image_.ok()) { + if (err) { + *err = out->image_.error(); + } + return false; + } + // 数据区工作副本(8 字节定宽槽) + out->data_.assign(out->image_.data_bytes(), + out->image_.data_bytes() + out->image_.data_len()); + out->ctu_prev_.assign(out->data_len() / 8, 0); + + // MAIN 帧(跨周期保留) + const uint32_t entry = out->image_.header().entry_fn_id; + const isa::FuncRow main = out->image_.func_row(entry); + out->frames_.clear(); + Frame f; + f.fn_id = entry; + f.regs.assign(main.nregs, 0); + out->frames_.push_back(std::move(f)); + + out->pc_ = 0; + out->cycle_count_ = 0; + out->fault_ = Fault::None; + out->ended_ = false; + return true; } + +Fault Machine::run_cycle() { + cycle_count_ = 0; + ended_ = false; + fault_ = Fault::None; + pc_ = 0; + while (!ended_ && fault_ == Fault::None) { + if (!exec_one()) { + break; + } + } + // 周期结束:清调用栈(保留 MAIN 帧,PROGRAM 变量状态跨周期) + while (frames_.size() > 1) { + frames_.pop_back(); + } + return fault_; +} + +bool Machine::step() { + if (ended_ || fault_ != Fault::None) { + return false; + } + return exec_one(); +} + +Fault Machine::fault() const { return fault_; } +uint32_t Machine::pc() const { return pc_; } +uint32_t Machine::fn_id() const { return cur_frame().fn_id; } +uint32_t Machine::cycle_count() const { return cycle_count_; } +uint32_t Machine::call_depth() const { return static_cast(frames_.size()); } +uint8_t* Machine::data() { return data_.data(); } +const uint8_t* Machine::data() const { return data_.data(); } +size_t Machine::data_len() const { return data_.size(); } +int64_t Machine::reg(uint8_t i) const { return cur_frame().regs[i]; } + +Machine::Frame& Machine::cur_frame() { return frames_.back(); } +const Machine::Frame& Machine::cur_frame() const { return frames_.back(); } + +bool Machine::exec_one() { + // ---- 取指 ---- + const isa::FuncRow row = image_.func_row(cur_frame().fn_id); + if (pc_ >= row.code_len) { + fault_ = Fault::BadOp; + return false; + } + const uint8_t* base = image_.code_bytes() + row.code_offset; + const isa::Instr w = reinterpret_cast(base)[pc_]; + + // ---- 周期指令计数 ---- + ++cycle_count_; + if (cycle_count_ > image_.header().cycle_limit) { + fault_ = Fault::CycleLimit; + return false; + } + + // 先指向下一条(跳转指令再按"相对下一条"语义叠加偏移) + ++pc_; + + const isa::Op op = isa::op(w); + const uint8_t rd = isa::rd(w); + const uint8_t ra = isa::a(w); + const uint8_t rb = isa::b(w); + std::vector& regs = cur_frame().regs; + + // 寄存器越界检查(仅对把字段当寄存器的指令;a|b 作偏移/槽号/常量 id 时跳过) + const bool uses_rd = op == isa::Op::MOVE || op == isa::Op::NOT || op == isa::Op::AND || + op == isa::Op::OR || op == isa::Op::ADD || op == isa::Op::SUB || + op == isa::Op::MUL || op == isa::Op::DIV || op == isa::Op::CMP_EQ || + op == isa::Op::CMP_NE || op == isa::Op::CMP_LT || + op == isa::Op::CMP_LE || op == isa::Op::CMP_GT || + op == isa::Op::CMP_GE || op == isa::Op::JT || op == isa::Op::JF || + op == isa::Op::LOADK || op == isa::Op::LOAD_I || + op == isa::Op::LOAD_M || op == isa::Op::LOAD_GLOBAL || + op == isa::Op::STORE_Q || op == isa::Op::STORE_M || + op == isa::Op::STORE_GLOBAL; + const bool uses_ra = op == isa::Op::MOVE || op == isa::Op::NOT || op == isa::Op::AND || + op == isa::Op::OR || op == isa::Op::ADD || op == isa::Op::SUB || + op == isa::Op::MUL || op == isa::Op::DIV || op == isa::Op::CMP_EQ || + op == isa::Op::CMP_NE || op == isa::Op::CMP_LT || + op == isa::Op::CMP_LE || op == isa::Op::CMP_GT || + op == isa::Op::CMP_GE; + const bool uses_rb = uses_ra; + if ((uses_rd && rd >= regs.size()) || (uses_ra && ra >= regs.size()) || + (uses_rb && rb >= regs.size())) { + fault_ = Fault::BadOp; + return false; + } + + switch (op) { + case isa::Op::MOVE: + regs[rd] = regs[ra]; + return true; + case isa::Op::LOADK: { + const uint16_t cid = isa::imm16(w); + if (cid >= image_.header().n_consts) { + fault_ = Fault::BadConst; + return false; + } + const isa::ConstEntry c = image_.const_entry(cid); + if (c.tag == isa::types::Bool) { + regs[rd] = c.value ? 1 : 0; + } else if (c.tag == isa::types::Int) { + regs[rd] = static_cast(c.value); // 符号扩展 + } else { + regs[rd] = static_cast(c.value); + } + return true; + } + case isa::Op::NOT: + regs[rd] = (regs[ra] == 0) ? 1 : 0; + return true; + case isa::Op::AND: + regs[rd] = (regs[ra] != 0 && regs[rb] != 0) ? 1 : 0; + return true; + case isa::Op::OR: + regs[rd] = (regs[ra] != 0 || regs[rb] != 0) ? 1 : 0; + return true; + case isa::Op::ADD: + regs[rd] = isa::types::sat_add(static_cast(regs[ra]), + static_cast(regs[rb])); + return true; + case isa::Op::SUB: + regs[rd] = isa::types::sat_sub(static_cast(regs[ra]), + static_cast(regs[rb])); + return true; + case isa::Op::MUL: + regs[rd] = isa::types::sat_mul(static_cast(regs[ra]), + static_cast(regs[rb])); + return true; + case isa::Op::DIV: + regs[rd] = isa::types::sat_div(static_cast(regs[ra]), + static_cast(regs[rb])); + return true; + case isa::Op::CMP_EQ: + case isa::Op::CMP_NE: + case isa::Op::CMP_LT: + case isa::Op::CMP_LE: + case isa::Op::CMP_GT: + case isa::Op::CMP_GE: { + const int64_t l = regs[ra]; + const int64_t r = regs[rb]; + bool res = false; + switch (op) { + case isa::Op::CMP_EQ: res = (l == r); break; + case isa::Op::CMP_NE: res = (l != r); break; + case isa::Op::CMP_LT: res = (l < r); break; + case isa::Op::CMP_LE: res = (l <= r); break; + case isa::Op::CMP_GT: res = (l > r); break; + case isa::Op::CMP_GE: res = (l >= r); break; + default: break; + } + regs[rd] = res ? 1 : 0; + return true; + } + case isa::Op::JMP: + pc_ += isa::off16(w); + return true; + case isa::Op::JT: + if (regs[rd] != 0) { + pc_ += isa::off16(w); + } + return true; + case isa::Op::JF: + if (regs[rd] == 0) { + pc_ += isa::off16(w); + } + return true; + case isa::Op::LOAD_I: + case isa::Op::LOAD_M: + case isa::Op::LOAD_GLOBAL: { + const uint16_t slot = isa::imm16(w); + const size_t off = static_cast(slot) * 8; + if (off + 8 > data_.size()) { + fault_ = Fault::BadSlot; + return false; + } + int64_t v = 0; + for (int i = 0; i < 8; ++i) { + v |= static_cast(data_[off + i]) << (8 * i); + } + regs[rd] = v; + return true; + } + case isa::Op::STORE_Q: + case isa::Op::STORE_M: + case isa::Op::STORE_GLOBAL: { + const uint16_t slot = isa::imm16(w); + const size_t off = static_cast(slot) * 8; + if (off + 8 > data_.size()) { + fault_ = Fault::BadSlot; + return false; + } + const int64_t v = regs[rd]; + for (int i = 0; i < 8; ++i) { + data_[off + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + return true; + } + case isa::Op::RET: + // MAIN 的 RET = 周期结束(步骤 3 处理非 MAIN 帧返回) + ended_ = true; + return false; + default: + // CALL / CAL_TON / CAL_TOF / CAL_CTU(步骤 3 / 4 实现) + fault_ = Fault::BadOp; + return false; + } +} + +} // namespace vm From a84a1a62ce9f69641c3ec62d6ea4ce92fd1d838c Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:17:55 +0800 Subject: [PATCH 43/69] =?UTF-8?q?12.9=20=E6=AD=A5=E9=AA=A4=203=EF=BC=9ACAL?= =?UTF-8?q?L/RET=20=E5=B8=A7=E6=A0=88=E4=B8=8E=E6=95=85=E9=9A=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - do_call:压新帧(nregs 分配、复制 r0..r7、记 ret_pc/ret_fn_id)→ pc=0;fn_id 越界 BadOp、深度≥64 StackOverflow - do_ret:复制 r0..r7 回调用方(r0=结果)→ 弹栈 → pc=ret_pc;MAIN 的 RET = 周期结束 - 验证:用例 13 x=Add(3,4)=7、双周期帧保留;手拼自调用映像触发 StackOverflow(深度 64 后清栈) --- vm/src/Machine.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index e81f9b5..8dd0167 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -14,6 +14,8 @@ #include "vm/Machine.h" +#include + #include "isa/Encode.h" #include "isa/Instr.h" #include "isa/Op.h" @@ -248,15 +250,60 @@ bool Machine::exec_one() { } return true; } + case isa::Op::CALL: + fault_ = do_call(isa::imm16(w)); + return fault_ == Fault::None; case isa::Op::RET: - // MAIN 的 RET = 周期结束(步骤 3 处理非 MAIN 帧返回) - ended_ = true; - return false; + fault_ = do_ret(); + if (fault_ != Fault::None) { + return false; + } + return !ended_; // MAIN 的 RET → 周期结束(返回 false) default: - // CALL / CAL_TON / CAL_TOF / CAL_CTU(步骤 3 / 4 实现) + // CAL_TON / CAL_TOF / CAL_CTU(步骤 4 实现) fault_ = Fault::BadOp; return false; } } +Fault Machine::do_call(uint32_t fn_id) { + if (fn_id >= image_.header().n_funcs) { + return Fault::BadOp; + } + if (frames_.size() >= 64) { + return Fault::StackOverflow; + } + const isa::FuncRow row = image_.func_row(fn_id); + Frame f; + f.fn_id = fn_id; + f.regs.assign(row.nregs, 0); + f.ret_pc = pc_; // 已指向 CALL 的下一条 + f.ret_fn_id = cur_frame().fn_id; + // 调用约定:复制当前帧 r0..r7 → 新帧(实参已由调用点 MOVE 进 r1..r7) + const size_t n = std::min(8, std::min(cur_frame().regs.size(), f.regs.size())); + for (size_t i = 0; i < n; ++i) { + f.regs[i] = cur_frame().regs[i]; + } + frames_.push_back(std::move(f)); + pc_ = 0; + return Fault::None; +} + +Fault Machine::do_ret() { + if (frames_.size() <= 1) { + ended_ = true; // MAIN 的 RET:周期结束 + return Fault::None; + } + Frame& cur = cur_frame(); + Frame& caller = frames_[frames_.size() - 2]; + // 调用约定:复制当前帧 r0..r7 → 调用方(r0 = 结果,r1..r7 原样返回) + const size_t n = std::min(8, std::min(cur.regs.size(), caller.regs.size())); + for (size_t i = 0; i < n; ++i) { + caller.regs[i] = cur.regs[i]; + } + pc_ = cur.ret_pc; + frames_.pop_back(); + return Fault::None; +} + } // namespace vm From ada9e5f99ea2aa6fa5f4e59a6a86217854b7fcde Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:20:26 +0800 Subject: [PATCH 44/69] =?UTF-8?q?12.9=20=E6=AD=A5=E9=AA=A4=204=EF=BC=9A?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E5=99=A8=20CAL=5FTON/CAL=5FTOF/CAL=5FCTU?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - do_cal:内建布局冻结(TON/TOF: in/pt/q/et、CTU: cu/r/pv/q/cv,字段=槽序号×8) - TON:in 真 → et+=dt(到 pt 停)、q=et≥pt;假 → 清零 - TOF:in 真 → q=1/et=0;掉电 → et+=dt、et≥pt → q=0 - CTU:cu 上升沿(ctu_prev_ 跨周期)→ cv+1;r 复位;q=cv≥pv - 抽 slot_get/slot_set 复用(exec_one 的 LOAD/STORE 同步重构) - 验证:TON 3 周期到 q=1、TOF 掉电延时 2 周期、CTU 交替上升沿到 cv=3、复位;line1 三组 I 组合 Q 正确 + 确定性两遍一致 --- vm/include/vm/Machine.h | 4 ++ vm/src/Machine.cpp | 119 +++++++++++++++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h index ee3a238..263ea2b 100644 --- a/vm/include/vm/Machine.h +++ b/vm/include/vm/Machine.h @@ -107,5 +107,9 @@ namespace vm { Fault do_call(uint32_t fn_id); // 压帧;BadFn 用 BadOp 表示 Fault do_ret(); // 弹帧 / 周期结束 Fault do_cal(uint8_t op, uint16_t slot); // TON/TOF/CTU(12.9 步骤 4) + + // 数据区槽读写(8 字节定宽;越界返回 false) + bool slot_get(uint16_t slot, int64_t* out) const; + bool slot_set(uint16_t slot, int64_t v); }; } diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index 8dd0167..216b072 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -223,31 +223,20 @@ bool Machine::exec_one() { case isa::Op::LOAD_M: case isa::Op::LOAD_GLOBAL: { const uint16_t slot = isa::imm16(w); - const size_t off = static_cast(slot) * 8; - if (off + 8 > data_.size()) { + if (!slot_get(slot, ®s[rd])) { fault_ = Fault::BadSlot; return false; } - int64_t v = 0; - for (int i = 0; i < 8; ++i) { - v |= static_cast(data_[off + i]) << (8 * i); - } - regs[rd] = v; return true; } case isa::Op::STORE_Q: case isa::Op::STORE_M: case isa::Op::STORE_GLOBAL: { const uint16_t slot = isa::imm16(w); - const size_t off = static_cast(slot) * 8; - if (off + 8 > data_.size()) { + if (!slot_set(slot, regs[rd])) { fault_ = Fault::BadSlot; return false; } - const int64_t v = regs[rd]; - for (int i = 0; i < 8; ++i) { - data_[off + i] = static_cast((v >> (8 * i)) & 0xFFu); - } return true; } case isa::Op::CALL: @@ -259,13 +248,41 @@ bool Machine::exec_one() { return false; } return !ended_; // MAIN 的 RET → 周期结束(返回 false) + case isa::Op::CAL_TON: + case isa::Op::CAL_TOF: + case isa::Op::CAL_CTU: + fault_ = do_cal(static_cast(op), isa::imm16(w)); + return fault_ == Fault::None; default: - // CAL_TON / CAL_TOF / CAL_CTU(步骤 4 实现) fault_ = Fault::BadOp; return false; } } +bool Machine::slot_get(uint16_t slot, int64_t* out) const { + const size_t off = static_cast(slot) * 8; + if (off + 8 > data_.size()) { + return false; + } + int64_t v = 0; + for (int i = 0; i < 8; ++i) { + v |= static_cast(data_[off + i]) << (8 * i); + } + *out = v; + return true; +} + +bool Machine::slot_set(uint16_t slot, int64_t v) { + const size_t off = static_cast(slot) * 8; + if (off + 8 > data_.size()) { + return false; + } + for (int i = 0; i < 8; ++i) { + data_[off + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + return true; +} + Fault Machine::do_call(uint32_t fn_id) { if (fn_id >= image_.header().n_funcs) { return Fault::BadOp; @@ -306,4 +323,78 @@ Fault Machine::do_ret() { return Fault::None; } +Fault Machine::do_cal(uint8_t op, uint16_t slot) { + // 内建 FB 布局冻结(Doc/compiler/符号表与链接.md): + // TON/TOF:in/pt/q/et(字段 0/1/2/3) + // CTU:cu/r/pv/q/cv(字段 0/1/2/3/4) + // 实例槽号从 slot(基槽)起;字段偏移 = 字段序号 × 8(8 字节定宽) + const int64_t dt = image_.header().dt_ms; + int64_t v = 0; + + if (op == static_cast(isa::Op::CAL_TON) || + op == static_cast(isa::Op::CAL_TOF)) { + // 字段:in=0 pt=1 q=2 et=3 + if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; + const bool in = v != 0; + if (!slot_get(static_cast(slot + 1), &v)) return Fault::BadSlot; + const int64_t pt = v; + int64_t et = 0; + if (!slot_get(static_cast(slot + 3), &et)) return Fault::BadSlot; + int64_t q = 0; + if (op == static_cast(isa::Op::CAL_TON)) { + // TON:in 真 → et += dt(到 pt 停)、q = et ≥ pt;in 假 → et = 0、q = 0 + if (in) { + et += dt; + if (pt > 0 && et >= pt) { + et = pt; + } + q = (et >= pt) ? 1 : 0; + } else { + et = 0; + q = 0; + } + } else { + // TOF:in 真 → q = 1、et = 0;掉电 → et += dt、et ≥ pt → q = 0 + if (in) { + et = 0; + q = 1; + } else { + et += dt; + q = (pt > 0 && et >= pt) ? 0 : 1; + } + } + if (!slot_set(static_cast(slot + 3), et)) return Fault::BadSlot; + if (!slot_set(static_cast(slot + 2), q)) return Fault::BadSlot; + return Fault::None; + } + + if (op == static_cast(isa::Op::CAL_CTU)) { + // 字段:cu=0 r=1 pv=2 q=3 cv=4 + if (slot >= ctu_prev_.size()) return Fault::BadSlot; + if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; + const bool cu = v != 0; + if (!slot_get(static_cast(slot + 1), &v)) return Fault::BadSlot; + const bool r = v != 0; + if (!slot_get(static_cast(slot + 2), &v)) return Fault::BadSlot; + const int64_t pv = v; + int64_t cv = 0; + if (!slot_get(static_cast(slot + 4), &cv)) return Fault::BadSlot; + // 上升沿:本周期 cu 真且上次 cu 假 → cv + 1;r → 复位 + const bool prev = ctu_prev_[slot] != 0; + if (r) { + cv = 0; + } else if (cu && !prev) { + cv += 1; + } + ctu_prev_[slot] = cu ? 1 : 0; // 记录本次 cu(跨周期) + if (!slot_set(static_cast(slot + 4), cv)) return Fault::BadSlot; + if (!slot_set(static_cast(slot + 3), (cv >= pv) ? 1 : 0)) { + return Fault::BadSlot; + } + return Fault::None; + } + + return Fault::BadOp; +} + } // namespace vm From 8c0fc4fe98fc4a93387fbac18f2c95b11c6a08ed Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:22:36 +0800 Subject: [PATCH 45/69] =?UTF-8?q?12.9=20=E6=AD=A5=E9=AA=A4=206=EF=BC=9Avm?= =?UTF-8?q?=5Ftest=20=E5=9B=BA=E5=8C=96=EF=BC=88=E6=89=8B=E5=B7=A5?= =?UTF-8?q?=E6=98=A0=E5=83=8F=20+=20=E7=BC=96=E8=AF=91=E5=99=A8=E4=BA=A7?= =?UTF-8?q?=E7=89=A9=20+=20=E7=A1=AE=E5=AE=9A=E6=80=A7=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 手工映像:标量/跳转/数据区、CALL/RET 帧复制(r1/r2 实参 → r0 结果)、自调用 StackOverflow、越界槽 BadSlot、坏常量 BadConst - 编译器产物:用例 01~09/13/15/17/18/20 跑周期断言(TON 第 3 周期 q=1、line1 真值表 4 组) - 用例 06 CycleLimit;确定性:同一 I 序列两遍数据区逐字节一致 - step 单步:停在指令边界、cycle_count/regs 逐步核对 - ctest 10/10(vm_cycles 84 断言) --- tests/CMakeLists.txt | 11 ++ tests/src/vm_test.cpp | 440 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 tests/src/vm_test.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5ab5034..e06da6d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -78,3 +78,14 @@ target_compile_definitions(codegen_test PRIVATE add_test(NAME codegen_slice1 COMMAND codegen_test) + +# VM 测试:手工映像 + 编译器产物 + 定时器 + 确定性(REPO_ROOT 注入源目录绝对路径) +add_executable(vm_test + ./src/vm_test.cpp) + +target_link_libraries(vm_test PRIVATE compiler vm) +target_compile_definitions(vm_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME vm_cycles + COMMAND vm_test) diff --git a/tests/src/vm_test.cpp b/tests/src/vm_test.cpp new file mode 100644 index 0000000..01cec55 --- /dev/null +++ b/tests/src/vm_test.cpp @@ -0,0 +1,440 @@ +/** + * @file vm_test.cpp + * @brief VM 测试(12.9):手工映像 + 编译器产物 + 定时器 + 确定性 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include +#include + +#include "compiler/Codegen.h" +#include "compiler/Linker.h" +#include "compiler/Project.h" +#include "compiler/Typecheck.h" +#include "isa/Encode.h" +#include "isa/Image.h" +#include "isa/Instr.h" +#include "isa/Op.h" +#include "vm/Machine.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 数据区槽访问 ---- + +static int64_t slot(const vm::Machine& m, int s) { + int64_t v = 0; + for (int i = 0; i < 8; ++i) { + v |= static_cast(m.data()[s * 8 + i]) << (8 * i); + } + return v; +} + +static void wslot(vm::Machine& m, int s, int64_t v) { + for (int i = 0; i < 8; ++i) { + m.data()[s * 8 + i] = static_cast((v >> (8 * i)) & 0xFF); + } +} + +// ---- 编译器产物全链路(编译 → 建机 → 跑周期)---- + +static bool make_machine(const char* dir, vm::Machine* m, std::string* err) { + using namespace compiler; + const std::string toml = std::string(REPO_ROOT) + "/" + dir + "/project.toml"; + Project p; + if (!parse_project(toml, &p, err)) { + return false; + } + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + f, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + LinkResult link; + if (!link_project(p, units, &link, err)) { + return false; + } + if (!check_project(p, units, link, err)) { + return false; + } + std::vector img; + if (!codegen_project(p, units, link, &img, err)) { + return false; + } + return vm::Machine::create(img, m, err); +} + +// ---- 手工拼映像 ---- + +struct HFunc { + std::vector code; + uint32_t nregs = 8; +}; + +static std::vector hand_image(const std::vector& consts, + const std::vector& funcs, + uint32_t entry = 0, + uint32_t cycle_limit = 100000, + const std::vector& data = {}) { + size_t code_total = 0; + for (const HFunc& f : funcs) { + code_total += f.code.size() * 4; + } + const uint32_t off_const = 72; + const uint32_t off_funcs = off_const + static_cast(consts.size()) * 12; + const uint32_t off_code = off_funcs + static_cast(funcs.size()) * 12; + const uint32_t off_data = off_code + static_cast(code_total); + const uint32_t off_end = off_data + static_cast(data.size()); + + std::vector b(off_end, 0); + auto put32 = [&](size_t o, uint32_t v) { + b[o + 0] = static_cast(v & 0xFFu); + b[o + 1] = static_cast((v >> 8) & 0xFFu); + b[o + 2] = static_cast((v >> 16) & 0xFFu); + b[o + 3] = static_cast((v >> 24) & 0xFFu); + }; + auto put64 = [&](size_t o, uint64_t v) { + for (int i = 0; i < 8; ++i) { + b[o + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + }; + put32(0, isa::kMagic); + put32(4, isa::kVersion); + put32(8, cycle_limit); + put32(12, 10); // dt_ms + put32(24, entry); + put32(44, static_cast(consts.size())); + put32(48, static_cast(funcs.size())); + put32(52, off_const); + put32(56, off_funcs); + put32(60, off_code); + put32(64, off_data); + put32(68, off_data); + + for (size_t i = 0; i < consts.size(); ++i) { + const size_t o = off_const + i * 12; + put32(o, static_cast(consts[i].tag)); + put64(o + 4, consts[i].value); + } + size_t acc = 0; + for (size_t i = 0; i < funcs.size(); ++i) { + const size_t o = off_funcs + i * 12; + put32(o, funcs[i].nregs); + put32(o + 4, static_cast(acc)); + put32(o + 8, static_cast(funcs[i].code.size())); + acc += funcs[i].code.size() * 4; + } + size_t c = off_code; + for (const HFunc& f : funcs) { + for (const isa::Instr in : f.code) { + put32(c, in); + c += 4; + } + } + for (size_t i = 0; i < data.size(); ++i) { + b[off_data + i] = data[i]; + } + return b; +} + +// ---- 1. 手工映像:标量 + 跳转 + 数据区 ---- + +static bool test_hand_scalar() { + // r8 := 5;r9 := r8 + 3;槽 0 ← r9;槽 1 → r10;JT 跳过一条 + const std::vector consts = { + {isa::types::Int, 5}, + {isa::types::Int, 3}, + {isa::types::Int, 1}, + }; + const std::vector funcs = {{ + { + isa::enc_imm(isa::Op::LOADK, 8, 0), + isa::enc_rr(isa::Op::MOVE, 9, 8), + isa::enc_imm(isa::Op::LOADK, 11, 1), + isa::enc_rrr(isa::Op::ADD, 9, 9, 11), + isa::enc_slot(isa::Op::STORE_GLOBAL, 9, 0), + isa::enc_slot(isa::Op::LOAD_GLOBAL, 10, 0), + isa::enc_jc(isa::Op::JT, 9, 1), // r9≠0 → 跳过下一条 + isa::enc_imm(isa::Op::LOADK, 10, 2), + isa::enc_ret(), + }, + 12, + }}; + const std::vector img = hand_image(consts, funcs, 0, 100000, + std::vector(16, 0)); // 2 槽 + vm::Machine m; + std::string err; + CHECK(vm::Machine::create(img, &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 5); + CHECK(m.reg(9) == 8); // 5 + 3 + CHECK(slot(m, 0) == 8); // 槽 0 收到 8 + CHECK(m.reg(10) == 8); // 槽 1 读回(JT 跳过覆盖) + CHECK(m.cycle_count() == 8); + return true; +} + +// ---- 2. 手工映像:CALL/RET 帧复制 ---- + +static bool test_hand_call() { + // fn0(入口):r8=5、r2=7、实参 r1←r8;CALL 1;结果 r0 → r8 + // fn1:r0 = r1 + r2(r1/r2 来自调用约定区复制) + const std::vector consts = { + {isa::types::Int, 5}, + {isa::types::Int, 7}, + }; + const std::vector funcs = { + {{ + isa::enc_imm(isa::Op::LOADK, 8, 0), + isa::enc_imm(isa::Op::LOADK, 2, 1), + isa::enc_rr(isa::Op::MOVE, 1, 8), + isa::enc_call(1), + isa::enc_rr(isa::Op::MOVE, 8, 0), + isa::enc_ret(), + }, + 12}, + {{ + isa::enc_rrr(isa::Op::ADD, 0, 1, 2), + isa::enc_ret(), + }, + 10}, + }; + const std::vector img = hand_image(consts, funcs); + vm::Machine m; + std::string err; + CHECK(vm::Machine::create(img, &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 12); // 5 + 7 + CHECK(m.call_depth() == 1); + return true; +} + +// ---- 3. 手工映像:自调用 → StackOverflow ---- + +static bool test_stack_overflow() { + const std::vector funcs = {{ + { + isa::enc_call(0), + isa::enc_ret(), + }, + 8, + }}; + const std::vector img = hand_image({}, funcs); + vm::Machine m; + std::string err; + CHECK(vm::Machine::create(img, &m, &err)); + CHECK(m.run_cycle() == vm::Fault::StackOverflow); + CHECK(m.call_depth() == 1); // 周期结束已清栈(留 MAIN) + return true; +} + +// ---- 4. 手工映像:越界槽 → BadSlot ---- + +static bool test_bad_slot() { + const std::vector funcs = {{ + { + isa::enc_slot(isa::Op::STORE_GLOBAL, 8, 60000), // 数据区 0 字节 + isa::enc_ret(), + }, + 12, + }}; + const std::vector img = hand_image({}, funcs); + vm::Machine m; + std::string err; + CHECK(vm::Machine::create(img, &m, &err)); + CHECK(m.run_cycle() == vm::Fault::BadSlot); + return true; +} + +// ---- 5. 编译器产物正例 ---- + +static bool test_cases_positive() { + std::string err; + vm::Machine m; + + // 01 空 MAIN + CHECK(make_machine("tests/cases/01_empty_main", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + + // 02 BOOL 赋值:a=TRUE(r8) b=FALSE(r9) + CHECK(make_machine("tests/cases/02_bool_assign", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 1 && m.reg(9) == 0); + + // 03 短路:a=0 b=0 → x=0 + CHECK(make_machine("tests/cases/03_short_circuit", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(10) == 0); + + // 04 IF:sel=0 → out=10 + CHECK(make_machine("tests/cases/04_if_elsif_else", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(9) == 10); + + // 05 WHILE:n=10 + CHECK(make_machine("tests/cases/05_while_normal", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 10); + + // 07 算术:a=42 b=10 c=32 eq=1 + CHECK(make_machine("tests/cases/07_int_arith", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 42 && m.reg(9) == 10 && m.reg(10) == 32 && m.reg(11) == 1); + + // 08 TIME 字面量:t1=10 t2=1250 + CHECK(make_machine("tests/cases/08_time_literal", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 10 && m.reg(9) == 1250); + + // 09 GVL + EXTERNAL:x = G1 初值 5(槽 0) + CHECK(make_machine("tests/cases/09_gvl_external", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 5); + + // 13 FUNCTION:x = Add(3,4) = 7 + CHECK(make_machine("tests/cases/13_function_call", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 7); + + // 15 FB 内联:starter(start=TRUE, stop=FALSE) → q=TRUE + CHECK(make_machine("tests/cases/15_fb_instance", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(m.reg(8) == 1); + + // 17 TON:dt=10、pt=30,in 恒真 → 第 3 周期 q=1(槽 2) + CHECK(make_machine("tests/cases/17_ton", &m, &err)); + m.run_cycle(); + m.run_cycle(); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(slot(m, 2) == 1); // t.Q + CHECK(slot(m, 3) == 30); // t.ET + + // 18 TOF/CTU:跑一个周期无故障 + CHECK(make_machine("tests/cases/18_tof_ctu", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + + // 20 line1:I 组合 → Q0_0 真值表(槽 0=急停 1=I0_0 2=I0_1 3=Q0_0) + CHECK(make_machine("tests/cases/20_line1", &m, &err)); + struct { + int start, stop, es, expect; + } table[] = { + {1, 0, 0, 1}, + {1, 1, 0, 0}, + {0, 0, 0, 0}, + {1, 0, 1, 0}, + }; + for (const auto& t : table) { + wslot(m, 0, t.es); + wslot(m, 1, t.start); + wslot(m, 2, t.stop); + CHECK(m.run_cycle() == vm::Fault::None); + CHECK(slot(m, 3) == t.expect); + } + return true; +} + +// ---- 6. 用例 06:cycle_limit 打满 ---- + +static bool test_cycle_limit() { + std::string err; + vm::Machine m; + CHECK(make_machine("tests/cases/06_while_cycle_limit", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::CycleLimit); + return true; +} + +// ---- 7. 确定性:同一 I 序列跑两遍,数据区一致 ---- + +static bool test_determinism() { + std::string err; + vm::Machine a, b; + CHECK(make_machine("tests/cases/20_line1", &a, &err)); + CHECK(make_machine("tests/cases/20_line1", &b, &err)); + const int seq[4][3] = {{1, 0, 0}, {1, 1, 0}, {0, 0, 0}, {1, 0, 1}}; + for (int rep = 0; rep < 2; ++rep) { + vm::Machine& m = rep ? b : a; + for (const auto& s : seq) { + wslot(m, 0, s[2]); + wslot(m, 1, s[0]); + wslot(m, 2, s[1]); + CHECK(m.run_cycle() == vm::Fault::None); + } + } + // 两遍结束:数据区逐字节一致(含 starter 实例状态) + CHECK(a.data_len() == b.data_len()); + for (size_t i = 0; i < a.data_len(); ++i) { + if (a.data()[i] != b.data()[i]) { + std::printf("FAIL determinism byte %zu: %d vs %d\n", i, a.data()[i], b.data()[i]); + return false; + } + } + ++g_checks; + return true; +} + +// ---- 8. step 单步:停在指令边界 ---- + +static bool test_step() { + std::string err; + vm::Machine m; + CHECK(make_machine("tests/cases/02_bool_assign", &m, &err)); + // 3 条指令:LOADK ×2 + RET + CHECK(m.step()); + CHECK(m.cycle_count() == 1 && m.reg(8) == 1); + CHECK(m.step()); + CHECK(m.cycle_count() == 2 && m.reg(9) == 0); + CHECK(!m.step()); // RET → 周期结束 + CHECK(m.cycle_count() == 3); + CHECK(!m.step()); // 结束后不再执行 + return true; +} + +// ---- 9. 手工映像:坏常量 id → BadConst ---- + +static bool test_bad_const() { + const std::vector funcs = {{ + { + isa::enc_imm(isa::Op::LOADK, 8, 99), // 常量表为空 + isa::enc_ret(), + }, + 12, + }}; + const std::vector img = hand_image({}, funcs); + vm::Machine m; + std::string err; + CHECK(vm::Machine::create(img, &m, &err)); + CHECK(m.run_cycle() == vm::Fault::BadConst); + return true; +} + +int main() { + if (!test_hand_scalar()) return 1; + if (!test_hand_call()) return 1; + if (!test_stack_overflow()) return 1; + if (!test_bad_slot()) return 1; + if (!test_cases_positive()) return 1; + if (!test_cycle_limit()) return 1; + if (!test_determinism()) return 1; + if (!test_step()) return 1; + if (!test_bad_const()) return 1; + std::printf("vm_test: %d checks passed\n", g_checks); + return 0; +} From 00565f035d1a3588acd9a44e3449c0b1d4a8b937 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:26:28 +0800 Subject: [PATCH 46/69] =?UTF-8?q?12.10=20=E5=8F=8C=E5=8F=AF=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E6=8E=A5=E7=BA=BF=EF=BC=9A=E7=BC=96=E8=AF=91=E4=BA=A7?= =?UTF-8?q?=E5=87=BA=20sidecar=EF=BC=8CBytecodeExecutor=20=E8=B7=91?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STCompiler -o:另写 .runtime.toml(io 绑定 var→槽号→channel/bit,槽号来自链接结果) - BytecodeExecutor .stb [--cycles N] [--replay ]:加载 .stb + sidecar, 逐周期采样 I(回放行按 io.input 顺序)→ 跑周期 → 打印 I/Q;故障打印 FAULT 退出码 1 - 手写 sidecar 解析(冻结 TOML 子集);Machine 暴露 header()(dt_ms/cycle_limit) - 验证:line1 回放 0/1 组合 Q 正确(急停/start 语义)、用例 06 CycleLimit FAULT;ctest 10/10 --- Doc/executor/执行器入口.md | 10 ++ compiler/src/main.cpp | 23 ++++ executor/src/main.cpp | 234 ++++++++++++++++++++++++++++++++++++- vm/include/vm/Machine.h | 3 + vm/src/Machine.cpp | 1 + 5 files changed, 267 insertions(+), 4 deletions(-) diff --git a/Doc/executor/执行器入口.md b/Doc/executor/执行器入口.md index be0b218..e48d545 100644 --- a/Doc/executor/执行器入口.md +++ b/Doc/executor/执行器入口.md @@ -4,6 +4,16 @@ BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`( 第一版两个可执行:`STCompiler` 编译出 `.stb` 与 `.runtime.toml`;`BytecodeExecutor` 加载映像按扫描周期跑。总顺序见 [`初步计划.md`](../初步计划.md) 第 12.10 节。 +## 用法 + +```text +BytecodeExecutor .stb [--cycles N] [--replay ] +``` + +- `--cycles N`:跑 N 个周期(缺省 10) +- `--replay `:读录制的 I 序列(每行空格分隔的 0/1,按 sidecar `io.input` 绑定顺序),逐周期喂入;读尽后保持最后一行 +- 每周期打印 `cycle N: I=[...] Q=[...]`(按 sidecar 绑定顺序);故障打印 `FAULT(n)` 并退出码 1 + ## 职责 - 加载 `.stb` 映像与 `.runtime.toml` sidecar(I/O 绑定 var → 槽号 → channel/bit) diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index 30d0a8f..90f36ce 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -5,8 +5,10 @@ * @date 2026-08-21 */ +#include #include #include +#include #include #include @@ -167,6 +169,27 @@ int main(int argc, char** argv) { return 1; } + // sidecar:I/O 绑定 var → 槽号 → channel/bit(不进映像,执行器采样用) + std::vector bindings; + for (const isa::IoBinding& b : proj.io) { + isa::IoBinding out_b = b; + std::string key = b.var; + for (char& ch : key) { + ch = static_cast(std::tolower(static_cast(ch))); + } + const auto it = link.global_index.find(key); + if (it != link.global_index.end()) { + out_b.slot = it->second; // 12.6 已校验存在 + } + bindings.push_back(out_b); + } + std::filesystem::path sidecar = std::filesystem::path(out_path); + sidecar.replace_extension(".runtime.toml"); + if (!isa::write_sidecar_file(sidecar.string().c_str(), bindings, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + const isa::ImageView v = isa::ImageView::from(image); std::printf("compiled: %s (%zu bytes, %u functions, %u globals)\n", out_path.c_str(), image.size(), v.header().n_funcs, v.header().n_globals); diff --git a/executor/src/main.cpp b/executor/src/main.cpp index b1a1b6d..ed9e2bb 100644 --- a/executor/src/main.cpp +++ b/executor/src/main.cpp @@ -2,14 +2,240 @@ * @file main.cpp * @brief BytecodeExecutor 可执行入口 * @author - * @date 2026-08-19 + * @date 2026-08-21 + * + * @details 12.10:加载 .stb + sidecar,按扫描周期执行。 + * - `BytecodeExecutor .stb [--cycles N] [--replay ]` + * - 只链 vm + isa,不链 compiler;dt_ms / cycle_limit 从映像头取 + * - I 采样 / Q 写回按 sidecar(var → 槽号 → channel/bit),不创造变量 + * - --replay:读录制文本(每行空格分隔的 0/1,按 io.input 绑定顺序), + * 逐周期喂入;文件读尽后保持最后一行 + * - 每周期打印 I/Q(最小可观测) */ +#include #include +#include +#include +#include +#include + +#include "isa/Image.h" +#include "vm/Machine.h" + +namespace { + + void usage() { + std::printf("usage: BytecodeExecutor .stb [--cycles N] [--replay ]\n" + " 加载 .stb + .runtime.toml,按扫描周期执行\n" + " --cycles N 跑 N 个周期(缺省 10)\n" + " --replay 读录制的 I 序列(每行按 io.input 顺序的 0/1)\n" + " --help 打印本帮助\n"); + } + + // sidecar 绑定(TOML 子集,格式冻结于 Doc/isa/指令与映像.md) + struct Binding { + bool is_input = true; + std::string var; + uint32_t slot = 0; + uint32_t channel = 0; + uint32_t bit = 0; + }; + + // 解析 .runtime.toml(逐行手写解析,仅冻结子集) + bool parse_sidecar(const std::string& path, std::vector* out, + std::string* err) { + std::ifstream in(path); + if (!in) { + *err = "cannot open sidecar: " + path; + return false; + } + std::string line; + bool is_input = true; + bool in_entry = false; + Binding cur; + while (std::getline(in, line)) { + // 去首尾空白 + size_t b = line.find_first_not_of(" \t\r"); + if (b == std::string::npos) { + continue; + } + line = line.substr(b); + if (line.empty() || line[0] == '#') { + continue; + } + if (line == "[[io.input]]") { + if (in_entry) out->push_back(cur); + cur = Binding(); + is_input = true; + in_entry = true; + continue; + } + if (line == "[[io.output]]") { + if (in_entry) out->push_back(cur); + cur = Binding(); + is_input = false; + in_entry = true; + continue; + } + const size_t eq = line.find('='); + if (eq == std::string::npos) { + continue; + } + const std::string key = line.substr(0, eq); + std::string val = line.substr(eq + 1); + const size_t b2 = key.find_first_not_of(" \t"); + const size_t e2 = key.find_last_not_of(" \t"); + const size_t b3 = val.find_first_not_of(" \t"); + const size_t e3 = val.find_last_not_of(" \t\r"); + const std::string k = key.substr(b2, e2 - b2 + 1); + const std::string v = val.substr(b3, e3 - b3 + 1); + if (k == "var") { + cur.var = v.substr(1, v.size() - 2); // 去引号 + } else if (k == "slot") { + cur.slot = static_cast(std::stoul(v)); + } else if (k == "channel") { + cur.channel = static_cast(std::stoul(v)); + } else if (k == "bit") { + cur.bit = static_cast(std::stoul(v)); + } + cur.is_input = is_input; + } + if (in_entry) { + out->push_back(cur); + } + return true; + } + + // 读一个槽的 BOOL 值(低字节) + int slot_bool(vm::Machine& m, uint32_t slot) { + return m.data()[static_cast(slot) * 8] ? 1 : 0; + } + + void put_bool(vm::Machine& m, uint32_t slot, int v) { + m.data()[static_cast(slot) * 8] = v ? 1 : 0; + } + +} // namespace int main(int argc, char** argv) { - (void)argc; - (void)argv; - std::printf("BytecodeExecutor 0.1\n"); + if (argc < 2) { + std::printf("BytecodeExecutor 0.1\n"); + usage(); + return 0; + } + if (std::strcmp(argv[1], "--help") == 0) { + usage(); + return 0; + } + + const std::string stb_path = argv[1]; + uint32_t cycles = 10; + std::string replay; + for (int i = 2; i < argc; ++i) { + if (std::strcmp(argv[i], "--cycles") == 0 && i + 1 < argc) { + cycles = static_cast(std::stoul(argv[i + 1])); + } else if (std::strcmp(argv[i], "--replay") == 0 && i + 1 < argc) { + replay = argv[i + 1]; + } + } + + std::string err; + std::vector bytes; + if (!isa::read_stb_file(stb_path.c_str(), &bytes, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + vm::Machine machine; + if (!vm::Machine::create(bytes, &machine, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + + // sidecar:.runtime.toml + std::string sidecar_path = stb_path; + const size_t dot = sidecar_path.find_last_of('.'); + if (dot != std::string::npos) { + sidecar_path = sidecar_path.substr(0, dot); + } + sidecar_path += ".runtime.toml"; + std::vector bindings; + if (!parse_sidecar(sidecar_path, &bindings, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + + // 回放缓冲 + std::vector replay_line; + std::ifstream replay_in; + if (!replay.empty()) { + replay_in.open(replay); + if (!replay_in) { + std::fprintf(stderr, "error: cannot open replay: %s\n", replay.c_str()); + return 1; + } + } + + const isa::ImageHeader& h = machine.header(); + std::printf("image: %s (%zu bytes, dt_ms=%u, cycle_limit=%u)\n", stb_path.c_str(), + bytes.size(), h.dt_ms, h.cycle_limit); + + for (uint32_t c = 1; c <= cycles; ++c) { + // 采样:回放行按 io.input 顺序填,无回放则全 0 + if (replay_in.is_open()) { + std::string line; + if (std::getline(replay_in, line)) { + replay_line.clear(); + size_t pos = 0; + while (pos < line.size()) { + while (pos < line.size() && + std::isspace(static_cast(line[pos]))) { + ++pos; + } + if (pos >= line.size()) break; + const size_t s = pos; + while (pos < line.size() && + !std::isspace(static_cast(line[pos]))) { + ++pos; + } + replay_line.push_back(std::stoi(line.substr(s, pos - s))); + } + } + size_t idx = 0; + for (const Binding& b : bindings) { + if (!b.is_input) continue; + put_bool(machine, b.slot, idx < replay_line.size() ? replay_line[idx] : 0); + ++idx; + } + } else { + for (const Binding& b : bindings) { + if (b.is_input) { + put_bool(machine, b.slot, 0); + } + } + } + + const vm::Fault f = machine.run_cycle(); + + // 打印 I/Q(按 sidecar 绑定顺序) + std::string is, qs; + for (const Binding& b : bindings) { + if (b.is_input) { + if (!is.empty()) is += " "; + is += std::to_string(slot_bool(machine, b.slot)); + } else { + if (!qs.empty()) qs += " "; + qs += std::to_string(slot_bool(machine, b.slot)); + } + } + std::printf("cycle %u: I=[%s] Q=[%s]", c, is.c_str(), qs.c_str()); + if (f != vm::Fault::None) { + std::printf(" FAULT(%d)", static_cast(f)); + } + std::printf("\n"); + if (f != vm::Fault::None) { + return 1; + } + } return 0; } diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h index 263ea2b..9864bd6 100644 --- a/vm/include/vm/Machine.h +++ b/vm/include/vm/Machine.h @@ -62,6 +62,9 @@ namespace vm { /** @brief 最近一次故障(step 返回 false 后查询) */ Fault fault() const; + /** @brief 映像头(dt_ms / cycle_limit 等) */ + const isa::ImageHeader& header() const; + /** @brief 当前指令下标(相对当前函数字节码段) */ uint32_t pc() const; diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index 216b072..24e82a9 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -78,6 +78,7 @@ bool Machine::step() { } Fault Machine::fault() const { return fault_; } +const isa::ImageHeader& Machine::header() const { return image_.header(); } uint32_t Machine::pc() const { return pc_; } uint32_t Machine::fn_id() const { return cur_frame().fn_id; } uint32_t Machine::cycle_count() const { return cycle_count_; } From 66d9c6c5481d2a6b3d2d85253c0ba449f98e49c2 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:31:14 +0800 Subject: [PATCH 47/69] =?UTF-8?q?12.11=20=E9=97=AD=E7=8E=AF=EF=BC=9A?= =?UTF-8?q?=E4=BA=8C=E5=8D=81=E7=94=A8=E4=BE=8B=E7=82=B9=E4=BA=AE=20+=20?= =?UTF-8?q?=E5=8D=95=E6=AD=A5=E8=A7=82=E5=AF=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cases_test:tests/cases/01..20 逐一跑完整管线(解析→链接→类型→码→VM 周期), 按 EXPECTED.md 期望断言:13 正例运行通过(含双周期可重复)、06 CycleLimit、6 负例报错类别 - BytecodeExecutor --step:采样 I → 逐指令打印 pc/fn/反汇编 + 执行后非零寄存器 → I/Q - Machine 补 header()/ended()/nregs()/cur_instr() 观察接口 - ctest 11/11(cases_all 20 用例点亮);line1 回放 0/1 组合 Q 正确、单步 I=[0 1]→Q=[1] --- Doc/executor/执行器入口.md | 3 +- executor/src/main.cpp | 138 ++++++++++++++++++++--------- tests/CMakeLists.txt | 11 +++ tests/src/cases_test.cpp | 173 +++++++++++++++++++++++++++++++++++++ vm/include/vm/Machine.h | 10 +++ vm/src/Machine.cpp | 14 +++ 6 files changed, 308 insertions(+), 41 deletions(-) create mode 100644 tests/src/cases_test.cpp diff --git a/Doc/executor/执行器入口.md b/Doc/executor/执行器入口.md index e48d545..76de4af 100644 --- a/Doc/executor/执行器入口.md +++ b/Doc/executor/执行器入口.md @@ -7,11 +7,12 @@ BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`( ## 用法 ```text -BytecodeExecutor .stb [--cycles N] [--replay ] +BytecodeExecutor .stb [--cycles N] [--replay ] [--step] ``` - `--cycles N`:跑 N 个周期(缺省 10) - `--replay `:读录制的 I 序列(每行空格分隔的 0/1,按 sidecar `io.input` 绑定顺序),逐周期喂入;读尽后保持最后一行 +- `--step`:单步——采样 I 后逐指令打印(pc/fn/反汇编 + 执行后非零寄存器),跑 1 个周期,最后打印 I/Q - 每周期打印 `cycle N: I=[...] Q=[...]`(按 sidecar 绑定顺序);故障打印 `FAULT(n)` 并退出码 1 ## 职责 diff --git a/executor/src/main.cpp b/executor/src/main.cpp index ed9e2bb..a11e14d 100644 --- a/executor/src/main.cpp +++ b/executor/src/main.cpp @@ -20,16 +20,18 @@ #include #include +#include "isa/Encode.h" #include "isa/Image.h" #include "vm/Machine.h" namespace { void usage() { - std::printf("usage: BytecodeExecutor .stb [--cycles N] [--replay ]\n" + std::printf("usage: BytecodeExecutor .stb [--cycles N] [--replay ] [--step]\n" " 加载 .stb + .runtime.toml,按扫描周期执行\n" " --cycles N 跑 N 个周期(缺省 10)\n" " --replay 读录制的 I 序列(每行按 io.input 顺序的 0/1)\n" + " --step 单步:逐指令打印 pc/fn/反汇编/寄存器(跑 1 个周期)\n" " --help 打印本帮助\n"); } @@ -116,6 +118,58 @@ namespace { m.data()[static_cast(slot) * 8] = v ? 1 : 0; } + // 采样:回放行按 io.input 顺序填,无回放则全 0;文件读尽后保持最后一行 + void sample_inputs(vm::Machine& m, const std::vector& bindings, + std::ifstream& replay_in, std::vector& replay_line) { + if (replay_in.is_open()) { + std::string line; + if (std::getline(replay_in, line)) { + replay_line.clear(); + size_t pos = 0; + while (pos < line.size()) { + while (pos < line.size() && + std::isspace(static_cast(line[pos]))) { + ++pos; + } + if (pos >= line.size()) break; + const size_t s = pos; + while (pos < line.size() && + !std::isspace(static_cast(line[pos]))) { + ++pos; + } + replay_line.push_back(std::stoi(line.substr(s, pos - s))); + } + } + size_t idx = 0; + for (const Binding& b : bindings) { + if (!b.is_input) continue; + put_bool(m, b.slot, idx < replay_line.size() ? replay_line[idx] : 0); + ++idx; + } + } else { + for (const Binding& b : bindings) { + if (b.is_input) { + put_bool(m, b.slot, 0); + } + } + } + } + + // 按 sidecar 绑定顺序打印 I/Q + std::string iq_string(vm::Machine& m, const std::vector& bindings, + std::string* is, std::string* qs) { + for (const Binding& b : bindings) { + if (b.is_input) { + if (!is->empty()) *is += " "; + *is += std::to_string(slot_bool(m, b.slot)); + } else { + if (!qs->empty()) *qs += " "; + *qs += std::to_string(slot_bool(m, b.slot)); + } + } + return *is; + } + } // namespace int main(int argc, char** argv) { @@ -131,12 +185,15 @@ int main(int argc, char** argv) { const std::string stb_path = argv[1]; uint32_t cycles = 10; + bool step_mode = false; std::string replay; for (int i = 2; i < argc; ++i) { if (std::strcmp(argv[i], "--cycles") == 0 && i + 1 < argc) { cycles = static_cast(std::stoul(argv[i + 1])); } else if (std::strcmp(argv[i], "--replay") == 0 && i + 1 < argc) { replay = argv[i + 1]; + } else if (std::strcmp(argv[i], "--step") == 0) { + step_mode = true; } } @@ -180,54 +237,55 @@ int main(int argc, char** argv) { std::printf("image: %s (%zu bytes, dt_ms=%u, cycle_limit=%u)\n", stb_path.c_str(), bytes.size(), h.dt_ms, h.cycle_limit); - for (uint32_t c = 1; c <= cycles; ++c) { - // 采样:回放行按 io.input 顺序填,无回放则全 0 - if (replay_in.is_open()) { - std::string line; - if (std::getline(replay_in, line)) { - replay_line.clear(); - size_t pos = 0; - while (pos < line.size()) { - while (pos < line.size() && - std::isspace(static_cast(line[pos]))) { - ++pos; - } - if (pos >= line.size()) break; - const size_t s = pos; - while (pos < line.size() && - !std::isspace(static_cast(line[pos]))) { - ++pos; - } - replay_line.push_back(std::stoi(line.substr(s, pos - s))); + // 单步模式:采样 I → 逐指令打印(指令 + 执行后寄存器)→ I/Q + if (step_mode) { + std::printf("-- single step (cycle 1) --\n"); + sample_inputs(machine, bindings, replay_in, replay_line); + uint32_t guard = 0; + while (machine.fault() == vm::Fault::None && !machine.ended()) { + char buf[64]; + isa::disasm(machine.cur_instr(), buf, sizeof buf); + const uint32_t pc = machine.pc(); + const uint32_t fn = machine.fn_id(); + if (!machine.step()) { + break; + } + // 执行后状态:指令 + 非零寄存器 + std::printf(" pc=%u fn=%u %s", pc, fn, buf); + std::string regs; + const uint32_t n = machine.nregs() < 32 ? machine.nregs() : 32; + for (uint32_t i = 0; i < n; ++i) { + const int64_t v = machine.reg(static_cast(i)); + if (v != 0) { + if (!regs.empty()) regs += " "; + regs += "r" + std::to_string(i) + "=" + std::to_string(v); } } - size_t idx = 0; - for (const Binding& b : bindings) { - if (!b.is_input) continue; - put_bool(machine, b.slot, idx < replay_line.size() ? replay_line[idx] : 0); - ++idx; - } - } else { - for (const Binding& b : bindings) { - if (b.is_input) { - put_bool(machine, b.slot, 0); - } + std::printf(" [%s]\n", regs.c_str()); + if (++guard > 1000000) { + std::printf(" ...step limit\n"); + break; } } + if (machine.fault() != vm::Fault::None) { + std::printf(" FAULT(%d)\n", static_cast(machine.fault())); + return 1; + } + std::string is, qs; + iq_string(machine, bindings, &is, &qs); + std::printf("cycle 1: I=[%s] Q=[%s]\n", is.c_str(), qs.c_str()); + return 0; + } + + for (uint32_t c = 1; c <= cycles; ++c) { + // 采样:回放行按 io.input 顺序填,无回放则全 0 + sample_inputs(machine, bindings, replay_in, replay_line); const vm::Fault f = machine.run_cycle(); // 打印 I/Q(按 sidecar 绑定顺序) std::string is, qs; - for (const Binding& b : bindings) { - if (b.is_input) { - if (!is.empty()) is += " "; - is += std::to_string(slot_bool(machine, b.slot)); - } else { - if (!qs.empty()) qs += " "; - qs += std::to_string(slot_bool(machine, b.slot)); - } - } + iq_string(machine, bindings, &is, &qs); std::printf("cycle %u: I=[%s] Q=[%s]", c, is.c_str(), qs.c_str()); if (f != vm::Fault::None) { std::printf(" FAULT(%d)", static_cast(f)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e06da6d..7f96223 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -89,3 +89,14 @@ target_compile_definitions(vm_test PRIVATE add_test(NAME vm_cycles COMMAND vm_test) + +# 12.2 二十用例点亮(12.11):按 EXPECTED.md 期望跑完整管线(REPO_ROOT 注入源目录绝对路径) +add_executable(cases_test + ./src/cases_test.cpp) + +target_link_libraries(cases_test PRIVATE compiler vm) +target_compile_definitions(cases_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME cases_all + COMMAND cases_test) diff --git a/tests/src/cases_test.cpp b/tests/src/cases_test.cpp new file mode 100644 index 0000000..0374248 --- /dev/null +++ b/tests/src/cases_test.cpp @@ -0,0 +1,173 @@ +/** + * @file cases_test.cpp + * @brief 12.2 二十用例点亮(12.11):每例按 EXPECTED.md 期望跑完整管线 + * @author + * @date 2026-08-21 + * + * @details 对 tests/cases/01..20 逐一执行:解析 → 链接 → 类型检查 → 寄存器码 → VM 周期, + * 断言期望结果(编译运行通过 / 报错类别 / cycle_limit 故障),与各用例 EXPECTED.md 一致。 + */ + +#include +#include +#include + +#include "compiler/Codegen.h" +#include "compiler/Linker.h" +#include "compiler/Project.h" +#include "compiler/Typecheck.h" +#include "vm/Machine.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +namespace { + + enum class Outcome { Ok, CompileError, CycleLimit }; + + // 期望表(与 tests/cases/*/EXPECTED.md 一致;负例断言错误类别关键词) + struct CaseSpec { + const char* dir; + Outcome out; + const char* err_keyword; // CompileError 时须出现在错误消息里 + }; + + const CaseSpec kCases[] = { + {"01_empty_main", Outcome::Ok, ""}, + {"02_bool_assign", Outcome::Ok, ""}, + {"03_short_circuit", Outcome::Ok, ""}, + {"04_if_elsif_else", Outcome::Ok, ""}, + {"05_while_normal", Outcome::Ok, ""}, + {"06_while_cycle_limit", Outcome::CycleLimit, ""}, + {"07_int_arith", Outcome::Ok, ""}, + {"08_time_literal", Outcome::Ok, ""}, + {"09_gvl_external", Outcome::Ok, ""}, + {"10_gvl_wrong_file", Outcome::CompileError, "VAR_GLOBAL only allowed in gvl file"}, + {"11_duplicate_global", Outcome::CompileError, "duplicate global"}, + {"12_io_unknown_var", Outcome::CompileError, "io.var"}, + {"13_function_call", Outcome::Ok, ""}, + {"14_function_write_global", Outcome::CompileError, "function cannot write global"}, + {"15_fb_instance", Outcome::Ok, ""}, + {"16_fb_undeclared", Outcome::CompileError, "undeclared FB instance"}, + {"17_ton", Outcome::Ok, ""}, + {"18_tof_ctu", Outcome::Ok, ""}, + {"19_recursive_call", Outcome::CompileError, "recursive call"}, + {"20_line1", Outcome::Ok, ""}, + }; + + bool run_case(const char* dir, Outcome want, const char* keyword) { + using namespace compiler; + const std::string toml = std::string(REPO_ROOT) + "/tests/cases/" + dir + "/project.toml"; + std::string err; + + Project p; + if (!parse_project(toml, &p, &err)) { + if (want == Outcome::CompileError) { + ++g_checks; + return true; + } + std::printf("FAIL %s parse: %s\n", dir, err.c_str()); + return false; + } + std::vector units; + for (const std::string& f : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + f, &u, &err)) { + if (want == Outcome::CompileError) { + ++g_checks; + return true; + } + std::printf("FAIL %s load: %s\n", dir, err.c_str()); + return false; + } + units.push_back(std::move(u)); + } + LinkResult link; + if (!link_project(p, units, &link, &err)) { + if (want == Outcome::CompileError) { + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL %s: want '%s', got '%s'\n", dir, keyword, err.c_str()); + return false; + } + ++g_checks; + return true; + } + std::printf("FAIL %s link: %s\n", dir, err.c_str()); + return false; + } + if (!check_project(p, units, link, &err)) { + if (want == Outcome::CompileError) { + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL %s: want '%s', got '%s'\n", dir, keyword, err.c_str()); + return false; + } + ++g_checks; + return true; + } + std::printf("FAIL %s type: %s\n", dir, err.c_str()); + return false; + } + std::vector img; + if (!codegen_project(p, units, link, &img, &err)) { + if (want == Outcome::CompileError) { + ++g_checks; + return true; + } + std::printf("FAIL %s codegen: %s\n", dir, err.c_str()); + return false; + } + if (want == Outcome::CompileError) { + std::printf("FAIL %s: expected compile error\n", dir); + return false; + } + + vm::Machine m; + if (!vm::Machine::create(img, &m, &err)) { + std::printf("FAIL %s vm create: %s\n", dir, err.c_str()); + return false; + } + const vm::Fault f = m.run_cycle(); + if (want == Outcome::CycleLimit) { + if (f == vm::Fault::CycleLimit) { + ++g_checks; + return true; + } + std::printf("FAIL %s: want CycleLimit, got %d\n", dir, static_cast(f)); + return false; + } + if (f != vm::Fault::None) { + std::printf("FAIL %s: fault %d\n", dir, static_cast(f)); + return false; + } + // 可重复性:再跑一个周期 + if (m.run_cycle() != vm::Fault::None) { + std::printf("FAIL %s: 2nd cycle fault\n", dir); + return false; + } + ++g_checks; + return true; + } + +} // namespace + +int main() { + for (const CaseSpec& c : kCases) { + if (!run_case(c.dir, c.out, c.err_keyword)) { + return 1; + } + } + std::printf("cases_test: %d cases lit up\n", g_checks); + return 0; +} diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h index 9864bd6..4ffdc1e 100644 --- a/vm/include/vm/Machine.h +++ b/vm/include/vm/Machine.h @@ -20,6 +20,7 @@ #include #include "isa/Image.h" +#include "isa/Instr.h" namespace vm { @@ -62,6 +63,9 @@ namespace vm { /** @brief 最近一次故障(step 返回 false 后查询) */ Fault fault() const; + /** @brief 周期是否已结束(MAIN 的 RET 后为 true) */ + bool ended() const; + /** @brief 映像头(dt_ms / cycle_limit 等) */ const isa::ImageHeader& header() const; @@ -87,6 +91,12 @@ namespace vm { /** @brief 当前帧寄存器值(i < nregs) */ int64_t reg(uint8_t i) const; + /** @brief 当前帧寄存器数(函数头 nregs) */ + uint32_t nregs() const; + + /** @brief 当前指令(pc_ 处;单步/观察用) */ + isa::Instr cur_instr() const; + private: struct Frame { uint32_t fn_id = 0; // 本帧函数 diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index 24e82a9..24a61a2 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -78,6 +78,7 @@ bool Machine::step() { } Fault Machine::fault() const { return fault_; } +bool Machine::ended() const { return ended_; } const isa::ImageHeader& Machine::header() const { return image_.header(); } uint32_t Machine::pc() const { return pc_; } uint32_t Machine::fn_id() const { return cur_frame().fn_id; } @@ -88,6 +89,19 @@ const uint8_t* Machine::data() const { return data_.data(); } size_t Machine::data_len() const { return data_.size(); } int64_t Machine::reg(uint8_t i) const { return cur_frame().regs[i]; } +uint32_t Machine::nregs() const { + return static_cast(cur_frame().regs.size()); +} + +isa::Instr Machine::cur_instr() const { + const isa::FuncRow row = image_.func_row(cur_frame().fn_id); + const uint8_t* base = image_.code_bytes() + row.code_offset; + if (pc_ >= row.code_len) { + return isa::pack(isa::Op::RET, 0, 0, 0); + } + return reinterpret_cast(base)[pc_]; +} + Machine::Frame& Machine::cur_frame() { return frames_.back(); } const Machine::Frame& Machine::cur_frame() const { return frames_.back(); } From 26256a36b90ea981a63e8a71869a6b91279d3d9e Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:38:27 +0800 Subject: [PATCH 48/69] =?UTF-8?q?=E6=89=A9=E5=85=85=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=9D=97=E4=B8=BA=208=20=E4=B8=AA=EF=BC=9ATO?= =?UTF-8?q?N/TOF/TP/CTU/CTD/CTUD/R=5FTRIG/F=5FTRIG=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isa:CAL_* 操作码连续占 24..31(CALL/RET 后移为 32/33,kOpCount=34);disasm 支持 5 个新操作码 - 词法/语法/链接:新 8 个关键字与冻结布局(TP=in/pt/q/et、CTD=cd/ld/pv/q/cv、 CTUD=cu/cd/r/lu/pv/qu/qd/cv、R_TRIG/F_TRIG=clk/q) - VM:do_cal 全 8 个语义(TP 脉冲、CTD 递减、CTUD 双向+装载、R_TRIG 上升沿、F_TRIG 下降沿); edge_prev_ 每槽 2 字节存边沿上次输入;exec_one 分发补 5 个新操作码 - 验证:TP 30ms 脉冲 2 周期、CTD 装载+递减到 q=1、CTUD cu/cd 双向、R/F_TRIG 交替; ctest 11/11 全绿 - 文档:指令与映像/词法/初步计划/符号表与链接/指令执行/扫描周期/使用说明 同步 --- Doc/compiler/STCompiler使用说明.md | 4 +- Doc/compiler/符号表与链接.md | 11 ++- Doc/compiler/词法.md | 3 +- Doc/isa/stb文件格式.md | 2 +- Doc/isa/指令与映像.md | 6 +- Doc/vm/扫描周期.md | 4 +- Doc/vm/指令执行.md | 24 ++--- Doc/初步计划.md | 12 +-- compiler/include/compiler/Lexer.h | 5 ++ compiler/src/Codegen.cpp | 16 +++- compiler/src/Lexer.cpp | 7 +- compiler/src/Linker.cpp | 20 ++++- compiler/src/Parser.cpp | 25 ++++++ isa/include/isa/Op.h | 17 ++-- isa/src/Encode.cpp | 5 ++ tests/src/isa_test.cpp | 10 ++- vm/include/vm/Machine.h | 4 +- vm/src/Machine.cpp | 138 ++++++++++++++++++++++++----- 18 files changed, 249 insertions(+), 64 deletions(-) diff --git a/Doc/compiler/STCompiler使用说明.md b/Doc/compiler/STCompiler使用说明.md index 927e497..5f980c0 100644 --- a/Doc/compiler/STCompiler使用说明.md +++ b/Doc/compiler/STCompiler使用说明.md @@ -80,7 +80,7 @@ compiled: line1.stb (184 bytes, 2 functions, 4 globals) | `project.name` | 是 | 工程名 | | `project.entry` | 是 | 第一版必须 `"program MAIN"` | | `project.cycle_limit` | 是 | 每周期指令上限(>0) | -| `project.dt_ms` | 是 | 本周期 Δt 毫秒(>0),给 TON/TOF/CTU | +| `project.dt_ms` | 是 | 本周期 Δt 毫秒(>0),给内置 FB 定时器 | | `files.st` | 是 | 源文件列表(非空数组) | | `gvl.file` | 否 | 唯一允许 `VAR_GLOBAL` 的文件;已在 `files.st` 则不重复收录 | | `[[io.input]]` / `[[io.output]]` | 否 | `var` + `channel` + `bit`;`var` 必须已在 GVL 声明 | @@ -110,7 +110,7 @@ bit = 2 **变量段**:`VAR` / `VAR_INPUT` / `VAR_OUTPUT` / `VAR_GLOBAL`(仅 `gvl.file` 顶层)/ `VAR_EXTERNAL`。 -**类型**:`BOOL` / `INT` / `TIME`;内建 FB 类型 `TON` / `TOF` / `CTU`(关键字,不可作变量名)。 +**类型**:`BOOL` / `INT` / `TIME`;内建 FB 类型 `TON` / `TOF` / `TP` / `CTU` / `CTD` / `CTUD` / `R_TRIG` / `F_TRIG`(关键字,不可作变量名)。 **语句**:赋值 `:=`、`IF / ELSIF / ELSE / END_IF`、`WHILE / END_WHILE`、FB 调用 `fb(in := ..., ...);`、字段读 `fb.Q`。 diff --git a/Doc/compiler/符号表与链接.md b/Doc/compiler/符号表与链接.md index f540ed9..90eceeb 100644 --- a/Doc/compiler/符号表与链接.md +++ b/Doc/compiler/符号表与链接.md @@ -12,7 +12,7 @@ compiler 模块的符号收集与链接。输入:`Project`(toml)+ 各 `.st - `FUNCTION` 标无状态;`FUNCTION_BLOCK` 必须先有实例才能调用 - `CALL` 目标解析成 `fn_id`(立即数),无函数指针 - 校验:同名全局、未声明 `io.var`、重复 POU 名、未声明 FB 实例、函数循环调用 → 失败 -- FB 实例布局:按类型算字段清单(输入/输出/内部/内建 TON/TOF/CTU 固定字段) +- FB 实例布局:按类型算字段清单(输入/输出/内部/内建 8 个 FB 固定字段(12.11 扩充)) **不做(第一版)** @@ -70,15 +70,20 @@ struct Symbol { ## FB 实例布局 - 用户 FB:字段 = 全部 `VAR_INPUT` + `VAR_OUTPUT` + `VAR`(内部),按声明段序排 -- 内建(冻结,isa 规格): +- 内建(冻结,isa 规格;12.11 扩为 8 个): | FB | 字段(名 → 类型) | |---|---| | TON | in BOOL, pt TIME, q BOOL, et TIME | | TOF | in BOOL, pt TIME, q BOOL, et TIME | +| TP | in BOOL, pt TIME, q BOOL, et TIME | | CTU | cu BOOL, r BOOL, pv INT, q BOOL, cv INT | +| CTD | cd BOOL, ld BOOL, pv INT, q BOOL, cv INT | +| CTUD | cu BOOL, cd BOOL, r BOOL, lu BOOL, pv INT, qu BOOL, qd BOOL, cv INT | +| R_TRIG | clk BOOL, q BOOL | +| F_TRIG | clk BOOL, q BOOL | -- 实例布局 = 类型字段表的一份拷贝;字段偏移 12.8 按对齐规则算 +- 实例布局 = 类型字段表的一份拷贝;字段偏移 = 字段序号 × 8(8 字节定宽槽) ## 错误 diff --git a/Doc/compiler/词法.md b/Doc/compiler/词法.md index 908b864..f0dfef8 100644 --- a/Doc/compiler/词法.md +++ b/Doc/compiler/词法.md @@ -26,11 +26,12 @@ BOOL INT TIME IF ELSIF ELSE END_IF WHILE END_WHILE AND OR NOT TRUE FALSE -TON TOF CTU +TON TOF TP CTU CTD CTUD R_TRIG F_TRIG ``` > 12.5 修订:`THEN`、`DO` 补入关键字表(`IF ... THEN` / `WHILE ... DO` 语法需要), > 与 12.1 原表合并,其余不变。 +> 12.11 修订:内置 FB 扩为 8 个(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG)。 其余单词一律当标识符;`VAR_IN_OUT`、`REF`、`CLASS` 等由语法层(12.5)出明确错误。 diff --git a/Doc/isa/stb文件格式.md b/Doc/isa/stb文件格式.md index 7a32781..f7ccf98 100644 --- a/Doc/isa/stb文件格式.md +++ b/Doc/isa/stb文件格式.md @@ -86,7 +86,7 @@ line1 `MAIN` 前 5 条(`0x60` 起,slot 为 8 字节定宽槽号): ## FB 布局段(`offset_fb` 起) -v1 为空(用户 FB 内联展开,字段偏移已算进数据段;内建 TON/TOF/CTU 布局冻结在编译器中)。行格式约定:`[field_count:u32][field_count × (tag:u32, offset:u32)]`,offset 相对实例基址。 +v1 为空(用户 FB 内联展开,字段偏移已算进数据段;内建 8 个 FB 布局冻结在编译器中(12.11 扩充))。行格式约定:`[field_count:u32][field_count × (tag:u32, offset:u32)]`,offset 相对实例基址。 ## 数据段(`offset_data` 起) diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index a196a7d..3cb6867 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -106,11 +106,15 @@ JMP offset JT/JF r, offset LOAD_I / STORE_Q / LOAD_M / STORE_M LOAD_GLOBAL / STORE_GLOBAL -CAL_TON / CAL_TOF / CAL_CTU +CAL_TON / CAL_TOF / CAL_TP / CAL_CTU / CAL_CTD / CAL_CTUD / CAL_R_TRIG / CAL_F_TRIG CALL fn_id RET ``` +> 12.11 修订:内置功能块扩为 8 个(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG), +> `CAL_*` 操作码**连续占 24..31**(原 CALL=27、RET=28 后移为 32、33)。 +> 操作码数值即编码,本表为准。 + 局部 / `VAR` = 固定寄存器;表达式临时值往后编号;`nregs` 写进函数头。 `I`/`Q`/`M` 走映像指令,不是通用寄存器。 FB 实例固定布局,字段偏移编译期算死。 diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md index 0af8f6b..c40ce26 100644 --- a/Doc/vm/扫描周期.md +++ b/Doc/vm/扫描周期.md @@ -10,7 +10,7 @@ vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [ 1. 按 sidecar 的 io 绑定,采样 → I / 输入全局 2. 从 PROGRAM MAIN 的 pc=0 执行到 RET 3. 写回 Q / 输出全局 -4. 用本周期 Δt 推进 TON / TOF / CTU(不用 wall clock) +4. 用本周期 Δt 推进内置 FB(不用 wall clock) ``` ## 边界 @@ -97,7 +97,7 @@ namespace vm { - `CALL fn_id`:压新帧(复制 r0..r7)→ pc=0;`RET`:复制 r0..r7 回调用方 → 弹栈(MAIN 的 RET = 周期结束) - `cycle_limit` 超限(用例 6)、栈深 > 64、`BadOp` / `BadSlot` / `BadConst` → 周期中止返回故障 -### 步骤 4:定时器(CAL_TON / CAL_TOF / CAL_CTU) +### 步骤 4:定时器与边沿(8 个 CAL_*) - 执行时用映像 `dt_ms` 推进(每周期一次调用 = 推进一次),不读系统时钟 - TON:in 真 → et += dt(到 pt 停)、q = et ≥ pt;in 假 → et = 0 diff --git a/Doc/vm/指令执行.md b/Doc/vm/指令执行.md index 11b72e8..00f810d 100644 --- a/Doc/vm/指令执行.md +++ b/Doc/vm/指令执行.md @@ -80,7 +80,7 @@ STORE_* slot, rs → 写 data[slot*8 .. slot*8+8) = rs(8 字节原样) | `JT rd, off` / `JF rd, off` | `reg[rd] != 0`(JT)/ `== 0`(JF)时 `pc += off`,否则正常 +1 | | `LOAD_* rd, slot` | 见数据区访问 | | `STORE_* slot, rs` | 见数据区访问 | -| `CAL_TON/TOF/CTU slot` | 见定时器推进 | +| `CAL_TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG slot` | 见定时器与边沿推进 | | `CALL fn_id` / `RET` | 见帧与调用栈 | ### 跳转语义 @@ -95,26 +95,30 @@ STORE_* slot, rs → 写 data[slot*8 .. slot*8+8) = rs(8 字节原样) - 每条指令执行后 `cycle_count++`;超过映像头 `cycle_limit` → `CycleLimit` 故障(周期中止) - `JMP` / `JT` / `JF` / `CALL` 只算 1 条(cycle_limit 以"执行的指令数"计) -## 定时器推进(CAL_TON / CAL_TOF / CAL_CTU) +## 定时器与边沿推进(CAL_TON / CAL_TOF / CAL_TP / CAL_CTU / CAL_CTD / CAL_CTUD / CAL_R_TRIG / CAL_F_TRIG) -`CAL_* slot` 的 `slot` 是实例**基槽号**;字段偏移 = 实例布局字段序号 × 8(内建布局冻结:TON/TOF = in/pt/q/et,CTU = cu/r/pv/q/cv)。每次执行 = 推进一次,Δt = 映像头 `dt_ms`(不读系统时钟): +`CAL_* slot` 的 `slot` 是实例**基槽号**;字段偏移 = 字段序号 × 8(布局冻结:TON/TOF/TP = in/pt/q/et;CTU = cu/r/pv/q/cv;CTD = cd/ld/pv/q/cv;CTUD = cu/cd/r/lu/pv/qu/qd/cv;R_TRIG/F_TRIG = clk/q)。每次执行 = 推进一次,Δt = 映像头 `dt_ms`(不读系统时钟)。边沿检测的"上次输入"存 VM 侧 `edge_prev_`(每实例基槽 2 字节),跨周期保留: ```text CAL_TON: - in = 槽[in] 非零;pt = 槽[pt];et = 槽[et];q = 槽[q] in 为真:et += dt(et ≥ pt 时停在 pt);q = (et ≥ pt) ? 1 : 0 in 为假:et = 0;q = 0 CAL_TOF: in 为真:q = 1;et = 0 in 掉电:et += dt;et ≥ pt 时 q = 0 - (in 再次为真 → 重新 q = 1, et = 0) -CAL_CTU: - 上升沿检测:本周期 cu 为真且上次 cu 为假 → cv += 1 - (上次 cu 存 VM 侧 vector,按实例基槽索引,跨周期保留) - r 为真 → cv = 0 - q = (cv ≥ pv) ? 1 : 0 +CAL_TP(脉冲): + in 上升沿启动 PT 时长脉冲(期间 in 变化不影响) + 计时中:et += dt;et ≥ pt → q = 0、et 归零(脉冲结束);否则 q = 1 + 非计时:et = 0;q = 0 + +CAL_CTU:cu 上升沿 → cv += 1;r → cv = 0;q = (cv ≥ pv) ? 1 : 0 +CAL_CTD:cd 上升沿 → cv -= 1;ld → cv = pv;q = (cv ≤ 0) ? 1 : 0 +CAL_CTUD:cu 上升沿 → cv += 1;cd 上升沿 → cv -= 1;r → cv = 0;lu → cv = pv; + qu = (cv ≥ pv) ? 1 : 0;qd = (cv ≤ 0) ? 1 : 0 +CAL_R_TRIG:q = clk 且上次 clk 假(上升沿) +CAL_F_TRIG:q = !clk 且上次 clk 真(下降沿) ``` ## 故障 diff --git a/Doc/初步计划.md b/Doc/初步计划.md index ab003b8..9771ebe 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -45,7 +45,7 @@ 1. 按 toml 的 io 绑定,采样 → I / 输入全局 2. 从 PROGRAM MAIN 的 pc=0 执行到 RET 3. 写回 Q / 输出全局 - 4. 用本周期 Δt 推进 TON / TOF / CTU(不用 wall clock) + 4. 用本周期 Δt 推进内置 FB(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG,不用 wall clock) ``` 运行时是 **一台 VM、一个入口、一份映像**。多文件只影响源码怎么切、怎么链接。 @@ -219,7 +219,7 @@ JMP offset JT/JF r, offset LOAD_I / STORE_Q / LOAD_M / STORE_M LOAD_GLOBAL / STORE_GLOBAL -CAL_TON / CAL_TOF / CAL_CTU +CAL_TON / CAL_TOF / CAL_TP / CAL_CTU / CAL_CTD / CAL_CTUD / CAL_R_TRIG / CAL_F_TRIG CALL fn_id RET ``` @@ -354,7 +354,7 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom | `project.name` | 工程名 | 必填 | | `project.entry` | 入口 | 第一版必须是 `program MAIN` | | `project.cycle_limit` | 每周期指令上限 | 如 `100000`;超时是故障 | -| `project.dt_ms` | 本周期 Δt | 给 TON / TOF / CTU,不是 wall clock | +| `project.dt_ms` | 本周期 Δt | 给内置 FB 定时器,不是 wall clock | | `files.st` | 源文件列表 | 编译前全部已知 | | `gvl.file` | 唯一允许 `VAR_GLOBAL` 的文件 | 已在 `files.st` 则不重复收录 | | `[[io.input]]` / `[[io.output]]` | `var` + `channel` + `bit` | 可选;`var` 必须已在 GVL | @@ -370,7 +370,7 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom `THEN` `DO` (12.5 修订补入:`IF…THEN` / `WHILE…DO` 语法需要) `AND` `OR` `NOT` `TRUE` `FALSE` -`TON` `TOF` `CTU` +`TON` `TOF` `TP` `CTU` `CTD` `CTUD` `R_TRIG` `F_TRIG` (12.11 修订:内置 FB 扩为 8 个) 其余一律当标识符或直接拒绝(`VAR_IN_OUT`、`REF`、`CLASS` 等出明确错误)。 @@ -449,7 +449,7 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom 1. POU 外壳:`PROGRAM` / `FUNCTION` / `FUNCTION_BLOCK` 与对应 `END_*` 2. 变量段:`VAR` / `VAR_INPUT` / `VAR_OUTPUT` / `VAR_GLOBAL` / `VAR_EXTERNAL` -3. 类型名只接受 `BOOL` `INT` `TIME`;内置 FB 名 `TON` `TOF` `CTU` +3. 类型名只接受 `BOOL` `INT` `TIME`;内置 FB 名 `TON` `TOF` `TP` `CTU` `CTD` `CTUD` `R_TRIG` `F_TRIG` (12.11 修订:内置 FB 扩为 8 个) 4. 语句:赋值、`IF`、`WHILE`、FB 调用 `fb(in := …)`、字段 `fb.Q` 5. 表达式:`NOT`、`AND`/`OR`(AST 里保留短路语义)、比较、加减乘除 @@ -512,7 +512,7 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom 此时才写执行循环。只认 `isa` 映像。 ```text -采样 I → pc=0 执行到 RET → 写回 Q → 用本周期 Δt 推进 TON/TOF/CTU +采样 I → pc=0 执行到 RET → 写回 Q → 用本周期 Δt 推进内置 FB ``` - 译码 + 大 `switch`;寄存器文件按函数头 `nregs` 分配。 diff --git a/compiler/include/compiler/Lexer.h b/compiler/include/compiler/Lexer.h index 0d3b4bf..d66517f 100644 --- a/compiler/include/compiler/Lexer.h +++ b/compiler/include/compiler/Lexer.h @@ -46,7 +46,12 @@ namespace compiler { FALSE, TON, TOF, + TP, CTU, + CTD, + CTUD, + R_TRIG, + F_TRIG, // 字面量 IDENT, // text = 小写名 INT_LIT, // int_value diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 4e911bf..eb18252 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -327,10 +327,18 @@ namespace { f.code.push_back(isa::enc_slot(isa::Op::STORE_GLOBAL, t, it->second)); } if (inst->type_name == "ton" || inst->type_name == "tof" || - inst->type_name == "ctu") { - const isa::Op op = inst->type_name == "ton" ? isa::Op::CAL_TON - : inst->type_name == "tof" ? isa::Op::CAL_TOF - : isa::Op::CAL_CTU; + inst->type_name == "tp" || inst->type_name == "ctu" || + inst->type_name == "ctd" || inst->type_name == "ctud" || + inst->type_name == "r_trig" || inst->type_name == "f_trig") { + const isa::Op op = + inst->type_name == "ton" ? isa::Op::CAL_TON + : inst->type_name == "tof" ? isa::Op::CAL_TOF + : inst->type_name == "tp" ? isa::Op::CAL_TP + : inst->type_name == "ctu" ? isa::Op::CAL_CTU + : inst->type_name == "ctd" ? isa::Op::CAL_CTD + : inst->type_name == "ctud" ? isa::Op::CAL_CTUD + : inst->type_name == "r_trig" ? isa::Op::CAL_R_TRIG + : isa::Op::CAL_F_TRIG; f.code.push_back(isa::enc_slot(op, 0, inst->base)); return true; } diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index 85d0d1f..19ed538 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -78,10 +78,15 @@ namespace { // 字面量 {"true", Tok::TRUE}, {"false", Tok::FALSE}, - // 内建 FB 类型名(v1 冻结三件套) + // 内置 FB 类型名(12.11 扩充为 8 个) {"ton", Tok::TON}, {"tof", Tok::TOF}, + {"tp", Tok::TP}, {"ctu", Tok::CTU}, + {"ctd", Tok::CTD}, + {"ctud", Tok::CTUD}, + {"r_trig", Tok::R_TRIG}, + {"f_trig", Tok::F_TRIG}, }; /** diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp index e4fce44..da25153 100644 --- a/compiler/src/Linker.cpp +++ b/compiler/src/Linker.cpp @@ -96,24 +96,40 @@ namespace { return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time; } - // 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md) + // 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md;12.11 扩为 8 个) const FbLayout kTonLayout{"ton", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, {"q", TypeKind::Bool}, {"et", TypeKind::Time}}}; const FbLayout kTofLayout{"tof", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, {"q", TypeKind::Bool}, {"et", TypeKind::Time}}}; + const FbLayout kTpLayout{"tp", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, + {"q", TypeKind::Bool}, {"et", TypeKind::Time}}}; const FbLayout kCtuLayout{"ctu", {{"cu", TypeKind::Bool}, {"r", TypeKind::Bool}, {"pv", TypeKind::Int}, {"q", TypeKind::Bool}, {"cv", TypeKind::Int}}}; + const FbLayout kCtdLayout{"ctd", {{"cd", TypeKind::Bool}, {"ld", TypeKind::Bool}, + {"pv", TypeKind::Int}, {"q", TypeKind::Bool}, + {"cv", TypeKind::Int}}}; + const FbLayout kCtudLayout{"ctud", {{"cu", TypeKind::Bool}, {"cd", TypeKind::Bool}, + {"r", TypeKind::Bool}, {"lu", TypeKind::Bool}, + {"pv", TypeKind::Int}, {"qu", TypeKind::Bool}, + {"qd", TypeKind::Bool}, {"cv", TypeKind::Int}}}; + const FbLayout kRTrigLayout{"r_trig", {{"clk", TypeKind::Bool}, {"q", TypeKind::Bool}}}; + const FbLayout kFTrigLayout{"f_trig", {{"clk", TypeKind::Bool}, {"q", TypeKind::Bool}}}; /** * @brief 内建 FB 名 → 冻结布局 * @param name FB 类型名(小写) - * @return 布局指针(ton / tof / ctu);未知名字返回 nullptr + * @return 布局指针(ton/tof/tp/ctu/ctd/ctud/r_trig/f_trig);未知返回 nullptr */ const FbLayout* builtin_layout(const std::string& name) { if (name == "ton") return &kTonLayout; if (name == "tof") return &kTofLayout; + if (name == "tp") return &kTpLayout; if (name == "ctu") return &kCtuLayout; + if (name == "ctd") return &kCtdLayout; + if (name == "ctud") return &kCtudLayout; + if (name == "r_trig") return &kRTrigLayout; + if (name == "f_trig") return &kFTrigLayout; return nullptr; } diff --git a/compiler/src/Parser.cpp b/compiler/src/Parser.cpp index 38a570d..5c71730 100644 --- a/compiler/src/Parser.cpp +++ b/compiler/src/Parser.cpp @@ -458,11 +458,36 @@ namespace { t->name = "tof"; advance(); return true; + case Tok::TP: + t->kind = TypeKind::FbBuiltin; + t->name = "tp"; + advance(); + return true; case Tok::CTU: t->kind = TypeKind::FbBuiltin; t->name = "ctu"; advance(); return true; + case Tok::CTD: + t->kind = TypeKind::FbBuiltin; + t->name = "ctd"; + advance(); + return true; + case Tok::CTUD: + t->kind = TypeKind::FbBuiltin; + t->name = "ctud"; + advance(); + return true; + case Tok::R_TRIG: + t->kind = TypeKind::FbBuiltin; + t->name = "r_trig"; + advance(); + return true; + case Tok::F_TRIG: + t->kind = TypeKind::FbBuiltin; + t->name = "f_trig"; + advance(); + return true; case Tok::IDENT: { const std::string word = cur().text; if (is_forbidden(word)) { diff --git a/isa/include/isa/Op.h b/isa/include/isa/Op.h index e74b33e..cfa3d83 100644 --- a/isa/include/isa/Op.h +++ b/isa/include/isa/Op.h @@ -37,15 +37,21 @@ namespace isa { STORE_M = 21, // STORE_M rs, slot LOAD_GLOBAL = 22, // LOAD_GLOBAL rd, slot STORE_GLOBAL = 23, // STORE_GLOBAL rs, slot + // 内置功能块(8 个连续,12.11 扩充:TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG) CAL_TON = 24, // CAL_TON instance_slot CAL_TOF = 25, // CAL_TOF instance_slot - CAL_CTU = 26, // CAL_CTU instance_slot - CALL = 27, // CALL fn_id - RET = 28, // RET + CAL_TP = 26, // CAL_TP instance_slot + CAL_CTU = 27, // CAL_CTU instance_slot + CAL_CTD = 28, // CAL_CTD instance_slot + CAL_CTUD = 29, // CAL_CTUD instance_slot + CAL_R_TRIG = 30, // CAL_R_TRIG instance_slot + CAL_F_TRIG = 31, // CAL_F_TRIG instance_slot + CALL = 32, // CALL fn_id + RET = 33, // RET }; // 操作码总数 - static const int kOpCount = 29; + static const int kOpCount = 34; // 助记符:下标与 Op 数值一一对应 inline const char* mnemonic(Op op) { @@ -56,7 +62,8 @@ namespace isa { "JMP", "JT", "JF", "LOAD_I", "STORE_Q", "LOAD_M", "STORE_M", "LOAD_GLOBAL", "STORE_GLOBAL", - "CAL_TON", "CAL_TOF", "CAL_CTU", + "CAL_TON", "CAL_TOF", "CAL_TP", "CAL_CTU", "CAL_CTD", "CAL_CTUD", + "CAL_R_TRIG", "CAL_F_TRIG", "CALL", "RET", }; const int idx = static_cast(op); diff --git a/isa/src/Encode.cpp b/isa/src/Encode.cpp index f6f1391..9990d66 100644 --- a/isa/src/Encode.cpp +++ b/isa/src/Encode.cpp @@ -83,7 +83,12 @@ void disasm(Instr w, char* out, size_t cap) { break; case Op::CAL_TON: case Op::CAL_TOF: + case Op::CAL_TP: case Op::CAL_CTU: + case Op::CAL_CTD: + case Op::CAL_CTUD: + case Op::CAL_R_TRIG: + case Op::CAL_F_TRIG: case Op::CALL: snprintf(out, cap, "%s %u", m, static_cast(imm16(w))); break; diff --git a/tests/src/isa_test.cpp b/tests/src/isa_test.cpp index ce95a3b..6588332 100644 --- a/tests/src/isa_test.cpp +++ b/tests/src/isa_test.cpp @@ -54,9 +54,13 @@ static bool test_sat() { static bool test_op() { using namespace isa; CHECK(static_cast(Op::MOVE) == 0); - CHECK(static_cast(Op::RET) == 28); - CHECK(kOpCount == 29); - CHECK(std::strcmp(mnemonic(Op::CAL_CTU), "CAL_CTU") == 0); + CHECK(static_cast(Op::CAL_TON) == 24); + CHECK(static_cast(Op::CAL_F_TRIG) == 31); // 8 个 CAL_* 连续 24..31 + CHECK(static_cast(Op::CALL) == 32); + CHECK(static_cast(Op::RET) == 33); + CHECK(kOpCount == 34); + CHECK(std::strcmp(mnemonic(Op::CAL_CTUD), "CAL_CTUD") == 0); + CHECK(std::strcmp(mnemonic(Op::CAL_F_TRIG), "CAL_F_TRIG") == 0); CHECK(std::strcmp(mnemonic(static_cast(255)), "???") == 0); return true; } diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h index 4ffdc1e..9a63669 100644 --- a/vm/include/vm/Machine.h +++ b/vm/include/vm/Machine.h @@ -108,7 +108,9 @@ namespace vm { isa::ImageView image_; // 映像只读视图 std::vector data_; // 数据区工作副本(8 字节定宽槽) std::vector frames_; // 调用栈([0] = MAIN,跨周期保留) - std::vector ctu_prev_; // CTU 上次 cu(按实例基槽索引,12.9 步骤 4) + // 边沿检测上次输入(每槽 2 字节:[slot*2] 第一边沿、[slot*2+1] 第二边沿; + // CTU/CTD/R_TRIG/F_TRIG 用第一、CTUD 用两个),跨周期保留 + std::vector edge_prev_; uint32_t pc_ = 0; // 当前 pc(相对本帧函数字节码段) uint32_t cycle_count_ = 0; // 本周期指令数 Fault fault_ = Fault::None; // 最近一次故障 diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index 24a61a2..8493be0 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -35,7 +35,7 @@ bool Machine::create(const std::vector& image, Machine* out, // 数据区工作副本(8 字节定宽槽) out->data_.assign(out->image_.data_bytes(), out->image_.data_bytes() + out->image_.data_len()); - out->ctu_prev_.assign(out->data_len() / 8, 0); + out->edge_prev_.assign(out->data_len() / 8 * 2, 0); // MAIN 帧(跨周期保留) const uint32_t entry = out->image_.header().entry_fn_id; @@ -265,7 +265,12 @@ bool Machine::exec_one() { return !ended_; // MAIN 的 RET → 周期结束(返回 false) case isa::Op::CAL_TON: case isa::Op::CAL_TOF: + case isa::Op::CAL_TP: case isa::Op::CAL_CTU: + case isa::Op::CAL_CTD: + case isa::Op::CAL_CTUD: + case isa::Op::CAL_R_TRIG: + case isa::Op::CAL_F_TRIG: fault_ = do_cal(static_cast(op), isa::imm16(w)); return fault_ == Fault::None; default: @@ -340,15 +345,18 @@ Fault Machine::do_ret() { Fault Machine::do_cal(uint8_t op, uint16_t slot) { // 内建 FB 布局冻结(Doc/compiler/符号表与链接.md): - // TON/TOF:in/pt/q/et(字段 0/1/2/3) - // CTU:cu/r/pv/q/cv(字段 0/1/2/3/4) - // 实例槽号从 slot(基槽)起;字段偏移 = 字段序号 × 8(8 字节定宽) + // TON/TOF/TP:in/pt/q/et(字段 0/1/2/3) + // CTU:cu/r/pv/q/cv(0/1/2/3/4);CTD:cd/ld/pv/q/cv(同) + // CTUD:cu/cd/r/lu/pv/qu/qd/cv(0..7) + // R_TRIG / F_TRIG:clk/q(0/1) + // 字段偏移 = 字段序号 × 8(8 字节定宽槽);边沿存 edge_prev_ const int64_t dt = image_.header().dt_ms; int64_t v = 0; - if (op == static_cast(isa::Op::CAL_TON) || - op == static_cast(isa::Op::CAL_TOF)) { - // 字段:in=0 pt=1 q=2 et=3 + const bool is_ton = op == static_cast(isa::Op::CAL_TON); + const bool is_tof = op == static_cast(isa::Op::CAL_TOF); + if (is_ton || is_tof) { + // in=0 pt=1 q=2 et=3 if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; const bool in = v != 0; if (!slot_get(static_cast(slot + 1), &v)) return Fault::BadSlot; @@ -356,7 +364,7 @@ Fault Machine::do_cal(uint8_t op, uint16_t slot) { int64_t et = 0; if (!slot_get(static_cast(slot + 3), &et)) return Fault::BadSlot; int64_t q = 0; - if (op == static_cast(isa::Op::CAL_TON)) { + if (is_ton) { // TON:in 真 → et += dt(到 pt 停)、q = et ≥ pt;in 假 → et = 0、q = 0 if (in) { et += dt; @@ -383,29 +391,115 @@ Fault Machine::do_cal(uint8_t op, uint16_t slot) { return Fault::None; } - if (op == static_cast(isa::Op::CAL_CTU)) { - // 字段:cu=0 r=1 pv=2 q=3 cv=4 - if (slot >= ctu_prev_.size()) return Fault::BadSlot; + if (op == static_cast(isa::Op::CAL_TP)) { + // in=0 pt=1 q=2 et=3 + 上次 in(edge_prev_[slot*2]) + if (static_cast(slot) * 2 + 1 >= edge_prev_.size()) return Fault::BadSlot; if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; - const bool cu = v != 0; + const bool in = v != 0; if (!slot_get(static_cast(slot + 1), &v)) return Fault::BadSlot; - const bool r = v != 0; + const int64_t pt = v; + int64_t et = 0; + if (!slot_get(static_cast(slot + 3), &et)) return Fault::BadSlot; + // TP(脉冲):in 上升沿启动 PT 时长的脉冲,期间 in 变化不影响 + const bool prev = edge_prev_[slot * 2] != 0; + const bool rising = in && !prev; + int64_t q = 0; + if (rising) { + et = 0; + } + const bool timing = rising || (et > 0); + if (timing) { + et += dt; + if (pt > 0 && et >= pt) { + et = 0; // 脉冲结束:et 归零,下周期不再计时 + q = 0; + } else { + q = 1; + } + } else { + et = 0; + q = 0; + } + edge_prev_[slot * 2] = in ? 1 : 0; + if (!slot_set(static_cast(slot + 3), et)) return Fault::BadSlot; + if (!slot_set(static_cast(slot + 2), q)) return Fault::BadSlot; + return Fault::None; + } + + if (op == static_cast(isa::Op::CAL_CTU) || + op == static_cast(isa::Op::CAL_CTD)) { + // CTU:cu/r/pv/q/cv;CTD:cd/ld/pv/q/cv + if (static_cast(slot) * 2 >= edge_prev_.size()) return Fault::BadSlot; + if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; + const bool edge_in = v != 0; // cu(CTU)/ cd(CTD) + if (!slot_get(static_cast(slot + 1), &v)) return Fault::BadSlot; + const bool ld_in = v != 0; // r(CTU)/ ld(CTD) if (!slot_get(static_cast(slot + 2), &v)) return Fault::BadSlot; const int64_t pv = v; int64_t cv = 0; if (!slot_get(static_cast(slot + 4), &cv)) return Fault::BadSlot; - // 上升沿:本周期 cu 真且上次 cu 假 → cv + 1;r → 复位 - const bool prev = ctu_prev_[slot] != 0; + const bool prev = edge_prev_[slot * 2] != 0; + if (ld_in) { + cv = pv; + } else if (edge_in && !prev) { + cv += (op == static_cast(isa::Op::CAL_CTU)) ? 1 : -1; + } + edge_prev_[slot * 2] = edge_in ? 1 : 0; + const int64_t q = (op == static_cast(isa::Op::CAL_CTU)) + ? ((cv >= pv) ? 1 : 0) + : ((cv <= 0) ? 1 : 0); + if (!slot_set(static_cast(slot + 4), cv)) return Fault::BadSlot; + if (!slot_set(static_cast(slot + 3), q)) return Fault::BadSlot; + return Fault::None; + } + + if (op == static_cast(isa::Op::CAL_CTUD)) { + // cu=0 cd=1 r=2 lu=3 pv=4 qu=5 qd=6 cv=7 + if (static_cast(slot) * 2 + 1 >= edge_prev_.size()) return Fault::BadSlot; + if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; + const bool cu = v != 0; + if (!slot_get(static_cast(slot + 1), &v)) return Fault::BadSlot; + const bool cd = v != 0; + if (!slot_get(static_cast(slot + 2), &v)) return Fault::BadSlot; + const bool r = v != 0; + if (!slot_get(static_cast(slot + 3), &v)) return Fault::BadSlot; + const bool lu = v != 0; + if (!slot_get(static_cast(slot + 4), &v)) return Fault::BadSlot; + const int64_t pv = v; + int64_t cv = 0; + if (!slot_get(static_cast(slot + 7), &cv)) return Fault::BadSlot; + const bool prev_cu = edge_prev_[slot * 2] != 0; + const bool prev_cd = edge_prev_[slot * 2 + 1] != 0; if (r) { cv = 0; - } else if (cu && !prev) { - cv += 1; - } - ctu_prev_[slot] = cu ? 1 : 0; // 记录本次 cu(跨周期) - if (!slot_set(static_cast(slot + 4), cv)) return Fault::BadSlot; - if (!slot_set(static_cast(slot + 3), (cv >= pv) ? 1 : 0)) { - return Fault::BadSlot; + } else if (lu) { + cv = pv; + } else { + if (cu && !prev_cu) cv += 1; + if (cd && !prev_cd) cv -= 1; } + edge_prev_[slot * 2] = cu ? 1 : 0; + edge_prev_[slot * 2 + 1] = cd ? 1 : 0; + const int64_t qu = (cv >= pv) ? 1 : 0; + const int64_t qd = (cv <= 0) ? 1 : 0; + if (!slot_set(static_cast(slot + 7), cv)) return Fault::BadSlot; + if (!slot_set(static_cast(slot + 5), qu)) return Fault::BadSlot; + if (!slot_set(static_cast(slot + 6), qd)) return Fault::BadSlot; + return Fault::None; + } + + if (op == static_cast(isa::Op::CAL_R_TRIG) || + op == static_cast(isa::Op::CAL_F_TRIG)) { + // clk=0 q=1 + if (static_cast(slot) * 2 >= edge_prev_.size()) return Fault::BadSlot; + if (!slot_get(static_cast(slot + 0), &v)) return Fault::BadSlot; + const bool clk = v != 0; + const bool prev = edge_prev_[slot * 2] != 0; + const int64_t q = (op == static_cast(isa::Op::CAL_R_TRIG)) + ? ((clk && !prev) ? 1 : 0) + : ((!clk && prev) ? 1 : 0); + edge_prev_[slot * 2] = clk ? 1 : 0; + if (!slot_set(static_cast(slot + 1), q)) return Fault::BadSlot; return Fault::None; } From b86a9eaf07acf1aace3e8497e1920dfc3f1ba0fd Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 20:58:17 +0800 Subject: [PATCH 49/69] =?UTF-8?q?=E8=A7=84=E5=88=92=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=EF=BC=88ops.txt=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E7=99=BB=E8=AE=B0=E5=94=AF=E4=B8=80=E4=BA=8B=E5=AE=9E?= =?UTF-8?q?=E6=9D=A5=E6=BA=90=20+=20=E5=BC=BA=E6=A0=A1=E9=AA=8C=20+=20?= =?UTF-8?q?=E5=B1=8F=E8=94=BD=E6=9C=BA=E5=88=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Doc/isa/指令配置.md:格式(全属性必填)、OpTable(解析+与枚举强校验)、 各端接线(STCompiler --ops 编译路径必填/屏蔽传导;BytecodeExecutor 可选加载/Disabled 故障)、 加指令/屏蔽流程、7 步执行计划 - FB 注册并入 ops.txt 列为待讨论(未定案) - 索引登记 --- Doc/isa/指令配置.md | 94 +++++++++++++++++++++++++++++++++++++++++++++ Doc/索引.md | 2 + 2 files changed, 96 insertions(+) create mode 100644 Doc/isa/指令配置.md diff --git a/Doc/isa/指令配置.md b/Doc/isa/指令配置.md new file mode 100644 index 0000000..9033a70 --- /dev/null +++ b/Doc/isa/指令配置.md @@ -0,0 +1,94 @@ +# 指令配置文件(ops.txt) + +指令集的**唯一事实来源**:name / opcode / format / 参数名 / enabled。代码实现与配置文件在启动时**强校验**一致,防止两处漂移。 + +## 目标 + +- 加指令/屏蔽指令的登记集中在一处(配置文件),不再散落在代码各处手改 +- 执行器(VM):**语义实现必须在代码里**,不能纯从文件加载;配置文件用于**屏蔽**(`enabled=false` 的指令执行到 → 故障) +- 编译器(STCompiler):**启动时从文件加载**(校验 + 屏蔽传导) +- `.stb` ABI 不变:opcode 数值仍是冻结契约;屏蔽是编译期/运行期策略,不改数值 + +## 配置文件格式(`isa/ops.txt`) + +**全属性必填,不可省略**;`#` 注释、空行忽略: + +```text +# 格式:op <参数名...> enabled +op MOVE 0 RR rd rs enabled +op LOADK 1 IMM rd const_id enabled +op NOT 2 RR rd rs enabled +op AND 3 RRR rd ra rb enabled +op ADD 5 RRR rd ra rb enabled +op CMP_EQ 9 RRR rd ra rb enabled +op JMP 15 JMP off enabled +op JT 16 JC r off enabled +op LOAD_I 18 SLOT rd slot enabled +op CAL_TON 24 CAL instance enabled +op CALL 32 CALL fn_id enabled +op RET 33 NONE enabled +``` + +- 参数个数由 format 决定,解析时校验字段数: + +| format | 参数 | 现有指令 | +|---|---|---| +| `RR` | 2(rd rs) | MOVE / NOT | +| `RRR` | 3(rd ra rb) | AND/OR/ADD/SUB/MUL/DIV/CMP_xx | +| `IMM` | 2(rd const_id) | LOADK | +| `SLOT` | 2(rd slot) | LOAD_I/STORE_Q/LOAD_M/STORE_M/LOAD_GLOBAL/STORE_GLOBAL | +| `JMP` | 1(off) | JMP | +| `JC` | 2(r off) | JT / JF | +| `CALL` | 1(fn_id) | CALL | +| `CAL` | 1(instance) | CAL_*(8 个) | +| `NONE` | 0 | RET | + +## isa 层:`OpTable` + +```cpp +class OpTable { + bool load(const char* path, std::string* err); // 解析 + 强校验 + bool enabled(uint8_t op) const; + // name / format / 参数名查询(后续 FB 注册也走这里) +}; +``` + +**强校验(load 时)**: + +- 语法:字段数符合 format、enabled 必须出现 +- 操作码唯一、0..255 +- **与编译期枚举一致**:文件里每条指令的 name/opcode/format 必须与代码(`Op` 枚举 + 助记符 + `OpFormat`)逐条一致,任一不符 → 报错列出差异 +- 任一失败 → `load` 返回 false,调用方报错退出 + +前置改造:**OpFormat 显式化**——现有隐式"形态"(disasm 的 case)变成枚举 `RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE`;`disasm` 改表驱动(输出文本不变,现有断言兼容)。 + +## 各端接线 + +| 端 | 参数 | 行为 | +|---|---|---| +| STCompiler | `STCompiler [-o out.stb] --ops ` | **仅 `-o` 编译路径要求**;加载/校验失败 → 报错退出;codegen 生成被屏蔽指令 → 编译错误(屏蔽传导) | +| BytecodeExecutor | `--ops `(**可选**) | 不传 = 全部启用(兼容现有用法);传入 → 加载 + 校验,执行到 disabled 指令 → `Fault::Disabled`,周期中止退出码 1 | +| 语义实现 | — | 仍在 VM 代码里(每指令一个 case,不可省) | + +## 加东西的流程(改造后) + +```text +加普通指令:ops.txt 加一行(op name opcode format 参数 enabled) + + Op.h 枚举加一项 + VM 一个语义 case → 重建 isa/VM +屏蔽指令:ops.txt 把 enabled 改 disabled + → 编译器拒绝生成、执行器运行时报 Disabled 故障(零代码改动) +``` + +## 执行计划(单步确认) + +1. **OpFormat 显式化**:新增 `OpFormat` 枚举;`disasm` 改表驱动(输出不变,11 个 ctest 全绿) +2. **`ops.txt` 落地**:34 条指令,全属性必填 +3. **`OpTable`**:`isa/include/isa/OpTable.h` + `isa/src/OpTable.cpp`(解析 + 强校验 + 查询) +4. **STCompiler `--ops`**:加载/校验/屏蔽传导 +5. **BytecodeExecutor `--ops`**:可选加载 / `Fault::Disabled` +6. **测试**:`ops_test`(正例 + 缺属性/字段数错/重号/与枚举不一致负例)、屏蔽传导用例、11 个 ctest 回归 +7. **文档 + 提交** + +## 待讨论(未定案) + +- 内建 FB 注册是否并入 `ops.txt`(关键字/字段布局/CAL 操作码一行登记)——若并入,加 FB = 配置文件一行 + VM 一个语义 case,编译器免重建。讨论定案后另行实施,本次只做指令表。 diff --git a/Doc/索引.md b/Doc/索引.md index a02bd95..5b30e71 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -8,6 +8,7 @@ doc/ 初步计划.md isa/指令与映像.md isa/stb文件格式.md + isa/指令配置.md compiler/编译管线.md compiler/STCompiler使用说明.md compiler/词法.md @@ -25,6 +26,7 @@ doc/ | [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`isa/stb文件格式.md`](isa/stb文件格式.md) | `.stb` 结构说明(用 line1 真实字节拆解) | +| [`isa/指令配置.md`](isa/指令配置.md) | ops.txt:指令登记唯一事实来源、OpTable、屏蔽机制 | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | | [`compiler/STCompiler使用说明.md`](compiler/STCompiler使用说明.md) | 构建、用法、工程文件、ST 子集、错误类别 | | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | From 5bcab6791fd5c58c55671117c76ac52d4eb84d61 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 21:09:23 +0800 Subject: [PATCH 50/69] =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=AE=9A=E6=A1=88=EF=BC=9A=E4=B8=A4=E5=A4=A7=E5=88=86=E7=B1=BB?= =?UTF-8?q?=EF=BC=88PLAIN/INSTANCE=EF=BC=89+=20FB=20=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=B9=B6=E5=85=A5=20ops.txt=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 三层分类:class(操作对象)→ format(操作数形态)→ 参数名(语义) - op 行加 class 字段(PLAIN 26 条 / INSTANCE 8 条 CAL_*),类别-格式互锁校验 - fb 行登记内建 FB 布局(8 个),cal_opcode 与 op 行强校验一致 - 加 FB = 配置文件一行 + VM 语义 case,编译器免重建;执行计划更新为 7 步 --- Doc/isa/指令配置.md | 99 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/Doc/isa/指令配置.md b/Doc/isa/指令配置.md index 9033a70..66d21e0 100644 --- a/Doc/isa/指令配置.md +++ b/Doc/isa/指令配置.md @@ -1,6 +1,6 @@ # 指令配置文件(ops.txt) -指令集的**唯一事实来源**:name / opcode / format / 参数名 / enabled。代码实现与配置文件在启动时**强校验**一致,防止两处漂移。 +指令集的**唯一事实来源**:name / opcode / class / format / 参数名 / enabled。代码实现与配置文件在启动时**强校验**一致,防止两处漂移。 ## 目标 @@ -9,24 +9,55 @@ - 编译器(STCompiler):**启动时从文件加载**(校验 + 屏蔽传导) - `.stb` ABI 不变:opcode 数值仍是冻结契约;屏蔽是编译期/运行期策略,不改数值 +## 指令的三层分类(定案) + +```text +class(操作对象) → format(操作数形态) → 参数名(操作数语义) + PLAIN/INSTANCE RR/RRR/IMM/SLOT/ rd/rs/const_id/ + JMP/JC/CALL/CAL/NONE slot/off/fn_id/instance +``` + +| 层 | 回答的问题 | 谁需要它 | +|---|---|---| +| **class** | 操作对象:**值/搬运** 还是 **实例算法** | 语义归属、校验互锁(INSTANCE ↔ CAL) | +| **format** | 操作数的**形态**:寄存器 / 常量 id / 槽号 / 带符号偏移 | disasm 渲染、编码助手 | +| **参数名** | 每个操作数的**语义命名** | 元数据、文档、校验 | + +### 两个大类 + +| 大类 | 操作对象 | 现有指令 | 操作数形态 | +|---|---|---|---| +| **无实例**(PLAIN) | 寄存器 / 立即数 / 槽号 / 分支 / 调用 | MOVE/LOADK/NOT/AND/OR/算术/CMP_* / JMP/JT/JF / LOAD_*/STORE_* / CALL/RET(26 条) | RR/RRR/IMM/SLOT/JMP/JC/CALL/NONE | +| **有实例**(INSTANCE) | **数据区实例块**(内建 FB) | CAL_TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG(8 条) | CAL(一个实例基槽号) | + +区别的本质: + +```text +LOAD_GLOBAL(无实例):从槽读一个"值" → 地址是编译期槽号,纯搬运 +CAL_TON(有实例) :对实例块执行"算法" → 基槽号 + 完整定时器语义(读字段/推进/写字段) +``` + +format 不可省:同是"第二个操作数是数字",`LOADK`/`LOAD_I`/`CALL` 渲染相同但语义不同(常量 id / 槽号 / fn_id,由参数名表达);"数字 vs 寄存器 vs 偏移"的**渲染差异**只有 format 能表达(`r%d` / `%u` / `%+d`)。 + ## 配置文件格式(`isa/ops.txt`) **全属性必填,不可省略**;`#` 注释、空行忽略: ```text -# 格式:op <参数名...> enabled -op MOVE 0 RR rd rs enabled -op LOADK 1 IMM rd const_id enabled -op NOT 2 RR rd rs enabled -op AND 3 RRR rd ra rb enabled -op ADD 5 RRR rd ra rb enabled -op CMP_EQ 9 RRR rd ra rb enabled -op JMP 15 JMP off enabled -op JT 16 JC r off enabled -op LOAD_I 18 SLOT rd slot enabled -op CAL_TON 24 CAL instance enabled -op CALL 32 CALL fn_id enabled -op RET 33 NONE enabled +# op <参数名...> +# class: PLAIN(无实例)/ INSTANCE(有实例) +op MOVE 0 PLAIN RR rd rs enabled +op LOADK 1 PLAIN IMM rd const_id enabled +op NOT 2 PLAIN RR rd rs enabled +op AND 3 PLAIN RRR rd ra rb enabled +op ADD 5 PLAIN RRR rd ra rb enabled +op CMP_EQ 9 PLAIN RRR rd ra rb enabled +op JMP 15 PLAIN JMP off enabled +op JT 16 PLAIN JC r off enabled +op LOAD_I 18 PLAIN SLOT rd slot enabled +op CAL_TON 24 INSTANCE CAL instance enabled +op CALL 32 PLAIN CALL fn_id enabled +op RET 33 PLAIN NONE enabled ``` - 参数个数由 format 决定,解析时校验字段数: @@ -43,6 +74,28 @@ op RET 33 NONE enabled | `CAL` | 1(instance) | CAL_*(8 个) | | `NONE` | 0 | RET | +- **类别与格式互锁**:`INSTANCE` 类的 format 必须是 `CAL`,反之亦然——配置错误启动期暴露 + +### 内建 FB 注册(定案:并入 ops.txt) + +`CAL_*` 已是 op 行(与 ADD 同机制:编码/译码/disasm/屏蔽);fb 行登记其**数据结构(布局)**——ST 语言层的登记处(Lexer/Parser/Linker/Codegen 查表): + +```text +# fb <字段:类型 ...> +fb ton 24 in:BOOL pt:TIME q:BOOL et:TIME +fb tof 25 in:BOOL pt:TIME q:BOOL et:TIME +fb tp 26 in:BOOL pt:TIME q:BOOL et:TIME +fb ctu 27 cu:BOOL r:BOOL pv:INT q:BOOL cv:INT +fb ctd 28 cd:BOOL ld:BOOL pv:INT q:BOOL cv:INT +fb ctud 29 cu:BOOL cd:BOOL r:BOOL lu:BOOL pv:INT qu:BOOL qd:BOOL cv:INT +fb r_trig 30 clk:BOOL q:BOOL +fb f_trig 31 clk:BOOL q:BOOL +``` + +- fb 行的 `cal_opcode` 与 op 行的 CAL_* 操作码**强校验一致**(防两处漂移) +- 字段类型名小写(`bool/int/time`),与语言关键字对齐 +- **加 FB = fb 行 + VM 一个语义 case,编译器免重建**(关键字/布局/操作码映射全部从表读) + ## isa 层:`OpTable` ```cpp @@ -73,22 +126,20 @@ class OpTable { ## 加东西的流程(改造后) ```text -加普通指令:ops.txt 加一行(op name opcode format 参数 enabled) +加普通指令:ops.txt 加一行 op(name opcode class format 参数 enabled) + Op.h 枚举加一项 + VM 一个语义 case → 重建 isa/VM +加内建 FB:ops.txt 加一行 fb(name cal_opcode 字段:类型) + + VM 一个语义 case → 重建 VM(编译器免重建) 屏蔽指令:ops.txt 把 enabled 改 disabled → 编译器拒绝生成、执行器运行时报 Disabled 故障(零代码改动) ``` ## 执行计划(单步确认) -1. **OpFormat 显式化**:新增 `OpFormat` 枚举;`disasm` 改表驱动(输出不变,11 个 ctest 全绿) -2. **`ops.txt` 落地**:34 条指令,全属性必填 -3. **`OpTable`**:`isa/include/isa/OpTable.h` + `isa/src/OpTable.cpp`(解析 + 强校验 + 查询) -4. **STCompiler `--ops`**:加载/校验/屏蔽传导 +1. **OpFormat / OpClass 显式化**:新增 `OpFormat`(RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE)与 `OpClass`(PLAIN/INSTANCE);`disasm` 改表驱动(输出不变,11 个 ctest 全绿) +2. **`ops.txt` 落地**:34 条 op 行 + 8 条 fb 行,全属性必填 +3. **`OpTable`**:`isa/include/isa/OpTable.h` + `isa/src/OpTable.cpp`(解析 + 强校验:语法/重号/越界/类别-格式互锁/fb-opcode 一致/与编译期枚举一致) +4. **STCompiler `--ops`**:加载/校验/屏蔽传导(codegen 生成被屏蔽指令 → 编译错误) 5. **BytecodeExecutor `--ops`**:可选加载 / `Fault::Disabled` -6. **测试**:`ops_test`(正例 + 缺属性/字段数错/重号/与枚举不一致负例)、屏蔽传导用例、11 个 ctest 回归 +6. **测试**:`ops_test`(正例 + 缺属性/字段数错/重号/互锁破坏/与枚举不一致负例)、屏蔽传导用例、11 个 ctest 回归 7. **文档 + 提交** - -## 待讨论(未定案) - -- 内建 FB 注册是否并入 `ops.txt`(关键字/字段布局/CAL 操作码一行登记)——若并入,加 FB = 配置文件一行 + VM 一个语义 case,编译器免重建。讨论定案后另行实施,本次只做指令表。 From 35dabf4c3baa113ef4f8b540505d2ec751aa7711 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 21:13:36 +0800 Subject: [PATCH 51/69] =?UTF-8?q?=E6=AD=A5=E9=AA=A4=201=EF=BC=9AOpFormat/O?= =?UTF-8?q?pClass=20=E6=98=BE=E5=BC=8F=E5=8C=96=20+=20disasm=20=E8=A1=A8?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Op.h:新增 OpFormat(RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE)与 OpClass(Plain/Instance)枚举;34 条编译期表 kOpDefs(name/format/class, 与 ops.txt 强校验的基准);mnemonic/format/op_class 查表 - Encode.cpp:disasm 按 format 表驱动输出(文本与之前逐条一致) - 验证:11 个 ctest 全绿;表查询断言(8 个 INSTANCE=CAL、越界哨兵) --- isa/include/isa/Op.h | 88 +++++++++++++++++++++++++++++++++++++------- isa/src/Encode.cpp | 49 ++++++------------------ 2 files changed, 86 insertions(+), 51 deletions(-) diff --git a/isa/include/isa/Op.h b/isa/include/isa/Op.h index cfa3d83..a28440d 100644 --- a/isa/include/isa/Op.h +++ b/isa/include/isa/Op.h @@ -53,20 +53,82 @@ namespace isa { // 操作码总数 static const int kOpCount = 34; - // 助记符:下标与 Op 数值一一对应 + // 操作数形态(三层分类第二层,见 Doc/isa/指令配置.md) + enum class OpFormat : uint8_t { + RR, // 两寄存器:rd rs(MOVE / NOT) + RRR, // 三寄存器:rd ra rb(逻辑 / 算术 / 比较) + IMM, // 常量:rd const_id(LOADK) + SLOT, // 槽:rd slot(LOAD_* / STORE_*) + JMP, // 偏移:off + JC, // 条件跳转:r off(JT / JF) + CALL, // 调用:fn_id + CAL, // 实例:instance(CAL_*) + NONE, // 无操作数(RET) + }; + + // 操作对象大类(三层分类第一层) + enum class OpClass : uint8_t { + Plain, // 无实例:寄存器 / 立即数 / 槽 / 分支 / 调用 + Instance, // 有实例:操作数据区实例块(内建 FB) + }; + + // 编译期指令表:下标与 Op 数值一一对应(ops.txt 强校验的基准) + struct OpDef { + const char* name; + OpFormat format; + OpClass cls; + }; + + static const OpDef kOpDefs[kOpCount] = { + {"MOVE", OpFormat::RR, OpClass::Plain}, // 0 + {"LOADK", OpFormat::IMM, OpClass::Plain}, // 1 + {"NOT", OpFormat::RR, OpClass::Plain}, // 2 + {"AND", OpFormat::RRR, OpClass::Plain}, // 3 + {"OR", OpFormat::RRR, OpClass::Plain}, // 4 + {"ADD", OpFormat::RRR, OpClass::Plain}, // 5 + {"SUB", OpFormat::RRR, OpClass::Plain}, // 6 + {"MUL", OpFormat::RRR, OpClass::Plain}, // 7 + {"DIV", OpFormat::RRR, OpClass::Plain}, // 8 + {"CMP_EQ", OpFormat::RRR, OpClass::Plain}, // 9 + {"CMP_NE", OpFormat::RRR, OpClass::Plain}, // 10 + {"CMP_LT", OpFormat::RRR, OpClass::Plain}, // 11 + {"CMP_LE", OpFormat::RRR, OpClass::Plain}, // 12 + {"CMP_GT", OpFormat::RRR, OpClass::Plain}, // 13 + {"CMP_GE", OpFormat::RRR, OpClass::Plain}, // 14 + {"JMP", OpFormat::JMP, OpClass::Plain}, // 15 + {"JT", OpFormat::JC, OpClass::Plain}, // 16 + {"JF", OpFormat::JC, OpClass::Plain}, // 17 + {"LOAD_I", OpFormat::SLOT, OpClass::Plain}, // 18 + {"STORE_Q", OpFormat::SLOT, OpClass::Plain}, // 19 + {"LOAD_M", OpFormat::SLOT, OpClass::Plain}, // 20 + {"STORE_M", OpFormat::SLOT, OpClass::Plain}, // 21 + {"LOAD_GLOBAL", OpFormat::SLOT, OpClass::Plain}, // 22 + {"STORE_GLOBAL", OpFormat::SLOT, OpClass::Plain},// 23 + {"CAL_TON", OpFormat::CAL, OpClass::Instance}, // 24 + {"CAL_TOF", OpFormat::CAL, OpClass::Instance}, // 25 + {"CAL_TP", OpFormat::CAL, OpClass::Instance}, // 26 + {"CAL_CTU", OpFormat::CAL, OpClass::Instance}, // 27 + {"CAL_CTD", OpFormat::CAL, OpClass::Instance}, // 28 + {"CAL_CTUD", OpFormat::CAL, OpClass::Instance}, // 29 + {"CAL_R_TRIG", OpFormat::CAL, OpClass::Instance},// 30 + {"CAL_F_TRIG", OpFormat::CAL, OpClass::Instance},// 31 + {"CALL", OpFormat::CALL, OpClass::Plain}, // 32 + {"RET", OpFormat::NONE, OpClass::Plain}, // 33 + }; + + // 助记符 / 格式 / 大类:查表(越界返回哨兵) inline const char* mnemonic(Op op) { - static const char* const kMnemonic[kOpCount] = { - "MOVE", "LOADK", "NOT", "AND", "OR", - "ADD", "SUB", "MUL", "DIV", - "CMP_EQ", "CMP_NE", "CMP_LT", "CMP_LE", "CMP_GT", "CMP_GE", - "JMP", "JT", "JF", - "LOAD_I", "STORE_Q", "LOAD_M", "STORE_M", - "LOAD_GLOBAL", "STORE_GLOBAL", - "CAL_TON", "CAL_TOF", "CAL_TP", "CAL_CTU", "CAL_CTD", "CAL_CTUD", - "CAL_R_TRIG", "CAL_F_TRIG", - "CALL", "RET", - }; const int idx = static_cast(op); - return (idx >= 0 && idx < kOpCount) ? kMnemonic[idx] : "???"; + return (idx >= 0 && idx < kOpCount) ? kOpDefs[idx].name : "???"; + } + + inline OpFormat format(Op op) { + const int idx = static_cast(op); + return (idx >= 0 && idx < kOpCount) ? kOpDefs[idx].format : OpFormat::NONE; + } + + inline OpClass op_class(Op op) { + const int idx = static_cast(op); + return (idx >= 0 && idx < kOpCount) ? kOpDefs[idx].cls : OpClass::Plain; } } diff --git a/isa/src/Encode.cpp b/isa/src/Encode.cpp index 9990d66..fdd74a9 100644 --- a/isa/src/Encode.cpp +++ b/isa/src/Encode.cpp @@ -38,61 +38,34 @@ void disasm(Instr w, char* out, size_t cap) { } const char* m = mnemonic(o); - switch (o) { - case Op::MOVE: - case Op::NOT: + // 表驱动:按操作数形态输出(文本与 Doc/isa/指令与映像.md 一致) + switch (format(o)) { + case OpFormat::RR: snprintf(out, cap, "%s r%u, r%u", m, static_cast(rd(w)), static_cast(a(w))); break; - case Op::AND: - case Op::OR: - case Op::ADD: - case Op::SUB: - case Op::MUL: - case Op::DIV: - case Op::CMP_EQ: - case Op::CMP_NE: - case Op::CMP_LT: - case Op::CMP_LE: - case Op::CMP_GT: - case Op::CMP_GE: + case OpFormat::RRR: snprintf(out, cap, "%s r%u, r%u, r%u", m, static_cast(rd(w)), static_cast(a(w)), static_cast(b(w))); break; - case Op::LOADK: + case OpFormat::IMM: + case OpFormat::SLOT: snprintf(out, cap, "%s r%u, %u", m, static_cast(rd(w)), static_cast(imm16(w))); break; - case Op::JMP: + case OpFormat::JMP: snprintf(out, cap, "%s %+d", m, static_cast(off16(w))); break; - case Op::JT: - case Op::JF: + case OpFormat::JC: snprintf(out, cap, "%s r%u, %+d", m, static_cast(rd(w)), static_cast(off16(w))); break; - case Op::LOAD_I: - case Op::STORE_Q: - case Op::LOAD_M: - case Op::STORE_M: - case Op::LOAD_GLOBAL: - case Op::STORE_GLOBAL: - snprintf(out, cap, "%s r%u, %u", m, - static_cast(rd(w)), static_cast(imm16(w))); - break; - case Op::CAL_TON: - case Op::CAL_TOF: - case Op::CAL_TP: - case Op::CAL_CTU: - case Op::CAL_CTD: - case Op::CAL_CTUD: - case Op::CAL_R_TRIG: - case Op::CAL_F_TRIG: - case Op::CALL: + case OpFormat::CALL: + case OpFormat::CAL: snprintf(out, cap, "%s %u", m, static_cast(imm16(w))); break; - case Op::RET: + case OpFormat::NONE: snprintf(out, cap, "%s", m); break; } From 728ed9e71f7217c49fd78081075cb4e74b9aaa66 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:00:44 +0800 Subject: [PATCH 52/69] =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=AE=9A=E6=A1=88=E6=9B=B4=E6=96=B0=EF=BC=9A=E6=9C=80=E7=BB=88?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=20+=20.stb=20=E6=A0=BC=E5=BC=8F=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E4=BF=AE=E8=AE=A2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 指令配置.md 重写:架构图(machine.toml 只给编译器、vm 不读配置、isa 归执行器只管指令)、 决策清单 9 项、TOML 草案(meta/type 别名+range+tag/op/fb)、类型系统(基元内置+值域+tag)、 .stb 加固(型号标识[32]+SHA-256 文件尾各实现一份)、强校验清单、加东西流程、8 步执行计划、 4 项实现前确认 - 指令与映像.md:头 72→104 字节(型号标识[32] 偏移 72)、文件尾 SHA-256[32],12.13 修订注记 - stb文件格式.md:12.13 修订注记(落地后字节数字重新生成) --- Doc/isa/stb文件格式.md | 2 + Doc/isa/指令与映像.md | 13 +- Doc/isa/指令配置.md | 263 +++++++++++++++++++++++------------------ 3 files changed, 160 insertions(+), 118 deletions(-) diff --git a/Doc/isa/stb文件格式.md b/Doc/isa/stb文件格式.md index f7ccf98..ee4f3ec 100644 --- a/Doc/isa/stb文件格式.md +++ b/Doc/isa/stb文件格式.md @@ -2,6 +2,8 @@ 编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(224 字节)做真实拆解。 +> **12.13 修订**(尚未落地到代码,本文拆解仍为旧格式):头新增型号标识[32](头 72 → 104 字节,段偏移基准同步后移),文件尾新增 SHA-256[32]。落地后本文全部字节数字需重新生成。 + ## 概览 - 魔数 `STSC`,版本 `1`,**全部整数小端** diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 3cb6867..1805a6a 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -142,9 +142,13 @@ FB 实例固定布局,字段偏移编译期算死。 ## 映像 -魔数 `STSC`,版本 `1`。全部整数小端。文件 = 头 + 五个段,段序:常量表 → 函数表 → 字节码 → FB 布局 → 数据。偏移记在头里,段必须连续、偏移单调不减,末段终点不超过文件长度。 +魔数 `STSC`,版本 `1`。全部整数小端。文件 = 头 + 五个段 + **文件尾校验**,段序:常量表 → 函数表 → 字节码 → FB 布局 → 数据。偏移记在头里,段必须连续、偏移单调不减,末段终点不超过文件长度(文件尾在数据段之后)。 -### 头(72 字节) +> 12.13 修订:头新增 **型号标识[32]**(原头 72 字节 → 104 字节,段偏移基准同步后移); +> 文件尾新增 **SHA-256[32]**(对文件尾之前全部内容计算,compiler 写侧 / vm 读侧各自实现)。 +> 执行器读取时:型号标识与自身支持型号不匹配 → 报错;SHA-256 不匹配 → 报错(损坏/篡改)。 + +### 头(104 字节) | 偏移 | 宽 | 字段 | |---|---|---| @@ -165,6 +169,11 @@ FB 实例固定布局,字段偏移编译期算死。 | 60 | 4 | `offset_code` | | 64 | 4 | `offset_fb` | | 68 | 4 | `offset_data` | +| 72 | 32 | **型号标识**(定长 ASCII,含版本,如 `"STATOR" + "1"` 拼 32 字节补 `'\0'`) | + +### 文件尾(SHA-256) + +数据段之后追加 **32 字节 SHA-256**:对文件尾之前全部内容计算。执行器必须校验通过才运行;compiler 写入。 ### 常量表一行(12 字节) diff --git a/Doc/isa/指令配置.md b/Doc/isa/指令配置.md index 66d21e0..88baef7 100644 --- a/Doc/isa/指令配置.md +++ b/Doc/isa/指令配置.md @@ -1,15 +1,42 @@ -# 指令配置文件(ops.txt) +# 机器定义文件(machine.toml) -指令集的**唯一事实来源**:name / opcode / class / format / 参数名 / enabled。代码实现与配置文件在启动时**强校验**一致,防止两处漂移。 +指令集与类型的**唯一事实来源**,**只给编译器**(STCompiler/compiler)使用。执行器(vm)不读本文件——它是下位机程序,内建全部指令。 -## 目标 +## 架构(最终定案) -- 加指令/屏蔽指令的登记集中在一处(配置文件),不再散落在代码各处手改 -- 执行器(VM):**语义实现必须在代码里**,不能纯从文件加载;配置文件用于**屏蔽**(`enabled=false` 的指令执行到 → 故障) -- 编译器(STCompiler):**启动时从文件加载**(校验 + 屏蔽传导) -- `.stb` ABI 不变:opcode 数值仍是冻结契约;屏蔽是编译期/运行期策略,不改数值 +```text +machine.toml(只给编译器:meta + type 别名表 + op 表 + fb 表) + │ 运行时加载(toml++)+ 强校验 + ▼ +STCompiler → compiler(编码 opcode / 类型元数据 / FB 布局 全部来自配置) + │ 写 .stb(compiler 自带写实现:型号标识[32] + SHA-256 文件尾) + ▼ +.stb ──→ BytecodeExecutor → vm(执行 switch 在 vm;读 .stb 自实现) + │ + 型号匹配校验 + SHA-256 校验 + ▼ + isa(归执行器侧,只管指令定义) +``` -## 指令的三层分类(定案) +| 模块 | 职责 | +|---|---| +| `compiler` | 配置驱动编译;自带 `.stb` 写实现;`--disasm` 用配置参数名/format 自格式化 | +| `vm` | 内建全指令集(执行 switch 在 vm);自带 `.stb` 读实现 + 型号/SHA-256 校验;无屏蔽、不读配置 | +| `isa` | **归执行器侧**(vm 依赖),**只管指令定义**:Op 枚举/Instr/Encode/OpFormat/OpClass/类型/饱和/disasm。指令的具体执行实现在 vm | +| `machine.toml` | 编译器唯一知识来源 | + +## 决策清单(定案) + +1. 指令两大分类:**PLAIN**(无实例,26 条)/ **INSTANCE**(有实例,8 条 `CAL_*`) +2. 三层分类:`class`(操作对象)→ `format`(操作数形态)→ `参数名`(语义) +3. isa 归执行器侧、只管指令定义;执行 switch 留在 vm +4. 配置改 TOML、改名 machine.toml,只给编译器;vm 不需要配置 +5. 一致性靠 `.stb`:**型号标识[32]**(定长字符串含版本)+ **SHA-256 文件尾**(compiler/vm 各实现一份)+ 强校验 +6. 类型:**基元内置**(编译器内建 bit/int8..uint64/float32/float64,元数据在代码);配置类型行 = `name + base + range? + tag`(别名);**值域约束进配置**(如 BOOL range=[0,1]) +7. 字符串:**方案 A**(v1 不做;"组合类型"(含定长字符串 STRING(n))为未来扩展,需时再扩 .stb 格式) +8. `.stb` 读写:compiler 写、vm 读,**各自实现**(无共享 image 模块) +9. 冻结哈希测试更新(接受) + +## 指令三层分类 ```text class(操作对象) → format(操作数形态) → 参数名(操作数语义) @@ -17,129 +44,133 @@ class(操作对象) → format(操作数形态) → 参数名(操作 JMP/JC/CALL/CAL/NONE slot/off/fn_id/instance ``` -| 层 | 回答的问题 | 谁需要它 | +| 大类 | 操作对象 | 现有指令 | |---|---|---| -| **class** | 操作对象:**值/搬运** 还是 **实例算法** | 语义归属、校验互锁(INSTANCE ↔ CAL) | -| **format** | 操作数的**形态**:寄存器 / 常量 id / 槽号 / 带符号偏移 | disasm 渲染、编码助手 | -| **参数名** | 每个操作数的**语义命名** | 元数据、文档、校验 | +| **PLAIN**(无实例) | 寄存器 / 立即数 / 槽号 / 分支 / 调用 | MOVE/LOADK/NOT/AND/OR/算术/CMP_* / JMP/JT/JF / LOAD_*/STORE_* / CALL/RET(26 条) | +| **INSTANCE**(有实例) | 数据区实例块(内建 FB) | CAL_TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG(8 条) | -### 两个大类 +format 不可省:同是"第二个操作数是数字",`LOADK`/`LOAD_I`/`CALL` 渲染相同但语义不同(常量 id / 槽号 / fn_id,由参数名表达);"数字 vs 寄存器 vs 偏移"的渲染差异(`r%d` / `%u` / `%+d`)只有 format 能表达。 -| 大类 | 操作对象 | 现有指令 | 操作数形态 | -|---|---|---|---| -| **无实例**(PLAIN) | 寄存器 / 立即数 / 槽号 / 分支 / 调用 | MOVE/LOADK/NOT/AND/OR/算术/CMP_* / JMP/JT/JF / LOAD_*/STORE_* / CALL/RET(26 条) | RR/RRR/IMM/SLOT/JMP/JC/CALL/NONE | -| **有实例**(INSTANCE) | **数据区实例块**(内建 FB) | CAL_TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG(8 条) | CAL(一个实例基槽号) | +## 配置文件格式(TOML) -区别的本质: +**全属性必填,不可省略**: + +```toml +[meta] +name = "STATOR" # 型号名(参与 .stb 型号标识) +version = 1 # 指令集版本(参与 .stb 型号标识) + +# ---- 类型:编译器内建基元(bit/int8/int16/int32/int64/uint8/uint16/uint32/uint64/float32/float64) +# 配置类型 = 基元别名(+ 值域约束 + .stb 契约 tag) +[[type]] +name = "BOOL" +base = "uint8" +range = [0, 1] # 值域约束(语言语义数据化;缺省 = 基元全范围) +tag = 0 # 常量表契约(强校验) + +[[type]] +name = "INT" +base = "int16" +tag = 1 + +[[type]] +name = "TIME" +base = "int64" +tag = 2 + +# ---- 指令(全属性必填) +[[op]] +name = "MOVE" +opcode = 0 +class = "plain" # plain / instance +format = "RR" +params = ["rd", "rs"] +enabled = true + +[[op]] +name = "CAL_TON" +opcode = 24 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "RET" +opcode = 33 +class = "plain" +format = "NONE" +params = [] +enabled = true + +# ---- 内建 FB(布局登记;cal_opcode 与 op 行强校验一致) +[[fb]] +name = "ton" +opcode = 24 +fields = [["in", "BOOL"], ["pt", "TIME"], ["q", "BOOL"], ["et", "TIME"]] + +[[fb]] +name = "ctud" +opcode = 29 +fields = [["cu", "BOOL"], ["cd", "BOOL"], ["r", "BOOL"], ["lu", "BOOL"], + ["pv", "INT"], ["qu", "BOOL"], ["qd", "BOOL"], ["cv", "INT"]] +``` + +## 类型系统 + +- **基元内置**:编译器内建 `bit/int8/int16/int32/int64/uint8/uint16/uint32/uint64/float32/float64`(宽度/有无符号/是否浮点元数据在代码,C 语义) +- **别名**:配置类型行只写 `name + base + range? + tag`——宽度/符号/浮点从 base 继承 +- **值域约束**:`range`(如 BOOL [0,1])供编译器做常量/赋值检查;缺省 = 基元全范围 +- **tag 契约**:`tag`(0/1/2)与 `.stb` 常量表格式强校验(防 type 表改序破坏旧 .stb) +- **字符串**:v1 不做(方案 A);组合类型(含 `STRING(n)`)为未来扩展,文档预留,需时再扩 `.stb` 格式 +- 半数据化边界:**类型定义数据化**(配置),**运算/类型检查语义代码化**(如 AND 只吃 BOOL 是语言规则,在 compiler 代码;饱和算术在 vm/isa) + +## .stb 加固(格式冻结更新) + +见 [`指令与映像.md`](指令与映像.md):头新增 **型号标识[32]**(定长 ASCII 字符串,含版本,不足补 `'\0'`);文件尾新增 **SHA-256[32]**(对前面全部内容计算,compiler 写侧 / vm 读侧各自实现)。执行器读取时: ```text -LOAD_GLOBAL(无实例):从槽读一个"值" → 地址是编译期槽号,纯搬运 -CAL_TON(有实例) :对实例块执行"算法" → 基槽号 + 完整定时器语义(读字段/推进/写字段) +型号标识与自身支持型号不匹配 → 直接报错 +SHA-256 不匹配 → 报错(文件损坏/篡改) ``` -format 不可省:同是"第二个操作数是数字",`LOADK`/`LOAD_I`/`CALL` 渲染相同但语义不同(常量 id / 槽号 / fn_id,由参数名表达);"数字 vs 寄存器 vs 偏移"的**渲染差异**只有 format 能表达(`r%d` / `%u` / `%+d`)。 +## 强校验清单(加载/读取时,任一不符 → 报错退出) -## 配置文件格式(`isa/ops.txt`) +1. TOML 语法与字段完整性(全属性必填) +2. `type.base` 命中内建基元;`range` 合法(min ≤ max、类型相符) +3. `tag` 与 `.stb` 常量表契约一致(0/1/2) +4. `op.opcode` 唯一、0..255;`class` 合法;`format` 合法;**类别-格式互锁**(INSTANCE ↔ CAL) +5. `fb.opcode` 与 op 行的 CAL_* 一致;fb 字段类型命中配置类型表 +6. 型号标识(.stb 侧) -**全属性必填,不可省略**;`#` 注释、空行忽略: +## 加东西的流程 ```text -# op <参数名...> -# class: PLAIN(无实例)/ INSTANCE(有实例) -op MOVE 0 PLAIN RR rd rs enabled -op LOADK 1 PLAIN IMM rd const_id enabled -op NOT 2 PLAIN RR rd rs enabled -op AND 3 PLAIN RRR rd ra rb enabled -op ADD 5 PLAIN RRR rd ra rb enabled -op CMP_EQ 9 PLAIN RRR rd ra rb enabled -op JMP 15 PLAIN JMP off enabled -op JT 16 PLAIN JC r off enabled -op LOAD_I 18 PLAIN SLOT rd slot enabled -op CAL_TON 24 INSTANCE CAL instance enabled -op CALL 32 PLAIN CALL fn_id enabled -op RET 33 PLAIN NONE enabled +加普通指令:machine.toml 加 [[op]] 一行 + vm 一个语义 case → 重建 vm +加内建 FB :machine.toml 加 [[fb]] 一行 + vm 一个语义 case → 重建 vm(compiler 免重建) +加语言类型:machine.toml 加 [[type]] 一行(别名+tag)+ 类型检查/VM 语义代码 → 重建 +改型号版本:machine.toml [meta] version +1 → 旧 .stb 被旧/新执行器按型号校验决定是否接受 ``` -- 参数个数由 format 决定,解析时校验字段数: +## 执行计划(分支 dev_ops_config) -| format | 参数 | 现有指令 | -|---|---|---| -| `RR` | 2(rd rs) | MOVE / NOT | -| `RRR` | 3(rd ra rb) | AND/OR/ADD/SUB/MUL/DIV/CMP_xx | -| `IMM` | 2(rd const_id) | LOADK | -| `SLOT` | 2(rd slot) | LOAD_I/STORE_Q/LOAD_M/STORE_M/LOAD_GLOBAL/STORE_GLOBAL | -| `JMP` | 1(off) | JMP | -| `JC` | 2(r off) | JT / JF | -| `CALL` | 1(fn_id) | CALL | -| `CAL` | 1(instance) | CAL_*(8 个) | -| `NONE` | 0 | RET | - -- **类别与格式互锁**:`INSTANCE` 类的 format 必须是 `CAL`,反之亦然——配置错误启动期暴露 - -### 内建 FB 注册(定案:并入 ops.txt) - -`CAL_*` 已是 op 行(与 ADD 同机制:编码/译码/disasm/屏蔽);fb 行登记其**数据结构(布局)**——ST 语言层的登记处(Lexer/Parser/Linker/Codegen 查表): - -```text -# fb <字段:类型 ...> -fb ton 24 in:BOOL pt:TIME q:BOOL et:TIME -fb tof 25 in:BOOL pt:TIME q:BOOL et:TIME -fb tp 26 in:BOOL pt:TIME q:BOOL et:TIME -fb ctu 27 cu:BOOL r:BOOL pv:INT q:BOOL cv:INT -fb ctd 28 cd:BOOL ld:BOOL pv:INT q:BOOL cv:INT -fb ctud 29 cu:BOOL cd:BOOL r:BOOL lu:BOOL pv:INT qu:BOOL qd:BOOL cv:INT -fb r_trig 30 clk:BOOL q:BOOL -fb f_trig 31 clk:BOOL q:BOOL +``` +0. 文档定案(本文 + 指令与映像.md 格式冻结更新 + stb文件格式.md 修订注记) ← 当前 +1. ✅ OpFormat/OpClass 显式化 + disasm 表驱动(已完成,35dabf4) +2. machine.toml 落地:meta + type 表 + op 34 条 + fb 8 条 +3. compiler 配置驱动改造:toml++ 加载 + 强校验;编码 opcode / 类型元数据 / + FB 布局来自配置(compiler 不再依赖 isa) +4. .stb 写侧:compiler 实现 型号标识[32] + SHA-256 文件尾 +5. .stb 读侧:vm 自实现读 + 型号匹配 + SHA-256 校验(isa 的 Image 拆出) +6. CLI:STCompiler --machine (编译路径必填) +7. 测试:machine 解析/校验负例、型号不匹配报错、SHA-256 篡改检测、 + 冻结哈希更新、全量回归 +8. 文档 + 提交 ``` -- fb 行的 `cal_opcode` 与 op 行的 CAL_* 操作码**强校验一致**(防两处漂移) -- 字段类型名小写(`bool/int/time`),与语言关键字对齐 -- **加 FB = fb 行 + VM 一个语义 case,编译器免重建**(关键字/布局/操作码映射全部从表读) +## 实现前确认项(4 项,未定案) -## isa 层:`OpTable` - -```cpp -class OpTable { - bool load(const char* path, std::string* err); // 解析 + 强校验 - bool enabled(uint8_t op) const; - // name / format / 参数名查询(后续 FB 注册也走这里) -}; -``` - -**强校验(load 时)**: - -- 语法:字段数符合 format、enabled 必须出现 -- 操作码唯一、0..255 -- **与编译期枚举一致**:文件里每条指令的 name/opcode/format 必须与代码(`Op` 枚举 + 助记符 + `OpFormat`)逐条一致,任一不符 → 报错列出差异 -- 任一失败 → `load` 返回 false,调用方报错退出 - -前置改造:**OpFormat 显式化**——现有隐式"形态"(disasm 的 case)变成枚举 `RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE`;`disasm` 改表驱动(输出文本不变,现有断言兼容)。 - -## 各端接线 - -| 端 | 参数 | 行为 | -|---|---|---| -| STCompiler | `STCompiler [-o out.stb] --ops ` | **仅 `-o` 编译路径要求**;加载/校验失败 → 报错退出;codegen 生成被屏蔽指令 → 编译错误(屏蔽传导) | -| BytecodeExecutor | `--ops `(**可选**) | 不传 = 全部启用(兼容现有用法);传入 → 加载 + 校验,执行到 disabled 指令 → `Fault::Disabled`,周期中止退出码 1 | -| 语义实现 | — | 仍在 VM 代码里(每指令一个 case,不可省) | - -## 加东西的流程(改造后) - -```text -加普通指令:ops.txt 加一行 op(name opcode class format 参数 enabled) - + Op.h 枚举加一项 + VM 一个语义 case → 重建 isa/VM -加内建 FB:ops.txt 加一行 fb(name cal_opcode 字段:类型) - + VM 一个语义 case → 重建 VM(编译器免重建) -屏蔽指令:ops.txt 把 enabled 改 disabled - → 编译器拒绝生成、执行器运行时报 Disabled 故障(零代码改动) -``` - -## 执行计划(单步确认) - -1. **OpFormat / OpClass 显式化**:新增 `OpFormat`(RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE)与 `OpClass`(PLAIN/INSTANCE);`disasm` 改表驱动(输出不变,11 个 ctest 全绿) -2. **`ops.txt` 落地**:34 条 op 行 + 8 条 fb 行,全属性必填 -3. **`OpTable`**:`isa/include/isa/OpTable.h` + `isa/src/OpTable.cpp`(解析 + 强校验:语法/重号/越界/类别-格式互锁/fb-opcode 一致/与编译期枚举一致) -4. **STCompiler `--ops`**:加载/校验/屏蔽传导(codegen 生成被屏蔽指令 → 编译错误) -5. **BytecodeExecutor `--ops`**:可选加载 / `Fault::Disabled` -6. **测试**:`ops_test`(正例 + 缺属性/字段数错/重号/互锁破坏/与枚举不一致负例)、屏蔽传导用例、11 个 ctest 回归 -7. **文档 + 提交** +1. 配置文件名与位置:建议 `compiler/machine.toml`(只归编译器用) +2. 型号标识生成规则:建议 `[meta] name + version` 拼成 32 字节(如 `"STATOR" + "1"` → 补 `'\0'`);vm 内建"支持型号列表"比对 +3. compiler 内部 `TypeKind`(Bool/Int/Time):保留为语言语义枚举(类型检查规则),元数据从配置 base 查 +4. 依赖方向确认:`compiler` 不再依赖 `isa`(`compiler → toml++` 自有;`STCompiler → compiler`;`vm → isa`) From 4dab914e7c482f9ea799fda11ecb742f6f8bed5a Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:07:04 +0800 Subject: [PATCH 53/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20A=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=202=EF=BC=9Amachine.toml=20=E8=90=BD=E5=9C=B0?= =?UTF-8?q?=EF=BC=88meta/type/op/fb=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - meta:STATOR v1(参与 .stb 型号标识) - type 3 条:BOOL(uint8, range=[0,1], tag=0) / INT(int16, tag=1) / TIME(int64, tag=2) - op 34 条:全属性必填,与 kOpDefs 逐条一致(26 PLAIN + 8 INSTANCE=CAL) - fb 8 条:opcode 与 op 行一致,字段类型命中 type 表 - 核对程序通过:types=3 ops=34 fbs=8 ALL CONSISTENT --- compiler/machine.toml | 342 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 compiler/machine.toml diff --git a/compiler/machine.toml b/compiler/machine.toml new file mode 100644 index 0000000..1145146 --- /dev/null +++ b/compiler/machine.toml @@ -0,0 +1,342 @@ +# 机器定义:指令集与类型的唯一事实来源(只给编译器) +# 格式见 Doc/isa/指令配置.md;全属性必填,与代码强校验一致。 + +[meta] +name = "STATOR" # 型号名(参与 .stb 型号标识[32]) +version = 1 # 指令集版本(参与 .stb 型号标识[32]) + +# ---- 类型:编译器内建基元(bit/int8/int16/int32/int64/uint8/uint16/uint32/uint64/float32/float64) +# 配置类型 = 基元别名(+ 值域约束 + .stb 契约 tag) + +[[type]] +name = "BOOL" +base = "uint8" +range = [0, 1] # 值域约束:只允许 0/1 +tag = 0 + +[[type]] +name = "INT" +base = "int16" +tag = 1 + +[[type]] +name = "TIME" +base = "int64" +tag = 2 + +# ---- 指令(全属性必填;class: plain/instance;format: RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE) + +[[op]] +name = "MOVE" +opcode = 0 +class = "plain" +format = "RR" +params = ["rd", "rs"] +enabled = true + +[[op]] +name = "LOADK" +opcode = 1 +class = "plain" +format = "IMM" +params = ["rd", "const_id"] +enabled = true + +[[op]] +name = "NOT" +opcode = 2 +class = "plain" +format = "RR" +params = ["rd", "rs"] +enabled = true + +[[op]] +name = "AND" +opcode = 3 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "OR" +opcode = 4 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "ADD" +opcode = 5 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "SUB" +opcode = 6 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "MUL" +opcode = 7 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "DIV" +opcode = 8 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "CMP_EQ" +opcode = 9 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "CMP_NE" +opcode = 10 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "CMP_LT" +opcode = 11 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "CMP_LE" +opcode = 12 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "CMP_GT" +opcode = 13 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "CMP_GE" +opcode = 14 +class = "plain" +format = "RRR" +params = ["rd", "ra", "rb"] +enabled = true + +[[op]] +name = "JMP" +opcode = 15 +class = "plain" +format = "JMP" +params = ["off"] +enabled = true + +[[op]] +name = "JT" +opcode = 16 +class = "plain" +format = "JC" +params = ["r", "off"] +enabled = true + +[[op]] +name = "JF" +opcode = 17 +class = "plain" +format = "JC" +params = ["r", "off"] +enabled = true + +[[op]] +name = "LOAD_I" +opcode = 18 +class = "plain" +format = "SLOT" +params = ["rd", "slot"] +enabled = true + +[[op]] +name = "STORE_Q" +opcode = 19 +class = "plain" +format = "SLOT" +params = ["rs", "slot"] +enabled = true + +[[op]] +name = "LOAD_M" +opcode = 20 +class = "plain" +format = "SLOT" +params = ["rd", "slot"] +enabled = true + +[[op]] +name = "STORE_M" +opcode = 21 +class = "plain" +format = "SLOT" +params = ["rs", "slot"] +enabled = true + +[[op]] +name = "LOAD_GLOBAL" +opcode = 22 +class = "plain" +format = "SLOT" +params = ["rd", "slot"] +enabled = true + +[[op]] +name = "STORE_GLOBAL" +opcode = 23 +class = "plain" +format = "SLOT" +params = ["rs", "slot"] +enabled = true + +[[op]] +name = "CAL_TON" +opcode = 24 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_TOF" +opcode = 25 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_TP" +opcode = 26 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_CTU" +opcode = 27 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_CTD" +opcode = 28 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_CTUD" +opcode = 29 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_R_TRIG" +opcode = 30 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CAL_F_TRIG" +opcode = 31 +class = "instance" +format = "CAL" +params = ["instance"] +enabled = true + +[[op]] +name = "CALL" +opcode = 32 +class = "plain" +format = "CALL" +params = ["fn_id"] +enabled = true + +[[op]] +name = "RET" +opcode = 33 +class = "plain" +format = "NONE" +params = [] +enabled = true + +# ---- 内建 FB(布局登记;opcode 与 op 行强校验一致;字段类型命中 type 表) + +[[fb]] +name = "ton" +opcode = 24 +fields = [["in", "BOOL"], ["pt", "TIME"], ["q", "BOOL"], ["et", "TIME"]] + +[[fb]] +name = "tof" +opcode = 25 +fields = [["in", "BOOL"], ["pt", "TIME"], ["q", "BOOL"], ["et", "TIME"]] + +[[fb]] +name = "tp" +opcode = 26 +fields = [["in", "BOOL"], ["pt", "TIME"], ["q", "BOOL"], ["et", "TIME"]] + +[[fb]] +name = "ctu" +opcode = 27 +fields = [["cu", "BOOL"], ["r", "BOOL"], ["pv", "INT"], ["q", "BOOL"], ["cv", "INT"]] + +[[fb]] +name = "ctd" +opcode = 28 +fields = [["cd", "BOOL"], ["ld", "BOOL"], ["pv", "INT"], ["q", "BOOL"], ["cv", "INT"]] + +[[fb]] +name = "ctud" +opcode = 29 +fields = [["cu", "BOOL"], ["cd", "BOOL"], ["r", "BOOL"], ["lu", "BOOL"], + ["pv", "INT"], ["qu", "BOOL"], ["qd", "BOOL"], ["cv", "INT"]] + +[[fb]] +name = "r_trig" +opcode = 30 +fields = [["clk", "BOOL"], ["q", "BOOL"]] + +[[fb]] +name = "f_trig" +opcode = 31 +fields = [["clk", "BOOL"], ["q", "BOOL"]] From 44b5ecd46d495d092bd5a4daba07b07d5bea0ba6 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:09:39 +0800 Subject: [PATCH 54/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20A=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=203=EF=BC=9AMachineConfig=20=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=B1=82=EF=BC=88toml++=20+=20=E5=BC=BA=E6=A0=A1=E9=AA=8C=205?= =?UTF-8?q?=20=E9=A1=B9=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MachineConfig.h/cpp:解析 machine.toml → meta/types/ops/fbs 表 - 强校验:全属性必填、base 命中内建基元、range min≤max、tag 契约(BOOL=0/INT=1/TIME=2 唯一)、 opcode 唯一 0..255、类别-格式互锁(instance↔CAL)、params 数量与 format 一致、 fb.opcode 匹配 instance op、fb 字段类型命中 type 表 - 基元内置表(bit/int8..uint64/float32/64,宽度/符号/浮点) - machine_test:38 断言(正例仓库表 + 9 类负例 + 最小配置),ctest 12/12 --- compiler/CMakeLists.txt | 3 +- compiler/include/compiler/MachineConfig.h | 92 +++++ compiler/src/MachineConfig.cpp | 396 ++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/machine_test.cpp | 261 ++++++++++++++ 5 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 compiler/include/compiler/MachineConfig.h create mode 100644 compiler/src/MachineConfig.cpp create mode 100644 tests/src/machine_test.cpp diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index eecfc8e..3432b2f 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -13,7 +13,8 @@ add_library(compiler STATIC ./src/Parser.cpp ./src/Linker.cpp ./src/Typecheck.cpp - ./src/Codegen.cpp) + ./src/Codegen.cpp + ./src/MachineConfig.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) diff --git a/compiler/include/compiler/MachineConfig.h b/compiler/include/compiler/MachineConfig.h new file mode 100644 index 0000000..52f7600 --- /dev/null +++ b/compiler/include/compiler/MachineConfig.h @@ -0,0 +1,92 @@ +/** + * @file MachineConfig.h + * @brief 机器定义(machine.toml)加载与强校验 + * @author + * @date 2026-08-21 + * + * @details 见 Doc/isa/指令配置.md: + * - 配置只给编译器(toml++ 运行时加载);vm 不读配置(内建全指令集) + * - 强校验 5 项:字段必填 / type.base 命中内建基元·range 合法·tag 契约 / + * op opcode 唯一·类别-格式互锁 / fb.opcode 与 op 一致·字段类型命中 type 表 + * - 基元内置(bit/int8..uint64/float32/float64,元数据在代码) + */ + +#pragma once + +#include +#include +#include + +namespace compiler { + + // 内建基元(编译器内建,C 语义) + struct PrimType { + const char* name; + uint32_t width; // 字节 + bool is_signed; + bool is_float; + }; + + // 配置类型 = 基元别名(+ 值域约束 + .stb 契约 tag) + struct ConfigType { + std::string name; + std::string base; + bool has_range = false; + int64_t range_min = 0; + int64_t range_max = 0; + uint32_t tag = 0; + }; + + // 配置指令 + struct ConfigOp { + std::string name; + uint32_t opcode = 0; + bool is_instance = false; // class: instance / plain + std::string format; // RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE + std::vector params; + bool enabled = false; + }; + + struct ConfigFbField { + std::string name; + std::string type; // 配置类型名(BOOL/INT/TIME) + }; + + struct ConfigFb { + std::string name; + uint32_t opcode = 0; + std::vector fields; + }; + + class MachineConfig { + public: + // 加载 + 强校验;失败返回 false 并写 err(前缀 "machine error") + bool load(const std::string& path, std::string* err); + + bool ok() const { return ok_; } + const std::string& model_name() const { return model_name_; } + uint32_t version() const { return version_; } + + const std::vector& types() const { return types_; } + const std::vector& ops() const { return ops_; } + const std::vector& fbs() const { return fbs_; } + + const ConfigType* find_type(const std::string& name) const; + const ConfigOp* find_op(const std::string& name) const; + const ConfigOp* find_op_by_code(uint32_t opcode) const; + bool op_enabled(uint32_t opcode) const; + const ConfigFb* find_fb(const std::string& name) const; + + // 内建基元表 + static const std::vector& prims(); + static const PrimType* find_prim(const std::string& name); + + private: + bool ok_ = false; + std::string model_name_; + uint32_t version_ = 0; + std::vector types_; + std::vector ops_; + std::vector fbs_; + }; +} diff --git a/compiler/src/MachineConfig.cpp b/compiler/src/MachineConfig.cpp new file mode 100644 index 0000000..f3c000a --- /dev/null +++ b/compiler/src/MachineConfig.cpp @@ -0,0 +1,396 @@ +/** + * @file MachineConfig.cpp + * @brief 机器定义(machine.toml)加载与强校验 + * @author + * @date 2026-08-21 + * + * @details 校验项(见 Doc/isa/指令配置.md 强校验清单 1~5): + * 1. TOML 语法与字段完整性(全属性必填) + * 2. type.base 命中内建基元;range 合法(min ≤ max) + * 3. tag 与 .stb 常量表契约一致(BOOL=0、INT=1、TIME=2,唯一) + * 4. op.opcode 唯一、0..255;class/format 合法;类别-格式互锁(INSTANCE ↔ CAL); + * params 数量与 format 一致 + * 5. fb.opcode 与 op 行(instance 类)一致;字段类型命中配置类型表;字段名唯一 + */ + +#include "compiler/MachineConfig.h" + +#include + +#include +#include + +namespace compiler { + +const std::vector& MachineConfig::prims() { + static const std::vector kPrims = { + {"bit", 1, false, false}, + {"int8", 1, true, false}, + {"int16", 2, true, false}, + {"int32", 4, true, false}, + {"int64", 8, true, false}, + {"uint8", 1, false, false}, + {"uint16", 2, false, false}, + {"uint32", 4, false, false}, + {"uint64", 8, false, false}, + {"float32", 4, true, true}, + {"float64", 8, true, true}, + }; + return kPrims; +} + +const PrimType* MachineConfig::find_prim(const std::string& name) { + for (const PrimType& p : prims()) { + if (p.name == name) { + return &p; + } + } + return nullptr; +} + +namespace { + + // 稳定前缀 + std::string fail(std::string* err, const std::string& msg) { + if (err) { + *err = "machine error: " + msg; + } + return msg; + } + + // format → 参数数量(解析时校验) + int params_of(const std::string& format) { + if (format == "RR") return 2; + if (format == "RRR") return 3; + if (format == "IMM") return 2; + if (format == "SLOT") return 2; + if (format == "JMP") return 1; + if (format == "JC") return 2; + if (format == "CALL") return 1; + if (format == "CAL") return 1; + if (format == "NONE") return 0; + return -1; + } + + bool is_known_format(const std::string& f) { + return params_of(f) >= 0; + } + + bool is_known_tag(uint32_t tag) { + return tag <= 2; // .stb 常量表契约:0=BOOL 1=INT 2=TIME + } + + // 必填字符串 + bool req_string(const toml::table& t, const char* what, const std::string& key, + std::string* out, std::string* err) { + const auto nv = t[key]; + if (!nv || !nv.is_string()) { + fail(err, std::string("missing field '") + key + "' in " + what); + return false; + } + *out = nv.value_or(std::string()); + return true; + } + + // 必填整数 + bool req_int(const toml::table& t, const char* what, const std::string& key, + int64_t* out, std::string* err) { + const auto nv = t[key]; + if (!nv || !nv.is_integer()) { + fail(err, std::string("missing field '") + key + "' in " + what); + return false; + } + *out = nv.value_or(0); + return true; + } + + // 必填布尔 + bool req_bool(const toml::table& t, const char* what, const std::string& key, + bool* out, std::string* err) { + const auto nv = t[key]; + if (!nv || !nv.is_boolean()) { + fail(err, std::string("missing field '") + key + "' in " + what); + return false; + } + *out = nv.value_or(false); + return true; + } + +} // namespace + +bool MachineConfig::load(const std::string& path, std::string* err) { + ok_ = false; + model_name_.clear(); + version_ = 0; + types_.clear(); + ops_.clear(); + fbs_.clear(); + + toml::table root; + try { + root = toml::parse_file(path); + } catch (const toml::parse_error& e) { + fail(err, std::string("parse error: ") + std::string(e.description())); + return false; + } + + // ---- meta ---- + const toml::table* meta = root["meta"].as_table(); + if (!meta) { + fail(err, "missing table 'meta'"); + return false; + } + if (!req_string(*meta, "[meta]", "name", &model_name_, err)) return false; + int64_t ver = 0; + if (!req_int(*meta, "[meta]", "version", &ver, err)) return false; + if (ver < 0 || ver > 0xFFFFFFFFLL) { + fail(err, "invalid version in [meta]"); + return false; + } + version_ = static_cast(ver); + + // ---- type ---- + { + const toml::array* arr = root["type"].as_array(); + if (!arr || arr->empty()) { + fail(err, "missing 'type' table"); + return false; + } + std::map tag_owner; // tag → 类型名 + for (const auto& el : *arr) { + const toml::table* t = el.as_table(); + if (!t) { + fail(err, "bad entry in type"); + return false; + } + ConfigType ct; + if (!req_string(*t, "type", "name", &ct.name, err)) return false; + if (!req_string(*t, "type", "base", &ct.base, err)) return false; + if (find_prim(ct.base) == nullptr) { + fail(err, "type '" + ct.name + "': unknown base '" + ct.base + "'"); + return false; + } + int64_t tag = 0; + if (!req_int(*t, "type", "tag", &tag, err)) return false; + if (tag < 0 || !is_known_tag(static_cast(tag))) { + fail(err, "type '" + ct.name + "': tag out of contract (0..2)"); + return false; + } + ct.tag = static_cast(tag); + if (tag_owner.count(ct.tag)) { + fail(err, "duplicate tag " + std::to_string(ct.tag)); + return false; + } + tag_owner[ct.tag] = ct.name; + if (t->contains("range")) { + const toml::array* r = (*t)["range"].as_array(); + if (!r || r->size() != 2) { + fail(err, "type '" + ct.name + "': range must be [min, max]"); + return false; + } + ct.has_range = true; + ct.range_min = (*r)[0].value_or(0); + ct.range_max = (*r)[1].value_or(0); + if (ct.range_min > ct.range_max) { + fail(err, "type '" + ct.name + "': range min > max"); + return false; + } + } + types_.push_back(ct); + } + // .stb 契约:BOOL/INT/TIME 必须存在且 tag 为 0/1/2 + const std::string need[3] = {"BOOL", "INT", "TIME"}; + for (uint32_t i = 0; i < 3; ++i) { + const auto it = tag_owner.find(i); + if (it == tag_owner.end() || it->second != need[i]) { + fail(err, std::string("type contract broken: tag ") + std::to_string(i) + + " must be " + need[i]); + return false; + } + } + } + + // ---- op ---- + { + const toml::array* arr = root["op"].as_array(); + if (!arr || arr->empty()) { + fail(err, "missing 'op' table"); + return false; + } + std::map code_owner; + std::map name_owner; + for (const auto& el : *arr) { + const toml::table* t = el.as_table(); + if (!t) { + fail(err, "bad entry in op"); + return false; + } + ConfigOp op; + if (!req_string(*t, "op", "name", &op.name, err)) return false; + int64_t code = 0; + if (!req_int(*t, "op", "opcode", &code, err)) return false; + if (code < 0 || code > 255) { + fail(err, "op '" + op.name + "': opcode out of range"); + return false; + } + op.opcode = static_cast(code); + if (code_owner.count(op.opcode)) { + fail(err, "duplicate opcode " + std::to_string(op.opcode)); + return false; + } + code_owner[op.opcode] = op.name; + if (name_owner.count(op.name)) { + fail(err, "duplicate op name '" + op.name + "'"); + return false; + } + name_owner[op.name] = op.opcode; + std::string cls; + if (!req_string(*t, "op", "class", &cls, err)) return false; + if (cls == "instance") { + op.is_instance = true; + } else if (cls == "plain") { + op.is_instance = false; + } else { + fail(err, "op '" + op.name + "': bad class '" + cls + "'"); + return false; + } + if (!req_string(*t, "op", "format", &op.format, err)) return false; + if (!is_known_format(op.format)) { + fail(err, "op '" + op.name + "': bad format '" + op.format + "'"); + return false; + } + // 类别-格式互锁:INSTANCE ↔ CAL + const bool is_cal = op.format == "CAL"; + if (op.is_instance != is_cal) { + fail(err, "op '" + op.name + "': class-format mismatch (instance <-> CAL)"); + return false; + } + if (!req_bool(*t, "op", "enabled", &op.enabled, err)) return false; + const auto pv = (*t)["params"]; + if (!pv || !pv.is_array()) { + fail(err, "op '" + op.name + "': missing 'params'"); + return false; + } + const int want = params_of(op.format); + for (const auto& p : *pv.as_array()) { + if (!p.is_string()) { + fail(err, "op '" + op.name + "': params must be strings"); + return false; + } + op.params.push_back(p.value_or(std::string())); + } + if (static_cast(op.params.size()) != want) { + fail(err, "op '" + op.name + "': params count " + + std::to_string(op.params.size()) + " != format expects " + + std::to_string(want)); + return false; + } + ops_.push_back(op); + } + } + + // ---- fb ---- + { + const toml::array* arr = root["fb"].as_array(); + if (!arr || arr->empty()) { + fail(err, "missing 'fb' table"); + return false; + } + for (const auto& el : *arr) { + const toml::table* t = el.as_table(); + if (!t) { + fail(err, "bad entry in fb"); + return false; + } + ConfigFb fb; + if (!req_string(*t, "fb", "name", &fb.name, err)) return false; + int64_t code = 0; + if (!req_int(*t, "fb", "opcode", &code, err)) return false; + fb.opcode = static_cast(code); + // opcode 必须命中 instance 类 op 行 + const ConfigOp* op = find_op_by_code(fb.opcode); + if (op == nullptr || !op->is_instance) { + fail(err, "fb '" + fb.name + "': opcode " + std::to_string(code) + + " must match an instance op"); + return false; + } + const auto fv = (*t)["fields"]; + if (!fv || !fv.is_array()) { + fail(err, "fb '" + fb.name + "': missing 'fields'"); + return false; + } + std::map fnames; + for (const auto& f : *fv.as_array()) { + const toml::array* pair = f.as_array(); + if (!pair || pair->size() != 2) { + fail(err, "fb '" + fb.name + "': field must be [name, type]"); + return false; + } + ConfigFbField field; + field.name = (*pair)[0].value_or(std::string()); + field.type = (*pair)[1].value_or(std::string()); + if (field.name.empty() || field.type.empty()) { + fail(err, "fb '" + fb.name + "': empty field name/type"); + return false; + } + if (fnames.count(field.name)) { + fail(err, "fb '" + fb.name + "': duplicate field '" + field.name + "'"); + return false; + } + fnames[field.name] = true; + if (find_type(field.type) == nullptr) { + fail(err, "fb '" + fb.name + "': unknown field type '" + field.type + "'"); + return false; + } + fb.fields.push_back(field); + } + fbs_.push_back(fb); + } + } + + ok_ = true; + return true; +} + +const ConfigType* MachineConfig::find_type(const std::string& name) const { + for (const ConfigType& t : types_) { + if (t.name == name) { + return &t; + } + } + return nullptr; +} + +const ConfigOp* MachineConfig::find_op(const std::string& name) const { + for (const ConfigOp& o : ops_) { + if (o.name == name) { + return &o; + } + } + return nullptr; +} + +const ConfigOp* MachineConfig::find_op_by_code(uint32_t opcode) const { + for (const ConfigOp& o : ops_) { + if (o.opcode == opcode) { + return &o; + } + } + return nullptr; +} + +bool MachineConfig::op_enabled(uint32_t opcode) const { + const ConfigOp* op = find_op_by_code(opcode); + return op != nullptr && op->enabled; +} + +const ConfigFb* MachineConfig::find_fb(const std::string& name) const { + for (const ConfigFb& f : fbs_) { + if (f.name == name) { + return &f; + } + } + return nullptr; +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7f96223..c8032fa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -100,3 +100,14 @@ target_compile_definitions(cases_test PRIVATE add_test(NAME cases_all COMMAND cases_test) + +# machine.toml 加载与强校验(阶段 A 步骤 3;REPO_ROOT 注入源目录绝对路径) +add_executable(machine_test + ./src/machine_test.cpp) + +target_link_libraries(machine_test PRIVATE compiler) +target_compile_definitions(machine_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME machine_config + COMMAND machine_test) diff --git a/tests/src/machine_test.cpp b/tests/src/machine_test.cpp new file mode 100644 index 0000000..1cdcb3b --- /dev/null +++ b/tests/src/machine_test.cpp @@ -0,0 +1,261 @@ +/** + * @file machine_test.cpp + * @brief machine.toml 加载与强校验测试(阶段 A 步骤 3) + * @author + * @date 2026-08-21 + */ + +#include +#include +#include + +#include "compiler/MachineConfig.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 写临时 toml ---- + +static std::string write_tmp(const char* name, const char* content) { + const std::filesystem::path dir = std::filesystem::temp_directory_path(); + const std::filesystem::path path = dir / name; + std::FILE* f = std::fopen(path.string().c_str(), "w"); + std::fputs(content, f); + std::fclose(f); + return path.string(); +} + +static bool expect_load_err(const char* name, const char* content, const char* keyword) { + const std::string path = write_tmp(name, content); + compiler::MachineConfig cfg; + std::string err; + if (cfg.load(path, &err)) { + std::printf("FAIL %s: loaded ok\n", name); + std::remove(path.c_str()); + return false; + } + if (err.find("machine error") != 0) { + std::printf("FAIL %s: want 'machine error', got '%s'\n", name, err.c_str()); + std::remove(path.c_str()); + return false; + } + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL %s: want '%s', got '%s'\n", name, keyword, err.c_str()); + std::remove(path.c_str()); + return false; + } + ++g_checks; + std::remove(path.c_str()); + return true; +} + +// 合法内容模板(可注入损坏) +static const char* kGood = + "[meta]\nname = \"STATOR\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\nrange = [0, 1]\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"MOVE\"\nopcode = 0\nclass = \"plain\"\nformat = \"RR\"\nparams = [\"rd\", \"rs\"]\nenabled = true\n" + "[[op]]\nname = \"CAL_TON\"\nopcode = 24\nclass = \"instance\"\nformat = \"CAL\"\nparams = [\"instance\"]\nenabled = true\n" + "[[op]]\nname = \"RET\"\nopcode = 33\nclass = \"plain\"\nformat = \"NONE\"\nparams = []\nenabled = true\n" + "[[fb]]\nname = \"ton\"\nopcode = 24\nfields = [[\"in\", \"BOOL\"], [\"pt\", \"TIME\"], [\"q\", \"BOOL\"], [\"et\", \"TIME\"]]\n"; + +// ---- 1. 正例:仓库 machine.toml ---- + +static bool test_positive() { + compiler::MachineConfig cfg; + std::string err; + CHECK(cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &err)); + CHECK(cfg.ok()); + CHECK(cfg.model_name() == "STATOR"); + CHECK(cfg.version() == 1); + + CHECK(cfg.types().size() == 3); + const compiler::ConfigType* bt = cfg.find_type("BOOL"); + CHECK(bt != nullptr && bt->base == "uint8" && bt->has_range && bt->range_min == 0 && + bt->range_max == 1 && bt->tag == 0); + CHECK(cfg.find_type("INT") != nullptr && cfg.find_type("INT")->tag == 1); + CHECK(cfg.find_type("TIME") != nullptr && cfg.find_type("TIME")->tag == 2); + CHECK(cfg.find_type("REAL") == nullptr); + + CHECK(cfg.ops().size() == 34); + const compiler::ConfigOp* add = cfg.find_op("ADD"); + CHECK(add != nullptr && add->opcode == 5 && add->format == "RRR" && !add->is_instance && + add->params.size() == 3); + const compiler::ConfigOp* ton = cfg.find_op("CAL_TON"); + CHECK(ton != nullptr && ton->opcode == 24 && ton->is_instance && ton->format == "CAL"); + CHECK(cfg.find_op("CAL_F_TRIG") != nullptr && cfg.find_op("CAL_F_TRIG")->opcode == 31); + CHECK(cfg.find_op_by_code(33) != nullptr && cfg.find_op_by_code(33)->name == "RET"); + CHECK(cfg.op_enabled(0) && cfg.op_enabled(24)); + CHECK(cfg.find_op("NOPE") == nullptr); + + CHECK(cfg.fbs().size() == 8); + const compiler::ConfigFb* ctud = cfg.find_fb("ctud"); + CHECK(ctud != nullptr && ctud->opcode == 29 && ctud->fields.size() == 8); + CHECK(cfg.find_fb("r_trig") != nullptr && cfg.find_fb("r_trig")->fields.size() == 2); + + // 基元表 + CHECK(compiler::MachineConfig::find_prim("int16") != nullptr); + const compiler::PrimType* p = compiler::MachineConfig::find_prim("uint8"); + CHECK(p != nullptr && p->width == 1 && !p->is_signed && !p->is_float); + CHECK(compiler::MachineConfig::find_prim("float64") != nullptr); + CHECK(compiler::MachineConfig::find_prim("bfloat16") == nullptr); + return true; +} + +// ---- 2. 负例:9 类 ---- + +static bool test_negative() { + // 缺属性(enabled 缺失) + if (!expect_load_err( + "mc_bad1.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"MOVE\"\nopcode = 0\nclass = \"plain\"\nformat = \"RR\"\nparams = [\"rd\", \"rs\"]\n", + "missing field 'enabled'")) { + return false; + } + // base 未命中基元 + if (!expect_load_err( + "mc_bad2.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"bigint\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"RET\"\nopcode = 33\nclass = \"plain\"\nformat = \"NONE\"\nparams = []\nenabled = true\n", + "unknown base")) { + return false; + } + // range min > max + if (!expect_load_err( + "mc_bad3.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\nrange = [1, 0]\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"RET\"\nopcode = 33\nclass = \"plain\"\nformat = \"NONE\"\nparams = []\nenabled = true\n", + "range min > max")) { + return false; + } + // tag 契约破坏(BOOL tag=5) + if (!expect_load_err( + "mc_bad4.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 5\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"RET\"\nopcode = 33\nclass = \"plain\"\nformat = \"NONE\"\nparams = []\nenabled = true\n", + "tag out of contract")) { + return false; + } + // tag 契约破坏(INT 占 tag 0) + if (!expect_load_err( + "mc_bad5.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 1\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 0\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"RET\"\nopcode = 33\nclass = \"plain\"\nformat = \"NONE\"\nparams = []\nenabled = true\n", + "type contract broken")) { + return false; + } + // opcode 重号 + if (!expect_load_err( + "mc_bad6.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"MOVE\"\nopcode = 0\nclass = \"plain\"\nformat = \"RR\"\nparams = [\"rd\", \"rs\"]\nenabled = true\n" + "[[op]]\nname = \"NOPE\"\nopcode = 0\nclass = \"plain\"\nformat = \"RR\"\nparams = [\"a\", \"b\"]\nenabled = true\n", + "duplicate opcode")) { + return false; + } + // 类别-格式互锁破坏(instance + RRR) + if (!expect_load_err( + "mc_bad7.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"BAD\"\nopcode = 24\nclass = \"instance\"\nformat = \"RRR\"\nparams = [\"a\", \"b\", \"c\"]\nenabled = true\n", + "class-format mismatch")) { + return false; + } + // params 数量与 format 不符 + if (!expect_load_err( + "mc_bad8.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"MOVE\"\nopcode = 0\nclass = \"plain\"\nformat = \"RR\"\nparams = [\"rd\"]\nenabled = true\n", + "params count")) { + return false; + } + // fb.opcode 不匹配 instance op + if (!expect_load_err( + "mc_bad9.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"RET\"\nopcode = 33\nclass = \"plain\"\nformat = \"NONE\"\nparams = []\nenabled = true\n" + "[[fb]]\nname = \"ton\"\nopcode = 33\nfields = [[\"in\", \"BOOL\"]]\n", + "must match an instance op")) { + return false; + } + // fb 字段类型未命中 type 表 + if (!expect_load_err( + "mc_bad10.toml", + "[meta]\nname = \"S\"\nversion = 1\n" + "[[type]]\nname = \"BOOL\"\nbase = \"uint8\"\ntag = 0\n" + "[[type]]\nname = \"INT\"\nbase = \"int16\"\ntag = 1\n" + "[[type]]\nname = \"TIME\"\nbase = \"int64\"\ntag = 2\n" + "[[op]]\nname = \"CAL_TON\"\nopcode = 24\nclass = \"instance\"\nformat = \"CAL\"\nparams = [\"instance\"]\nenabled = true\n" + "[[fb]]\nname = \"ton\"\nopcode = 24\nfields = [[\"in\", \"REAL\"]]\n", + "unknown field type")) { + return false; + } + // 语法错误 + if (!expect_load_err("mc_bad11.toml", "[meta\nname = \"S\"\n", "parse error")) { + return false; + } + return true; +} + +// ---- 3. 合法最小配置正例 ---- + +static bool test_positive_min() { + const std::string path = write_tmp("mc_good.toml", kGood); + compiler::MachineConfig cfg; + std::string err; + CHECK(cfg.load(path, &err)); + CHECK(cfg.ops().size() == 3); + CHECK(cfg.fbs().size() == 1); + CHECK(cfg.find_fb("ton")->fields.size() == 4); + std::remove(path.c_str()); + return true; +} + +int main() { + if (!test_positive()) return 1; + if (!test_negative()) return 1; + if (!test_positive_min()) return 1; + std::printf("machine_test: %d checks passed\n", g_checks); + return 0; +} From 447a8d011947b5ef8c24697b306854095b23f27a Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:14:29 +0800 Subject: [PATCH 55/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20B=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=204=EF=BC=9Acompiler=20=E9=85=8D=E7=BD=AE=E9=A9=B1?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=EF=BC=8C=E5=8E=BB=E9=99=A4=20isa=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Project.h 自带 IoBinding;Stb.h/cpp:compiler 侧 .stb 规范(常量/ConstEntry/FNV/StbView/读写/sidecar) - Codec.h/cpp:指令字 pack/字段/配置驱动 disasm(format 表驱动) - Codegen:opcode 全查 MachineConfig(E_* 发射器)、常量 tag 查配置类型、初值按基元宽度写、 映像拼装用 Stb 常量;codegen_project 增加 cfg 参数 - main.cpp:--machine (编译/反汇编必填),--disasm 用 StbView+Codec - CMake:compiler 不再链接 isa(仅 toml++ 私有);测试补链 isa(自身断言用) - 验证:20 用例字节不变(hash 0xc3fe4ae74ad45900 相同)、ctest 12/12、缺 --machine 报错退出 --- compiler/CMakeLists.txt | 6 +- compiler/include/compiler/Codec.h | 36 ++ compiler/include/compiler/Codegen.h | 6 +- compiler/include/compiler/Project.h | 15 +- compiler/include/compiler/Stb.h | 103 ++++++ compiler/src/Codec.cpp | 65 ++++ compiler/src/Codegen.cpp | 535 +++++++++++----------------- compiler/src/Linker.cpp | 2 +- compiler/src/Project.cpp | 8 +- compiler/src/Stb.cpp | 225 ++++++++++++ compiler/src/main.cpp | 92 +++-- tests/CMakeLists.txt | 2 +- tests/src/cases_test.cpp | 7 +- tests/src/codegen_test.cpp | 21 +- tests/src/vm_test.cpp | 6 +- 15 files changed, 739 insertions(+), 390 deletions(-) create mode 100644 compiler/include/compiler/Codec.h create mode 100644 compiler/include/compiler/Stb.h create mode 100644 compiler/src/Codec.cpp create mode 100644 compiler/src/Stb.cpp diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 3432b2f..966e3d3 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -7,6 +7,7 @@ project(Compiler DESCRIPTION "ST / toml 编译器") # 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 +# 指令 opcode / 类型 / FB 布局来自 machine.toml(MachineConfig),不依赖 isa add_library(compiler STATIC ./src/Lexer.cpp ./src/Project.cpp @@ -14,11 +15,12 @@ add_library(compiler STATIC ./src/Linker.cpp ./src/Typecheck.cpp ./src/Codegen.cpp - ./src/MachineConfig.cpp) + ./src/MachineConfig.cpp + ./src/Stb.cpp + ./src/Codec.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) -target_link_libraries(compiler PUBLIC isa) # 可执行入口:STCompiler -o .stb add_executable(STCompiler diff --git a/compiler/include/compiler/Codec.h b/compiler/include/compiler/Codec.h new file mode 100644 index 0000000..f62abc7 --- /dev/null +++ b/compiler/include/compiler/Codec.h @@ -0,0 +1,36 @@ +/** + * @file Codec.h + * @brief 指令字编解码 + 配置驱动反汇编(编译器侧) + * @author + * @date 2026-08-21 + * + * @details 编译器不依赖 isa:指令字布局([op:8|rd:8|a:8|b:8] 小端)与 + * 反汇编输出(按 machine.toml 的 format/参数名)全部由本模块 + MachineConfig 提供, + * 保证与执行器(isa)产生的字节一致。 + */ + +#pragma once + +#include +#include + +#include "compiler/MachineConfig.h" + +namespace compiler { + + // 指令字:[op:8 | rd:8 | a:8 | b:8],小端 u32 + typedef uint32_t Instr; + + Instr pack(uint8_t opcode, uint8_t rd, uint8_t a, uint8_t b); + + uint8_t op_of(Instr w); + uint8_t rd_of(Instr w); + uint8_t a_of(Instr w); + uint8_t b_of(Instr w); + uint16_t imm16_of(Instr w); // a|b 拼 u16(const_id / slot / fn_id) + int16_t off16_of(Instr w); // a|b 有符号偏移(相对下一条) + + // 配置驱动反汇编:按 machine.toml 的 format 输出一行文本。 + // 未知操作码 → "??? 0x"。输出格式与 Doc/isa/指令与映像.md 一致。 + void disasm(const MachineConfig& cfg, Instr w, char* out, size_t cap); +} diff --git a/compiler/include/compiler/Codegen.h b/compiler/include/compiler/Codegen.h index ee72f53..1312f20 100644 --- a/compiler/include/compiler/Codegen.h +++ b/compiler/include/compiler/Codegen.h @@ -12,14 +12,16 @@ #include #include "compiler/Linker.h" +#include "compiler/MachineConfig.h" #include "compiler/Parser.h" #include "compiler/Project.h" namespace compiler { // 编译工程为 .stb 映像字节(在链接 + 类型检查成功后调用)。 + // 指令 opcode / 类型 tag / FB 布局全部来自 machine.toml(cfg)。 // 失败返回 false,err 前缀 "codegen error"。 bool codegen_project(const Project& proj, const std::vector& units, - const LinkResult& link, std::vector* image, - std::string* err); + const LinkResult& link, const MachineConfig& cfg, + std::vector* image, std::string* err); } diff --git a/compiler/include/compiler/Project.h b/compiler/include/compiler/Project.h index eb9e24f..81bbe4e 100644 --- a/compiler/include/compiler/Project.h +++ b/compiler/include/compiler/Project.h @@ -11,10 +11,17 @@ #include #include -#include "isa/Image.h" - namespace compiler { + // I/O 绑定(project.toml [[io.*]];不创造变量,slot 由链接结果解析) + struct IoBinding { + std::string var; + uint32_t slot = 0; + uint32_t channel = 0; + uint32_t bit = 0; + bool is_input = false; // true = [[io.input]],false = [[io.output]] + }; + // 与 Doc/初步计划.md 12.1 的 toml 字段表一一对应。 // 字段白名单 / 必填 / io 完整性在 parse_project 内校验。 struct Project { @@ -24,7 +31,7 @@ namespace compiler { uint32_t dt_ms = 0; // 必填 > 0 std::vector files_st; // [files] 必填非空 std::string gvl_file; // [gvl] 可选;无则空串 - std::vector io; // [[io.*]] 可选;slot 12.6 才解析,此处填 0 + std::vector io; // [[io.*]] 可选;slot 12.6 才解析,此处填 0 std::string base_dir; // toml 所在目录(解析相对路径用) }; @@ -37,6 +44,6 @@ namespace compiler { std::vector compile_files(const Project& p); // 校验全部文件存在并计算工程哈希:集合按路径排序,对内容做 FNV-1a 64 增量; - // 空集合 = basis(isa::kFnvBasis)。缺文件报错前缀 "file missing"。 + // 空集合 = basis(Stb::kFnvBasis)。缺文件报错前缀 "file missing"。 bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err); } diff --git a/compiler/include/compiler/Stb.h b/compiler/include/compiler/Stb.h new file mode 100644 index 0000000..af0070d --- /dev/null +++ b/compiler/include/compiler/Stb.h @@ -0,0 +1,103 @@ +/** + * @file Stb.h + * @brief 编译器自带的 .stb 映像规范(写侧)+ 只读视图 + FNV-1a + * @author + * @date 2026-08-21 + * + * @details 执行器(vm)各有自己的读写实现;格式契约见 Doc/isa/指令与映像.md。 + * 12.13 修订(型号标识[32] 头 104 字节 + SHA-256 文件尾)在后续步骤落地。 + */ + +#pragma once + +#include +#include +#include +#include + +#include "compiler/Project.h" + +namespace compiler { + + // ---- .stb 格式常量(契约)---- + static const uint32_t kMagic = 0x43545353u; // "STSC" 小端 + static const uint32_t kVersion = 1; + static const size_t kHeaderSize = 72; + static const size_t kConstEntrySize = 12; + static const size_t kFuncRowSize = 12; + static const uint64_t kFnvBasis = 0xcbf29ce484222325ull; + static const uint64_t kFnvPrime = 0x100000001b3ull; + + // 常量表一行(tag:0=BOOL、1=INT、2=TIME,契约) + struct ConstEntry { + uint32_t tag = 0; + uint64_t value = 0; + }; + + // FNV-1a 64(工程哈希,非密码学) + uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len); + uint64_t fnv1a64(const uint8_t* data, size_t len); + + // 只读视图(--disasm 用;校验魔数/版本/段边界) + class StbView { + public: + struct FuncRow { + uint32_t nregs = 0; + uint32_t code_offset = 0; + uint32_t code_len = 0; + }; + + static StbView from(const uint8_t* buf, size_t len); + static StbView from(const std::vector& buf); + + bool ok() const { return ok_; } + const std::string& error() const { return err_; } + + uint32_t cycle_limit() const { return cycle_limit_; } + uint32_t dt_ms() const { return dt_ms_; } + uint64_t project_hash() const { return project_hash_; } + uint32_t entry_fn_id() const { return entry_fn_id_; } + uint32_t n_globals() const { return n_globals_; } + uint32_t n_consts() const { return n_consts_; } + uint32_t n_funcs() const { return n_funcs_; } + uint32_t offset_code() const { return offset_code_; } + uint32_t offset_fb() const { return offset_fb_; } + uint32_t offset_data() const { return offset_data_; } + + ConstEntry const_entry(size_t i) const; + FuncRow func_row(size_t i) const; + const uint8_t* code_bytes() const; + size_t code_len() const; + const uint8_t* data_bytes() const; + size_t data_len() const; + + private: + StbView() : buf_(0), len_(0) {} + + uint32_t offs_of_const() const; // offset_const(from 已校验段起点) + + const uint8_t* buf_; + size_t len_; + bool ok_ = false; + std::string err_; + uint32_t cycle_limit_ = 0; + uint32_t dt_ms_ = 0; + uint64_t project_hash_ = 0; + uint32_t entry_fn_id_ = 0; + uint32_t n_globals_ = 0; + uint32_t n_consts_ = 0; + uint32_t n_funcs_ = 0; + uint32_t offset_code_ = 0; + uint32_t offset_fb_ = 0; + uint32_t offset_data_ = 0; + }; + + // 文件读写(纯字节;校验交给 StbView) + bool read_stb_file(const char* path, std::vector* out, std::string* err); + bool write_stb_file(const char* path, const std::vector& img, std::string* err); + + // sidecar 生成与写文件(I/O 绑定 var → 槽号 → channel/bit) + std::string make_sidecar(const std::vector& bindings); + bool write_sidecar_file(const char* path, const std::vector& bindings, + std::string* err); +} diff --git a/compiler/src/Codec.cpp b/compiler/src/Codec.cpp new file mode 100644 index 0000000..0947b4c --- /dev/null +++ b/compiler/src/Codec.cpp @@ -0,0 +1,65 @@ +/** + * @file Codec.cpp + * @brief 指令字编解码 + 配置驱动反汇编(编译器侧) + * @author + * @date 2026-08-21 + */ + +#include "compiler/Codec.h" + +#include + +namespace compiler { + +Instr pack(uint8_t opcode, uint8_t rd, uint8_t a, uint8_t b) { + return static_cast(opcode) + | (static_cast(rd) << 8) + | (static_cast(a) << 16) + | (static_cast(b) << 24); +} + +uint8_t op_of(Instr w) { return static_cast(w & 0xFFu); } +uint8_t rd_of(Instr w) { return static_cast((w >> 8) & 0xFFu); } +uint8_t a_of(Instr w) { return static_cast((w >> 16) & 0xFFu); } +uint8_t b_of(Instr w) { return static_cast((w >> 24) & 0xFFu); } + +uint16_t imm16_of(Instr w) { + return static_cast(a_of(w) | (static_cast(b_of(w)) << 8)); +} + +int16_t off16_of(Instr w) { return static_cast(imm16_of(w)); } + +void disasm(const MachineConfig& cfg, Instr w, char* out, size_t cap) { + if (cap == 0) { + return; + } + out[0] = '\0'; + const ConfigOp* op = cfg.find_op_by_code(op_of(w)); + if (op == nullptr) { + snprintf(out, cap, "??? 0x%08x", static_cast(w)); + return; + } + const uint8_t rd = rd_of(w); + const std::string& f = op->format; + if (f == "RR") { + snprintf(out, cap, "%s r%u, r%u", op->name.c_str(), static_cast(rd), + static_cast(a_of(w))); + } else if (f == "RRR") { + snprintf(out, cap, "%s r%u, r%u, r%u", op->name.c_str(), static_cast(rd), + static_cast(a_of(w)), static_cast(b_of(w))); + } else if (f == "IMM" || f == "SLOT") { + snprintf(out, cap, "%s r%u, %u", op->name.c_str(), static_cast(rd), + static_cast(imm16_of(w))); + } else if (f == "JMP") { + snprintf(out, cap, "%s %+d", op->name.c_str(), static_cast(off16_of(w))); + } else if (f == "JC") { + snprintf(out, cap, "%s r%u, %+d", op->name.c_str(), static_cast(rd), + static_cast(off16_of(w))); + } else if (f == "CALL" || f == "CAL") { + snprintf(out, cap, "%s %u", op->name.c_str(), static_cast(imm16_of(w))); + } else { // NONE + snprintf(out, cap, "%s", op->name.c_str()); + } +} + +} // namespace compiler diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index eb18252..7634283 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -1,38 +1,32 @@ /** * @file Codegen.cpp - * @brief 寄存器码生成(12.8,切片 7:+ FB 实例、内联展开、CAL_*) + * @brief 寄存器码生成(配置驱动;compiler 不依赖 isa) * @author * @date 2026-08-21 * - * @details 设计说明(详见 Doc/compiler/寄存器码.md): - * - 切片 1~6:帧/字面量/MOVE/RET;全局数据区;短路 AND/OR;CMP 与 IF;WHILE 与四则; - * FUNCTION 与 CALL(调用约定 r0/r1..r7/r8+)。 - * - 切片 7:FB 实例 → 数据区实例块(跨周期持久);字段槽号 = 实例基槽 + 字段序号 - * (编译期算死);字段读写走 LOAD_GLOBAL / STORE_GLOBAL; - * 内建 TON/TOF/CTU → 实参写字段后 CAL_* <实例基槽>; - * 用户 FB → 调用点内联展开(实参写字段 → FB 体以实例字段为变量编译) - * - 方案 a(12.9 前置):数据段**每槽 8 字节定宽**,指令 slot = 槽号(无映射) + * @details 设计说明(详见 Doc/compiler/寄存器码.md 与 Doc/isa/指令配置.md): + * - 指令 opcode / 类型 tag / FB 布局全部来自 machine.toml(MachineConfig) + * - 帧/字面量/MOVE/RET;全局数据区;短路 AND/OR;CMP 与 IF;WHILE 与四则; + * FUNCTION 与 CALL(调用约定 r0/r1..r7/r8+) + * - FB 实例 → 数据区实例块(跨周期持久);字段槽号 = 实例基槽 + 字段序号 + * - 用户 FB 调用点内联展开;内建 FB → CAL_* <实例基槽> + * - 数据段每槽 8 字节定宽,指令 slot = 槽号 * * 函数清单: - * - put_le32 / put_le64 小端写入映像缓冲 - * - Builder::Builder (构造)存工程/源文件/链接结果/输出,收集 io 绑定分类 - * - Builder::run 布局数据区(全局 + 实例)→ 逐 POU 建函数 → 拼映像 - * - Builder::fail 组装 "codegen error: " 返回 false + * - Builder::Builder (构造)存工程/源文件/链接结果/机器配置/输出,收集 io 绑定 + * - Builder::run 布局数据区 → 逐 POU 建函数 → 拼映像 + * - Builder::fail / opc 错误 / 按名查配置 opcode + * - E_rr / E_rrr / E_imm / E_slot / E_jmp / E_jc / E_call / E_ret 指令发射(配置 opcode) * - find_pou / find_fn_id 按名查 POU AST / fn_id - * - build_function 编译一个 POU(PROGRAM/FUNCTION/FB 骨架;帧约定分配) - * - begin_stmt / alloc_temp 临时寄存器:语句内递增、语句结束复用基址 + * - build_function 编译一个 POU(帧约定分配;FB 占位) + * - begin_stmt / alloc_temp 临时寄存器管理 * - compile_stmt 语句编译(赋值 / IF / WHILE / FB 调用) - * - compile_if / compile_while IF 链 / WHILE 回环 - * - compile_fb_call FB 调用:内建 CAL_* 或用户内联展开 - * - compile_fb_inline 内联编译用户 FB 体(实例字段为变量) - * - store_target 赋值左值:STORE_* 到全局槽(或内联模式实例字段) - * - compile_expr 表达式编译到寄存器(含 Field 字段读) - * - cmp_op / arith_op / load_op / store_op 操作码选择 - * - global_offset 槽号恒等(方案 a 无映射) - * - instance_of 实例 → 字段槽号表 - * - patch_jump 回填跳转偏移(相对下一条指令) - * - layout_data / layout_fb_instances / init_of 数据区布局(8 字节定宽槽) - * - const_id 取常量表 id(无则追加) + * - compile_if / compile_while / compile_fb_call / compile_fb_inline + * - store_target / compile_expr 左值存储 / 表达式编译 + * - arith_name / cmp_name / load_name / store_name 操作码名选择 + * - patch_jump 回填跳转偏移(相对下一条) + * - layout_data / layout_fb_instances / instance_of / init_of + * - const_id 取常量表 id(tag 来自配置类型表) * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + 数据段 * - codegen_project 对外入口 */ @@ -45,31 +39,15 @@ #include #include -#include "isa/Encode.h" -#include "isa/Image.h" -#include "isa/Instr.h" -#include "isa/Types.h" +#include "compiler/Codec.h" +#include "compiler/MachineConfig.h" +#include "compiler/Stb.h" namespace compiler { namespace { - // ---- 小端写入(Image.cpp 内部实现不可见,这里自带最小版)---- - - void put_le32(std::vector& b, size_t off, uint32_t v) { - b[off + 0] = static_cast(v & 0xFFu); - b[off + 1] = static_cast((v >> 8) & 0xFFu); - b[off + 2] = static_cast((v >> 16) & 0xFFu); - b[off + 3] = static_cast((v >> 24) & 0xFFu); - } - - void put_le64(std::vector& b, size_t off, uint64_t v) { - for (int i = 0; i < 8; ++i) { - b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); - } - } - /** - * @brief 代码生成器(切片 1) + * @brief 代码生成器 */ class Builder { public: @@ -78,14 +56,16 @@ namespace { * @param proj 工程定义(cycle_limit / dt_ms / 哈希用) * @param units 全部源文件的 AST * @param link 链接结果(POU 顺序 / 符号) + * @param cfg 机器定义(machine.toml 强校验后;指令 opcode / 类型 tag / FB 布局) * @param image 输出映像字节 * @param err 错误输出;可为 nullptr(静默) */ Builder(const Project& proj, const std::vector& units, - const LinkResult& link, std::vector* image, std::string* err) - : proj_(proj), units_(units), link_(link), image_(image), err_(err) { + const LinkResult& link, const MachineConfig& cfg, + std::vector* image, std::string* err) + : proj_(proj), units_(units), link_(link), cfg_(cfg), image_(image), err_(err) { // io 绑定分类(名已折小写;不创造变量,只影响操作码选择) - for (const isa::IoBinding& b : proj_.io) { + for (const IoBinding& b : proj_.io) { std::string key = b.var; for (char& ch : key) { ch = static_cast(std::tolower(static_cast(ch))); @@ -100,7 +80,6 @@ namespace { /** * @brief 布局数据区 → 布局 FB 实例 → 逐 POU 建函数 → 拼映像 - * @details 偏移先定(函数编译引用 global_offset / instance_of) * @return true 成功;false(err 已写,前缀 "codegen error") */ bool run() { @@ -127,11 +106,6 @@ namespace { } private: - /** - * @brief 组装错误消息并返回 false - * @param msg 错误描述(不含前缀) - * @return 恒 false - */ bool fail(const std::string& msg) { if (err_) { *err_ = "codegen error: " + msg; @@ -140,10 +114,42 @@ namespace { } /** - * @brief 按名查 POU AST - * @param name POU 名(小写) - * @return POU 指针;未找到返回 nullptr + * @brief 按名查配置 opcode(MachineConfig 强校验保证存在) + * @param name 指令名(大写,如 "ADD") + * @return opcode(找不到返回 0,不应发生) */ + uint8_t opc(const char* name) const { + const ConfigOp* op = cfg_.find_op(name); + return op ? static_cast(op->opcode) : 0; + } + + // ---- 指令发射(配置 opcode)---- + Instr E_rr(const char* name, uint8_t rd, uint8_t rs) { + return pack(opc(name), rd, rs, 0); + } + Instr E_rrr(const char* name, uint8_t rd, uint8_t ra, uint8_t rb) { + return pack(opc(name), rd, ra, rb); + } + Instr E_imm(const char* name, uint8_t rd, uint16_t imm) { + return pack(opc(name), rd, static_cast(imm & 0xFFu), + static_cast((imm >> 8) & 0xFFu)); + } + Instr E_slot(const char* name, uint8_t rd, uint16_t slot) { + return E_imm(name, rd, slot); + } + Instr E_jmp(int16_t off) { + const uint16_t u = static_cast(off); + return pack(opc("JMP"), 0, static_cast(u & 0xFFu), + static_cast((u >> 8) & 0xFFu)); + } + Instr E_jc(const char* name, uint8_t r, int16_t off) { + const uint16_t u = static_cast(off); + return pack(opc(name), r, static_cast(u & 0xFFu), + static_cast((u >> 8) & 0xFFu)); + } + Instr E_call(uint16_t fn_id) { return E_imm("CALL", 0, fn_id); } + Instr E_ret() { return pack(opc("RET"), 0, 0, 0); } + const POU* find_pou(const std::string& name) const { for (const SourceUnit& u : units_) { for (const POU& p : u.ast.pous) { @@ -159,33 +165,24 @@ namespace { struct FuncCtx { std::string name; std::string pou_name; // 所属 POU 名(实例查找键) - std::vector code; // 字节码(函数表 code_offset 相对此段) + std::vector code; // 字节码 std::map regs; // 变量名 → 帧寄存器 - uint8_t nlocals = 0; // 变量区终点 = 临时寄存器起始 - uint8_t nregs = 0; // 峰值(变量 + 临时) - uint8_t temp_used = 0; // 本语句已用临时数(语句结束清零) - bool is_function = false; // FUNCTION(结果 r0 / 输入只读) - std::string result_name; // FUNCTION 名(结果寄存器映射) + uint8_t nlocals = 0; // 变量区终点 = 临时寄存器起始 + uint8_t nregs = 0; // 峰值(变量 + 临时) + uint8_t temp_used = 0; // 本语句已用临时数(语句结束清零) + bool is_function = false; // FUNCTION(结果 r0 / 输入只读) + std::string result_name; // FUNCTION 名(结果寄存器映射) }; - // FB 实例:字段名 → 数据区字节地址 + // FB 实例:字段名 → 数据区槽号 struct InstFields { std::map field_addr; uint32_t base = 0; std::string type_name; // 实例的 FB 类型名(内建 / 用户) }; - /** - * @brief 语句开始:临时寄存器基址复用 - * @param f 当前函数 - */ void begin_stmt(FuncCtx& f) { f.temp_used = 0; } - /** - * @brief 分配一个临时寄存器(语句内递增) - * @param f 当前函数 - * @return 临时寄存器号(可能更新 nregs 峰值) - */ uint8_t alloc_temp(FuncCtx& f) { const uint8_t r = f.nlocals + f.temp_used; ++f.temp_used; @@ -201,16 +198,12 @@ namespace { * FB 不生成业务字节码(调用点内联),只出空函数占位保持 fn_id 一致; * 帧约定(见 Doc/compiler/寄存器码.md):结果 r0、输入 r1..r7(只读)、 * 变量与临时全部从 r8 起 - * @param pou POU AST - * @param f 输出函数编译态 - * @return true 成功;false(err 已写) */ bool build_function(const POU& pou, FuncCtx* f) { if (pou.kind == PouKind::FunctionBlock) { - // FB 内联展开(见 compile_fb_call),此处只占 fn_id f->nlocals = 8; f->nregs = 8; - f->code.push_back(isa::enc_ret()); + f->code.push_back(E_ret()); return true; } f->is_function = pou.kind == PouKind::Function; @@ -229,16 +222,13 @@ namespace { } uint8_t r = 8; // 变量/临时基址(调用约定区 r0..r7 不占用) for (const VarBlock& b : pou.blocks) { - // External / Global 走数据区槽,不占帧寄存器 if (b.section == VarSection::External || b.section == VarSection::Global) { continue; } - // FUNCTION 输入已分配(r1..r7) if (f->is_function && b.section == VarSection::Input) { continue; } for (const VarDecl& d : b.vars) { - // FB 实例走数据区(layout_fb_instances),不占帧寄存器 if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { continue; } @@ -253,16 +243,10 @@ namespace { return false; } } - f->code.push_back(isa::enc_ret()); + f->code.push_back(E_ret()); return true; } - /** - * @brief 编译一条语句(切片 4:赋值 / IF) - * @param f 当前函数 - * @param st 语句 AST - * @return true 成功;false(err 已写) - */ bool compile_stmt(FuncCtx& f, const Stmt& st) { if (st.kind == StmtKind::If) { return compile_if(f, st); @@ -274,7 +258,7 @@ namespace { return compile_fb_call(f, st); } if (st.kind != StmtKind::Assign) { - return fail("statement not supported in slice 7"); + return fail("statement not supported"); } // 内联 FB 体内:左值可能是实例字段(STORE_GLOBAL) if (inline_fields_) { @@ -284,32 +268,20 @@ namespace { if (!compile_expr(f, *st.value, t)) { return false; } - f.code.push_back(isa::enc_slot(isa::Op::STORE_GLOBAL, t, fit->second)); + f.code.push_back(E_slot("STORE_GLOBAL", t, fit->second)); return true; } } const auto it = f.regs.find(st.target); if (it == f.regs.end()) { - // 全局 / 外部左值 → STORE_* 到数据区 return store_target(f, st.target, *st.value); } - // 函数输入只读(调用约定,见 Doc/compiler/寄存器码.md) if (f.is_function && it->second >= 1 && it->second <= 7) { return fail("cannot write function input '" + st.target + "'"); } - const uint8_t rd = it->second; - return compile_expr(f, *st.value, rd); + return compile_expr(f, *st.value, it->second); } - /** - * @brief 编译 FB 调用 - * @details 实参求值到临时 → STORE_GLOBAL 到实例字段; - * 内建 TON/TOF/CTU → CAL_* <实例偏移>; - * 用户 FB → compile_fb_inline 内联展开 - * @param f 当前函数 - * @param st FbCall 语句 AST - * @return true 成功;false(err 已写) - */ bool compile_fb_call(FuncCtx& f, const Stmt& st) { const InstFields* inst = instance_of(f.pou_name, st.instance); if (inst == nullptr) { @@ -324,22 +296,15 @@ namespace { if (!compile_expr(f, *a.value, t)) { return false; } - f.code.push_back(isa::enc_slot(isa::Op::STORE_GLOBAL, t, it->second)); + f.code.push_back(E_slot("STORE_GLOBAL", t, it->second)); } - if (inst->type_name == "ton" || inst->type_name == "tof" || - inst->type_name == "tp" || inst->type_name == "ctu" || - inst->type_name == "ctd" || inst->type_name == "ctud" || - inst->type_name == "r_trig" || inst->type_name == "f_trig") { - const isa::Op op = - inst->type_name == "ton" ? isa::Op::CAL_TON - : inst->type_name == "tof" ? isa::Op::CAL_TOF - : inst->type_name == "tp" ? isa::Op::CAL_TP - : inst->type_name == "ctu" ? isa::Op::CAL_CTU - : inst->type_name == "ctd" ? isa::Op::CAL_CTD - : inst->type_name == "ctud" ? isa::Op::CAL_CTUD - : inst->type_name == "r_trig" ? isa::Op::CAL_R_TRIG - : isa::Op::CAL_F_TRIG; - f.code.push_back(isa::enc_slot(op, 0, inst->base)); + // 内建 FB:配置里必须有对应 op 行(强校验保证)→ 直接查配置 opcode + const std::string& tn = inst->type_name; + if (cfg_.find_op("CAL_" + uppercase_of(tn)) != nullptr && + (tn == "ton" || tn == "tof" || tn == "tp" || tn == "ctu" || + tn == "ctd" || tn == "ctud" || tn == "r_trig" || tn == "f_trig")) { + const std::string opname = "CAL_" + uppercase_of(tn); + f.code.push_back(E_slot(opname.c_str(), 0, inst->base)); return true; } const POU* fb = find_pou(inst->type_name); @@ -349,15 +314,14 @@ namespace { return compile_fb_inline(f, *fb, *inst); } - /** - * @brief 内联编译用户 FB 体 - * @details 切到实例字段上下文(VarRef/赋值左值映射到字段地址); - * 表达式临时与调用方共用 r8+;字段读写全走 LOAD/STORE_GLOBAL - * @param f 当前函数 - * @param fb FB 类型 POU - * @param inst 本实例字段表 - * @return true 成功;false(err 已写) - */ + static std::string uppercase_of(const std::string& s) { + std::string out = s; + for (char& ch : out) { + ch = static_cast(std::toupper(static_cast(ch))); + } + return out; + } + bool compile_fb_inline(FuncCtx& f, const POU& fb, const InstFields& inst) { const InstFields* saved = inline_fields_; inline_fields_ = &inst; @@ -371,14 +335,6 @@ namespace { return true; } - /** - * @brief 编译 WHILE - * @details L_loop: JF t, L_end JMP L_loop L_end: - * 条件 JF 与回环 JMP 双回填 - * @param f 当前函数 - * @param st WHILE 语句 AST - * @return true 成功;false(err 已写) - */ bool compile_while(FuncCtx& f, const Stmt& st) { const size_t loop = f.code.size(); const uint8_t t = alloc_temp(f); @@ -386,7 +342,7 @@ namespace { return false; } const size_t jf_idx = f.code.size(); - f.code.push_back(isa::enc_jc(isa::Op::JF, t, 0)); // 假 → L_end + f.code.push_back(E_jc("JF", t, 0)); for (const Stmt& s : st.body) { begin_stmt(f); if (!compile_stmt(f, s)) { @@ -394,29 +350,20 @@ namespace { } } const size_t jmp_idx = f.code.size(); - f.code.push_back(isa::enc_jmp(0)); // 回环 + f.code.push_back(E_jmp(0)); patch_jump(f, jmp_idx, loop); patch_jump(f, jf_idx, f.code.size()); return true; } - /** - * @brief 编译 IF / ELSIF / ELSE - * @details 每分支: JF t, L_next → → JMP L_end; - * 最后一个分支无 else 时 JF 直接落 L_end(不补 JMP) - * @param f 当前函数 - * @param st IF 语句 AST - * @return true 成功;false(err 已写) - */ bool compile_if(FuncCtx& f, const Stmt& st) { - // 条件链:IF 体 + ELSIF 各体;ELSE 体可选 std::vector*>> branches; branches.push_back({st.cond.get(), &st.body}); for (const IfBranch& b : st.elsifs) { branches.push_back({b.cond.get(), &b.body}); } const bool has_else = !st.else_body.empty(); - std::vector end_jmps; // 各分支尾部 JMP(回填到 L_end) + std::vector end_jmps; for (size_t i = 0; i < branches.size(); ++i) { const uint8_t t = alloc_temp(f); @@ -424,7 +371,7 @@ namespace { return false; } const size_t jf_idx = f.code.size(); - f.code.push_back(isa::enc_jc(isa::Op::JF, t, 0)); // 假 → 下一分支 + f.code.push_back(E_jc("JF", t, 0)); for (const Stmt& s : *branches[i].second) { begin_stmt(f); if (!compile_stmt(f, s)) { @@ -433,10 +380,10 @@ namespace { } if (i + 1 < branches.size() || has_else) { const size_t jmp_idx = f.code.size(); - f.code.push_back(isa::enc_jmp(0)); // 分支尾部 → L_end + f.code.push_back(E_jmp(0)); end_jmps.push_back(jmp_idx); } - patch_jump(f, jf_idx, f.code.size()); // 回填 JF 到下一分支起点 + patch_jump(f, jf_idx, f.code.size()); } if (has_else) { for (const Stmt& s : st.else_body) { @@ -453,13 +400,6 @@ namespace { return true; } - /** - * @brief 赋值左值:帧寄存器直写或 STORE_* 到全局槽 - * @param f 当前函数 - * @param target 左值名(全局 / 外部) - * @param value 右值表达式 - * @return true 成功;false(err 已写) - */ bool store_target(FuncCtx& f, const std::string& target, const Expr& value) { const auto git = link_.global_index.find(target); if (git == link_.global_index.end()) { @@ -472,49 +412,36 @@ namespace { if (!compile_expr(f, value, tmp)) { return false; } - const uint16_t slot = global_offset(git->second); - f.code.push_back(isa::enc_slot(store_op(target), tmp, slot)); + f.code.push_back(E_slot(store_name(target), tmp, static_cast(git->second))); return true; } - /** - * @brief 表达式编译到目标寄存器 - * @details 字面量 → LOADK;帧变量 → MOVE;全局/外部 → LOAD_*(io.input 用 LOAD_I); - * NOT → 求值后 NOT rd, t;AND/OR → 短路跳转(JF/JT 跳过右侧) - * @param f 当前函数 - * @param e 表达式 AST - * @param rd 目标寄存器 - * @return true 成功;false(err 已写) - */ bool compile_expr(FuncCtx& f, const Expr& e, uint8_t rd) { if (e.kind == ExprKind::LitBool || e.kind == ExprKind::LitInt || e.kind == ExprKind::LitTime) { - const isa::types::TypeTag tag = - e.kind == ExprKind::LitBool ? isa::types::Bool - : e.kind == ExprKind::LitInt ? isa::types::Int - : isa::types::Time; - f.code.push_back( - isa::enc_imm(isa::Op::LOADK, rd, const_id(tag, e.int_value))); + const char* type_name = e.kind == ExprKind::LitBool ? "BOOL" + : e.kind == ExprKind::LitInt ? "INT" + : "TIME"; + f.code.push_back(E_imm("LOADK", rd, const_id(type_name, e.int_value))); return true; } if (e.kind == ExprKind::VarRef) { - // 内联 FB 体内:字段优先(实例字段为变量) if (inline_fields_) { const auto fit = inline_fields_->field_addr.find(e.name); if (fit != inline_fields_->field_addr.end()) { - f.code.push_back(isa::enc_slot(isa::Op::LOAD_GLOBAL, rd, fit->second)); + f.code.push_back(E_slot("LOAD_GLOBAL", rd, fit->second)); return true; } } const auto sit = f.regs.find(e.name); if (sit != f.regs.end()) { - f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, sit->second)); + f.code.push_back(E_rr("MOVE", rd, sit->second)); return true; } const auto git = link_.global_index.find(e.name); if (git != link_.global_index.end()) { - const uint16_t slot = global_offset(git->second); - f.code.push_back(isa::enc_slot(load_op(e.name), rd, slot)); + f.code.push_back( + E_slot(load_name(e.name), rd, static_cast(git->second))); return true; } return fail("no register or slot for '" + e.name + "'"); @@ -528,7 +455,7 @@ namespace { if (fit == inst->field_addr.end()) { return fail("unknown field '" + e.field + "' for '" + e.name + "'"); } - f.code.push_back(isa::enc_slot(isa::Op::LOAD_GLOBAL, rd, fit->second)); + f.code.push_back(E_slot("LOAD_GLOBAL", rd, fit->second)); return true; } if (e.kind == ExprKind::Add || e.kind == ExprKind::Sub || @@ -541,7 +468,7 @@ namespace { if (!compile_expr(f, *e.rhs, r)) { return false; } - f.code.push_back(isa::enc_rrr(arith_op(e.kind), rd, l, r)); + f.code.push_back(E_rrr(arith_name(e.kind), rd, l, r)); return true; } if (e.kind == ExprKind::Cmp) { @@ -553,7 +480,7 @@ namespace { if (!compile_expr(f, *e.rhs, r)) { return false; } - f.code.push_back(isa::enc_rrr(cmp_op(e.op), rd, l, r)); + f.code.push_back(E_rrr(cmp_name(e.op), rd, l, r)); return true; } if (e.kind == ExprKind::Not) { @@ -561,27 +488,25 @@ namespace { if (!compile_expr(f, *e.operand, t)) { return false; } - f.code.push_back(isa::enc_rr(isa::Op::NOT, rd, t)); + f.code.push_back(E_rr("NOT", rd, t)); return true; } if (e.kind == ExprKind::And || e.kind == ExprKind::Or) { - // 短路:左侧结果在 rd;AND 为假 / OR 为真时跳过右侧 if (!compile_expr(f, *e.lhs, rd)) { return false; } - const isa::Op jop = (e.kind == ExprKind::And) ? isa::Op::JF : isa::Op::JT; + const char* jname = (e.kind == ExprKind::And) ? "JF" : "JT"; const size_t jmp_idx = f.code.size(); - f.code.push_back(isa::enc_jc(jop, rd, 0)); // 偏移留洞,稍后回填 + f.code.push_back(E_jc(jname, rd, 0)); const uint8_t t = alloc_temp(f); if (!compile_expr(f, *e.rhs, t)) { return false; } - f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, t)); + f.code.push_back(E_rr("MOVE", rd, t)); patch_jump(f, jmp_idx, f.code.size()); return true; } if (e.kind == ExprKind::Call) { - // 调用约定:实参求值到 r8+ 临时 → MOVE 到 r1..r7 → CALL → 结果 r0 if (e.args.size() > 7) { return fail("too many arguments (max 7)"); } @@ -598,21 +523,15 @@ namespace { return fail("no fn_id for '" + e.name + "'"); } for (size_t i = 0; i < args.size(); ++i) { - f.code.push_back( - isa::enc_rr(isa::Op::MOVE, static_cast(1 + i), args[i])); + f.code.push_back(E_rr("MOVE", static_cast(1 + i), args[i])); } - f.code.push_back(isa::enc_call(static_cast(fn_id))); - f.code.push_back(isa::enc_rr(isa::Op::MOVE, rd, 0)); + f.code.push_back(E_call(static_cast(fn_id))); + f.code.push_back(E_rr("MOVE", rd, 0)); return true; } - return fail("expression not supported in slice 6"); + return fail("expression not supported"); } - /** - * @brief 按名查 fn_id(POU 收集序下标) - * @param name POU 名(小写) - * @return fn_id;未找到返回 -1 - */ int find_fn_id(const std::string& name) const { for (size_t i = 0; i < link_.scopes.size(); ++i) { if (link_.scopes[i].name == name) { @@ -622,87 +541,43 @@ namespace { return -1; } - /** - * @brief 回填跳转偏移(相对下一条指令) - * @param f 当前函数 - * @param idx 跳转指令下标(占位) - * @param target_idx 目标指令下标 - */ void patch_jump(FuncCtx& f, size_t idx, size_t target_idx) { const int16_t off = static_cast( static_cast(target_idx) - (static_cast(idx) + 1)); - const isa::Instr w = f.code[idx]; - f.code[idx] = isa::pack(isa::op(w), isa::rd(w), - static_cast(static_cast(off) & 0xFFu), - static_cast((static_cast(off) >> 8) & 0xFFu)); + const Instr w = f.code[idx]; + const uint16_t u = static_cast(off); + f.code[idx] = pack(op_of(w), rd_of(w), static_cast(u & 0xFFu), + static_cast((u >> 8) & 0xFFu)); } - /** - * @brief 算术运算 → 操作码 - * @param k 表达式种类(Add/Sub/Mul/Div) - * @return 对应操作码 - */ - isa::Op arith_op(ExprKind k) const { + // ---- 操作码名(MachineConfig 已强校验存在)---- + static const char* arith_name(ExprKind k) { switch (k) { - case ExprKind::Add: return isa::Op::ADD; - case ExprKind::Sub: return isa::Op::SUB; - case ExprKind::Mul: return isa::Op::MUL; - case ExprKind::Div: return isa::Op::DIV; - default: return isa::Op::ADD; + case ExprKind::Add: return "ADD"; + case ExprKind::Sub: return "SUB"; + case ExprKind::Mul: return "MUL"; + case ExprKind::Div: return "DIV"; + default: return "ADD"; } } - - /** - * @brief 比较符 → CMP_xx 操作码 - * @param op 比较运算 - * @return 对应操作码(CMP_EQ / CMP_NE / CMP_LT / CMP_LE / CMP_GT / CMP_GE) - */ - isa::Op cmp_op(BinOp op) const { + static const char* cmp_name(BinOp op) { switch (op) { - case BinOp::Eq: return isa::Op::CMP_EQ; - case BinOp::Ne: return isa::Op::CMP_NE; - case BinOp::Lt: return isa::Op::CMP_LT; - case BinOp::Le: return isa::Op::CMP_LE; - case BinOp::Gt: return isa::Op::CMP_GT; - case BinOp::Ge: return isa::Op::CMP_GE; - default: return isa::Op::CMP_EQ; // Add/Sub/Mul/Div 不经此函数 + case BinOp::Eq: return "CMP_EQ"; + case BinOp::Ne: return "CMP_NE"; + case BinOp::Lt: return "CMP_LT"; + case BinOp::Le: return "CMP_LE"; + case BinOp::Gt: return "CMP_GT"; + case BinOp::Ge: return "CMP_GE"; + default: return "CMP_EQ"; } } - - /** - * @brief 按 io 绑定选读取操作码 - * @param name 变量名(小写) - * @return io.input 绑定 → LOAD_I;否则 LOAD_GLOBAL - */ - isa::Op load_op(const std::string& name) const { - return io_input_.count(name) ? isa::Op::LOAD_I : isa::Op::LOAD_GLOBAL; + const char* load_name(const std::string& name) const { + return io_input_.count(name) ? "LOAD_I" : "LOAD_GLOBAL"; + } + const char* store_name(const std::string& name) const { + return io_output_.count(name) ? "STORE_Q" : "STORE_GLOBAL"; } - /** - * @brief 按 io 绑定选写入操作码 - * @param name 变量名(小写) - * @return io.output 绑定 → STORE_Q;否则 STORE_GLOBAL - */ - isa::Op store_op(const std::string& name) const { - return io_output_.count(name) ? isa::Op::STORE_Q : isa::Op::STORE_GLOBAL; - } - - /** - * @brief 逻辑槽号(符号表 address)即指令 slot(方案 a:8 字节定宽,无映射) - * @param slot 全局逻辑槽号 - * @return 同值(u16 可容,layout_data 已校验) - */ - uint16_t global_offset(uint32_t slot) const { - return static_cast(slot); - } - - /** - * @brief 布局全局数据区:每槽 8 字节定宽(方案 a) - * @details 槽号 = 符号表 address(声明序,12.6); - * BOOL 用低 1 字节、INT 用低 2 字节(小端)、TIME 全 8 字节; - * 初值取自 GVL 声明的 has_init(无则 0) - * @return true 成功;false(槽号越界,err 已写) - */ bool layout_data() { for (const Symbol& s : link_.globals) { if (s.address > 0xFFFF) { @@ -714,30 +589,33 @@ namespace { const int64_t v = has_init ? init : 0; const size_t off = static_cast(s.address) * 8; data_.resize(off + 8, 0); - if (s.type_name == "int") { + // 初值按基元宽度写(类型元数据来自配置别名 → base) + const ConfigType* ct = cfg_.find_type(uppercase_of(s.type_name)); + if (ct == nullptr) { + return fail("no type in machine.toml for '" + s.type_name + "'"); + } + const PrimType* prim = MachineConfig::find_prim(ct->base); + if (prim == nullptr) { + return fail("no prim '" + ct->base + "' for type '" + ct->name + "'"); + } + if (prim->width == 2 && !prim->is_float) { const uint16_t iv = static_cast(v); data_[off] = static_cast(iv & 0xFFu); data_[off + 1] = static_cast((iv >> 8) & 0xFFu); - } else if (s.type_name == "time") { + } else if (prim->width >= 8 && !prim->is_float) { const uint64_t tv = static_cast(v); for (int i = 0; i < 8; ++i) { data_[off + i] = static_cast((tv >> (8 * i)) & 0xFFu); } - } else { // bool + } else { data_[off] = v ? 1 : 0; } } return true; } - /** - * @brief 布局 FB 实例块:数据区全局之后,按 POU 收集序 / 实例声明序 - * @details 每实例:字段按布局段序各占一槽(8 字节定宽), - * 字段地址 = 实例基槽 + 字段序号;实例基槽 = 累计槽数 - * @return true 成功;false(err 已写) - */ bool layout_fb_instances() { - uint32_t cur = static_cast(data_.size() / 8); // 槽号起点 + uint32_t cur = static_cast(data_.size() / 8); bool any = false; for (const LinkResult::PouScope& sc : link_.scopes) { for (const Symbol& s : sc.syms) { @@ -749,7 +627,7 @@ namespace { return fail("no layout for instance '" + s.name + "'"); } InstFields inst; - inst.base = cur; // 实例基槽(CAL_* 操作数) + inst.base = cur; inst.type_name = s.type_name; for (size_t i = 0; i < lay->second.fields.size(); ++i) { inst.field_addr[lay->second.fields[i].name] = cur + i; @@ -759,31 +637,18 @@ namespace { any = true; } } - // 仅在有实例时把数据区补到实例区终点(无实例则保持全局区原样) if (any) { data_.resize(static_cast(cur) * 8, 0); } return true; } - /** - * @brief 按 POU + 实例名查实例字段表 - * @param pou POU 名 - * @param name 实例名 - * @return 实例字段表指针;未找到返回 nullptr - */ const InstFields* instance_of(const std::string& pou, const std::string& name) const { const auto it = instances_.find(pou + "/" + name); return it == instances_.end() ? nullptr : &it->second; } - /** - * @brief 查全局声明的初值(AST;gvl 文件顶层段,声明序 = 槽号序) - * @param name 全局名(小写) - * @param has_init 输出是否有初值 - * @param init 输出初值 - */ void init_of(const std::string& name, bool* has_init, int64_t* init) const { for (const SourceUnit& u : units_) { for (const VarBlock& b : u.ast.globals) { @@ -802,11 +667,13 @@ namespace { /** * @brief 取常量表 id(无则追加) - * @param tag 类型标记(BOOL/INT/TIME) + * @param type_name 语言类型名("BOOL"/"INT"/"TIME")→ 配置 tag(契约) * @param value 常量值 * @return const_id(u16) */ - uint16_t const_id(isa::types::TypeTag tag, int64_t value) { + uint16_t const_id(const char* type_name, int64_t value) { + const ConfigType* ct = cfg_.find_type(type_name); + const uint32_t tag = ct ? ct->tag : 0; for (size_t i = 0; i < consts_.size(); ++i) { if (consts_[i].tag == tag && consts_[i].value == static_cast(value)) { return static_cast(i); @@ -816,18 +683,12 @@ namespace { return static_cast(consts_.size() - 1); } - /** - * @brief 拼映像:头 + 常量表 + 函数表 + 字节码 + 数据段 - * @details 段序:const → funcs → code → fb(空)→ data; - * 数据段放全局初值(layout_data 已生成字节) - * @return true 成功;false(err 已写) - */ bool assemble_image() { - const uint32_t off_const = isa::kHeaderSize; + const uint32_t off_const = static_cast(kHeaderSize); const uint32_t off_funcs = off_const + - static_cast(consts_.size()) * isa::kConstEntrySize; + static_cast(consts_.size()) * static_cast(kConstEntrySize); uint32_t off_code = off_funcs + - static_cast(funcs_.size()) * isa::kFuncRowSize; + static_cast(funcs_.size()) * static_cast(kFuncRowSize); uint32_t code_total = 0; for (const FuncCtx& f : funcs_) { code_total += static_cast(f.code.size()) * 4; @@ -837,12 +698,12 @@ namespace { std::vector& b = *image_; b.assign(off_end, 0); - put_le32(b, 0, isa::kMagic); - put_le32(b, 4, isa::kVersion); + put_le32(b, 0, kMagic); + put_le32(b, 4, kVersion); put_le32(b, 8, proj_.cycle_limit); put_le32(b, 12, proj_.dt_ms); - uint64_t hash = isa::kFnvBasis; + uint64_t hash = kFnvBasis; if (!compute_project_hash(proj_, &hash, err_)) { return false; } @@ -855,80 +716,82 @@ namespace { } } put_le32(b, 24, entry); - put_le32(b, 28, static_cast(link_.globals.size())); // n_globals - put_le32(b, 32, 0); // n_i - put_le32(b, 36, 0); // n_q - put_le32(b, 40, 0); // n_m + put_le32(b, 28, static_cast(link_.globals.size())); + put_le32(b, 32, 0); + put_le32(b, 36, 0); + put_le32(b, 40, 0); put_le32(b, 44, static_cast(consts_.size())); put_le32(b, 48, static_cast(funcs_.size())); put_le32(b, 52, off_const); put_le32(b, 56, off_funcs); put_le32(b, 60, off_code); - put_le32(b, 64, off_data); // offset_fb(空,与数据段起点相同) - put_le32(b, 68, off_data); // offset_data + put_le32(b, 64, off_data); + put_le32(b, 68, off_data); for (size_t i = 0; i < consts_.size(); ++i) { - const size_t o = off_const + i * isa::kConstEntrySize; - put_le32(b, o, static_cast(consts_[i].tag)); + const size_t o = off_const + i * kConstEntrySize; + put_le32(b, o, consts_[i].tag); put_le64(b, o + 4, consts_[i].value); } uint32_t c = off_code; for (size_t i = 0; i < funcs_.size(); ++i) { const FuncCtx& f = funcs_[i]; - const size_t o = off_funcs + i * isa::kFuncRowSize; + const size_t o = off_funcs + i * kFuncRowSize; put_le32(b, o, f.nregs); put_le32(b, o + 8, static_cast(f.code.size())); - for (const isa::Instr in : f.code) { + for (const Instr in : f.code) { put_le32(b, c, in); c += 4; } } - // code_offset 回填(相对字节码段起点) uint32_t acc = 0; for (size_t i = 0; i < funcs_.size(); ++i) { - const size_t o = off_funcs + i * isa::kFuncRowSize; + const size_t o = off_funcs + i * kFuncRowSize; put_le32(b, o + 4, acc); acc += static_cast(funcs_[i].code.size()) * 4; } - // 数据段:全局初值字节 for (size_t i = 0; i < data_.size(); ++i) { b[off_data + i] = data_[i]; } return true; } + static void put_le32(std::vector& b, size_t off, uint32_t v) { + b[off + 0] = static_cast(v & 0xFFu); + b[off + 1] = static_cast((v >> 8) & 0xFFu); + b[off + 2] = static_cast((v >> 16) & 0xFFu); + b[off + 3] = static_cast((v >> 24) & 0xFFu); + } + static void put_le64(std::vector& b, size_t off, uint64_t v) { + for (int i = 0; i < 8; ++i) { + b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + } + // ---- 成员 ---- const Project& proj_; const std::vector& units_; const LinkResult& link_; + const MachineConfig& cfg_; std::vector* image_; std::string* err_; std::vector funcs_; - std::vector consts_; - std::map io_input_; // io.input 绑定名(小写) - std::map io_output_; // io.output 绑定名(小写) - std::vector data_; // 数据区(8 字节定宽槽:全局 + FB 实例块) - std::map instances_; // "POU/实例名" → 字段槽号 - const InstFields* inline_fields_ = nullptr; // 内联 FB 体字段上下文(可空) + std::vector consts_; + std::map io_input_; + std::map io_output_; + std::vector data_; + std::map instances_; + const InstFields* inline_fields_ = nullptr; }; } // namespace -/** - * @brief 编译工程为 .stb 映像字节(对外入口) - * @param proj 工程定义 - * @param units 全部源文件的 AST - * @param link 链接结果(在类型检查成功后调用) - * @param image 输出映像字节 - * @param err 错误输出;可为 nullptr(静默) - * @return true 成功;false 失败(err 前缀 "codegen error") - */ bool codegen_project(const Project& proj, const std::vector& units, - const LinkResult& link, std::vector* image, - std::string* err) { - Builder b(proj, units, link, image, err); + const LinkResult& link, const MachineConfig& cfg, + std::vector* image, std::string* err) { + Builder b(proj, units, link, cfg, image, err); return b.run(); } diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp index da25153..4bc9969 100644 --- a/compiler/src/Linker.cpp +++ b/compiler/src/Linker.cpp @@ -662,7 +662,7 @@ namespace { * @return true 全部绑定合法;false(err 已写) */ bool check_io() { - for (const isa::IoBinding& b : proj_.io) { + for (const IoBinding& b : proj_.io) { std::string key = b.var; for (char& ch : key) { ch = static_cast(std::tolower(static_cast(ch))); diff --git a/compiler/src/Project.cpp b/compiler/src/Project.cpp index a7f748a..af2b20c 100644 --- a/compiler/src/Project.cpp +++ b/compiler/src/Project.cpp @@ -16,7 +16,7 @@ #include #include -#include "isa/Image.h" +#include "compiler/Stb.h" namespace compiler { namespace { @@ -241,7 +241,7 @@ namespace { if (!check_keys(sec, is_input ? "io.input" : "io.output", allowed, 3, err)) { return false; } - isa::IoBinding b; + IoBinding b; b.is_input = is_input; b.slot = 0; // 12.6 链接阶段再解析 if (!req_string(sec, is_input ? "io.input" : "io.output", "var", &b.var, err)) { @@ -353,7 +353,7 @@ bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err) { std::sort(sorted.begin(), sorted.end()); std::filesystem::path base(p.base_dir); - uint64_t h = isa::kFnvBasis; + uint64_t h = kFnvBasis; for (const std::string& f : sorted) { std::ifstream in(base / f, std::ios::binary); if (!in) { @@ -365,7 +365,7 @@ bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err) { in.read(buf, sizeof buf); const std::streamsize n = in.gcount(); if (n > 0) { - h = isa::fnv1a64_update(h, reinterpret_cast(buf), + h = fnv1a64_update(h, reinterpret_cast(buf), static_cast(n)); } } diff --git a/compiler/src/Stb.cpp b/compiler/src/Stb.cpp new file mode 100644 index 0000000..2b63f84 --- /dev/null +++ b/compiler/src/Stb.cpp @@ -0,0 +1,225 @@ +/** + * @file Stb.cpp + * @brief 编译器自带的 .stb 映像规范(写侧)+ 只读视图 + FNV-1a + sidecar + * @author + * @date 2026-08-21 + */ + +#include "compiler/Stb.h" + +#include +#include +#include + +#include "compiler/Project.h" + +namespace compiler { + +uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + h ^= data[i]; + h *= kFnvPrime; + } + return h; +} + +uint64_t fnv1a64(const uint8_t* data, size_t len) { + return fnv1a64_update(kFnvBasis, data, len); +} + +namespace { + + uint32_t get_le32(const uint8_t* p) { + return static_cast(p[0]) + | (static_cast(p[1]) << 8) + | (static_cast(p[2]) << 16) + | (static_cast(p[3]) << 24); + } + + uint64_t get_le64(const uint8_t* p) { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) { + v |= static_cast(p[i]) << (8 * i); + } + return v; + } + +} // namespace + +StbView StbView::from(const uint8_t* buf, size_t len) { + StbView v; + v.buf_ = buf; + v.len_ = len; + if (buf == nullptr) { + v.err_ = "null buffer"; + return v; + } + if (len < kHeaderSize) { + v.err_ = "image too short"; + return v; + } + if (get_le32(buf + 0) != kMagic) { + v.err_ = "bad magic"; + return v; + } + if (get_le32(buf + 4) != kVersion) { + v.err_ = "bad version"; + return v; + } + v.cycle_limit_ = get_le32(buf + 8); + v.dt_ms_ = get_le32(buf + 12); + v.project_hash_ = get_le64(buf + 16); + v.entry_fn_id_ = get_le32(buf + 24); + v.n_globals_ = get_le32(buf + 28); + v.n_consts_ = get_le32(buf + 44); + v.n_funcs_ = get_le32(buf + 48); + v.offset_code_ = get_le32(buf + 60); + v.offset_fb_ = get_le32(buf + 64); + v.offset_data_ = get_le32(buf + 68); + + // 段校验 + const uint64_t offs[5] = {get_le32(buf + 52), get_le32(buf + 56), v.offset_code_, + v.offset_fb_, v.offset_data_}; + for (int i = 0; i < 5; ++i) { + if (offs[i] < kHeaderSize || offs[i] > len) { + v.err_ = "segment offset out of range"; + return v; + } + if (i > 0 && offs[i] < offs[i - 1]) { + v.err_ = "segment offsets not monotonic"; + return v; + } + } + if (offs[1] - offs[0] != static_cast(v.n_consts_) * kConstEntrySize) { + v.err_ = "const table size mismatch"; + return v; + } + if (offs[2] - offs[1] != static_cast(v.n_funcs_) * kFuncRowSize) { + v.err_ = "function table size mismatch"; + return v; + } + if ((v.offset_fb_ - v.offset_code_) % 4 != 0) { + v.err_ = "code segment not 4-byte aligned"; + return v; + } + if (v.entry_fn_id_ >= v.n_funcs_ && v.n_funcs_ != 0) { + v.err_ = "entry fn_id out of range"; + return v; + } + v.ok_ = true; + v.err_.clear(); + return v; +} + +StbView StbView::from(const std::vector& buf) { + return from(buf.data(), buf.size()); +} + +ConstEntry StbView::const_entry(size_t i) const { + ConstEntry e; + if (ok_ && i < n_consts_) { + const uint8_t* p = buf_ + offs_of_const() + i * kConstEntrySize; + e.tag = get_le32(p); + e.value = get_le64(p + 4); + } + return e; +} + +uint32_t StbView::offs_of_const() const { + // offset_const = offs[0],由 from() 已校验的段起点 + return static_cast(get_le32(buf_ + 52)); +} + +StbView::FuncRow StbView::func_row(size_t i) const { + FuncRow r; + if (ok_ && i < n_funcs_) { + const uint8_t* p = buf_ + get_le32(buf_ + 56) + i * kFuncRowSize; + r.nregs = get_le32(p + 0); + r.code_offset = get_le32(p + 4); + r.code_len = get_le32(p + 8); + } + return r; +} + +const uint8_t* StbView::code_bytes() const { + return ok_ ? buf_ + offset_code_ : nullptr; +} + +size_t StbView::code_len() const { + return ok_ ? offset_fb_ - offset_code_ : 0; +} + +const uint8_t* StbView::data_bytes() const { + return ok_ ? buf_ + offset_data_ : nullptr; +} + +size_t StbView::data_len() const { + return ok_ ? len_ - offset_data_ : 0; +} + +bool read_stb_file(const char* path, std::vector* out, std::string* err) { + std::ifstream in(path, std::ios::binary); + if (!in) { + if (err) { + *err = "cannot open for read: " + std::string(path); + } + return false; + } + out->assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + return true; +} + +bool write_stb_file(const char* path, const std::vector& img, std::string* err) { + std::FILE* f = std::fopen(path, "wb"); + if (f == nullptr) { + if (err) { + *err = "cannot open for write: " + std::string(path); + } + return false; + } + const bool ok = img.empty() || std::fwrite(&img[0], 1, img.size(), f) == img.size(); + std::fclose(f); + if (!ok && err) { + *err = "write failed: " + std::string(path); + } + return ok; +} + +std::string make_sidecar(const std::vector& bindings) { + std::string out; + for (const IoBinding& b : bindings) { + char buf[64]; + out += b.is_input ? "[[io.input]]\n" : "[[io.output]]\n"; + out += "var = \""; + out += b.var; + out += "\"\n"; + std::snprintf(buf, sizeof buf, "slot = %u\n", static_cast(b.slot)); + out += buf; + std::snprintf(buf, sizeof buf, "channel = %u\n", static_cast(b.channel)); + out += buf; + std::snprintf(buf, sizeof buf, "bit = %u\n", static_cast(b.bit)); + out += buf; + out += "\n"; + } + return out; +} + +bool write_sidecar_file(const char* path, const std::vector& bindings, + std::string* err) { + std::FILE* f = std::fopen(path, "wb"); + if (f == nullptr) { + if (err) { + *err = "cannot open for write: " + std::string(path); + } + return false; + } + const std::string s = make_sidecar(bindings); + const bool ok = s.empty() || std::fwrite(s.data(), 1, s.size(), f) == s.size(); + std::fclose(f); + if (!ok && err) { + *err = "write failed: " + std::string(path); + } + return ok; +} + +} // namespace compiler diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index 90f36ce..f10e788 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -12,67 +12,66 @@ #include #include +#include "compiler/Codec.h" #include "compiler/Codegen.h" #include "compiler/Linker.h" +#include "compiler/MachineConfig.h" #include "compiler/Project.h" +#include "compiler/Stb.h" #include "compiler/Typecheck.h" -#include "isa/Encode.h" -#include "isa/Image.h" namespace { void usage() { - std::printf("usage: STCompiler [-o .stb]\n" - " STCompiler .stb --disasm\n" + std::printf("usage: STCompiler [-o .stb] --machine \n" + " STCompiler .stb --disasm --machine \n" " 解析工程并编译(词法 → 语法 → 链接 → 类型 → 寄存器码)\n" " -o .stb 编译并写出映像文件\n" - " 无 -o 只打印文件集合与工程哈希(12.3 阶段)\n" - " --disasm 反汇编一个已编译的 .stb 映像\n" + " --machine 机器定义(machine.toml,编译路径必填)\n" + " --disasm 反汇编一个已编译的 .stb 映像(需要 --machine)\n" " --help 打印本帮助\n"); } // 反汇编一个已编译映像(函数表逐条 + 常量表 + 数据段摘要)。 - // 映像无符号表,按 fn_id / 槽号显示;指令偏移为文件绝对字节偏移。 - int dump_image(const char* path) { + // 指令偏移为文件绝对字节偏移;反汇编按 machine.toml 的 format 输出。 + int dump_image(const char* path, const compiler::MachineConfig& cfg) { std::vector bytes; std::string err; - if (!isa::read_stb_file(path, &bytes, &err)) { + if (!compiler::read_stb_file(path, &bytes, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } - const isa::ImageView v = isa::ImageView::from(bytes); + const compiler::StbView v = compiler::StbView::from(bytes); if (!v.ok()) { std::fprintf(stderr, "error: %s\n", v.error().c_str()); return 1; } - const isa::ImageHeader& h = v.header(); std::printf("image: %s (%zu bytes, %u functions, %u globals, entry fn %u)\n", - path, bytes.size(), h.n_funcs, h.n_globals, h.entry_fn_id); - std::printf(" dt_ms=%u cycle_limit=%u hash=0x%016llx\n", h.dt_ms, - h.cycle_limit, static_cast(h.project_hash)); + path, bytes.size(), v.n_funcs(), v.n_globals(), v.entry_fn_id()); + std::printf(" dt_ms=%u cycle_limit=%u hash=0x%016llx\n", v.dt_ms(), + v.cycle_limit(), static_cast(v.project_hash())); - if (h.n_consts) { + if (v.n_consts()) { std::printf("constants:\n"); - for (uint32_t i = 0; i < h.n_consts; ++i) { - const isa::ConstEntry c = v.const_entry(i); - const char* tag = c.tag == isa::types::Bool ? "BOOL" - : c.tag == isa::types::Int ? "INT" - : "TIME"; + for (uint32_t i = 0; i < v.n_consts(); ++i) { + const compiler::ConstEntry c = v.const_entry(i); + const char* tag = c.tag == 0 ? "BOOL" : c.tag == 1 ? "INT" : "TIME"; std::printf(" [%u] %s %llu\n", i, tag, static_cast(c.value)); } } std::printf("functions:\n"); - for (uint32_t i = 0; i < h.n_funcs; ++i) { - const isa::FuncRow r = v.func_row(i); + for (uint32_t i = 0; i < v.n_funcs(); ++i) { + const compiler::StbView::FuncRow r = v.func_row(i); std::printf(" fn %u: nregs=%u, offset=%u, len=%u\n", i, r.nregs, r.code_offset, r.code_len); const uint8_t* base = v.code_bytes() + r.code_offset; for (uint32_t j = 0; j < r.code_len; ++j) { char buf[64]; - isa::disasm(reinterpret_cast(base)[j], buf, sizeof buf); + compiler::disasm(cfg, reinterpret_cast(base)[j], buf, + sizeof buf); std::printf(" 0x%04x %s\n", - h.offset_code + r.code_offset + j * 4, buf); + v.offset_code() + r.code_offset + j * 4, buf); } } @@ -102,13 +101,37 @@ int main(int argc, char** argv) { return 0; } - // --disasm:第一参数是 .stb 路径,不重新编译 + // 参数收集 + std::string machine_path; + bool disasm_mode = false; + for (int i = 2; i + 1 < argc; ++i) { + if (std::strcmp(argv[i], "--machine") == 0) { + machine_path = argv[i + 1]; + } + } for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--disasm") == 0) { - return dump_image(argv[1]); + disasm_mode = true; } } + // 机器定义(编译与反汇编都需要;加载/校验失败报错退出) + if (machine_path.empty()) { + std::fprintf(stderr, "error: missing --machine \n"); + usage(); + return 1; + } + compiler::MachineConfig cfg; + std::string err; + if (!cfg.load(machine_path, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + + if (disasm_mode) { + return dump_image(argv[1], cfg); + } + const std::string toml_path = argv[1]; std::string out_path; for (int i = 2; i + 1 < argc; ++i) { @@ -118,7 +141,6 @@ int main(int argc, char** argv) { } compiler::Project proj; - std::string err; if (!compiler::parse_project(toml_path, &proj, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; @@ -160,19 +182,19 @@ int main(int argc, char** argv) { return 1; } std::vector image; - if (!compiler::codegen_project(proj, units, link, &image, &err)) { + if (!compiler::codegen_project(proj, units, link, cfg, &image, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } - if (!isa::write_stb_file(out_path.c_str(), image, &err)) { + if (!compiler::write_stb_file(out_path.c_str(), image, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } // sidecar:I/O 绑定 var → 槽号 → channel/bit(不进映像,执行器采样用) - std::vector bindings; - for (const isa::IoBinding& b : proj.io) { - isa::IoBinding out_b = b; + std::vector bindings; + for (const compiler::IoBinding& b : proj.io) { + compiler::IoBinding out_b = b; std::string key = b.var; for (char& ch : key) { ch = static_cast(std::tolower(static_cast(ch))); @@ -185,13 +207,13 @@ int main(int argc, char** argv) { } std::filesystem::path sidecar = std::filesystem::path(out_path); sidecar.replace_extension(".runtime.toml"); - if (!isa::write_sidecar_file(sidecar.string().c_str(), bindings, &err)) { + if (!compiler::write_sidecar_file(sidecar.string().c_str(), bindings, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } - const isa::ImageView v = isa::ImageView::from(image); + const compiler::StbView v = compiler::StbView::from(image); std::printf("compiled: %s (%zu bytes, %u functions, %u globals)\n", - out_path.c_str(), image.size(), v.header().n_funcs, v.header().n_globals); + out_path.c_str(), image.size(), v.n_funcs(), v.n_globals()); return 0; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c8032fa..966d654 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,7 +72,7 @@ add_test(NAME typecheck_types add_executable(codegen_test ./src/codegen_test.cpp) -target_link_libraries(codegen_test PRIVATE compiler) +target_link_libraries(codegen_test PRIVATE compiler isa) target_compile_definitions(codegen_test PRIVATE REPO_ROOT="${CMAKE_SOURCE_DIR}") diff --git a/tests/src/cases_test.cpp b/tests/src/cases_test.cpp index 0374248..2500ba6 100644 --- a/tests/src/cases_test.cpp +++ b/tests/src/cases_test.cpp @@ -119,8 +119,13 @@ namespace { std::printf("FAIL %s type: %s\n", dir, err.c_str()); return false; } + MachineConfig cfg; + if (!cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &err)) { + std::printf("FAIL %s machine load\n", dir); + return false; + } std::vector img; - if (!codegen_project(p, units, link, &img, &err)) { + if (!codegen_project(p, units, link, cfg, &img, &err)) { if (want == Outcome::CompileError) { ++g_checks; return true; diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 5090cab..8c6cda1 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -34,8 +34,19 @@ static int g_checks = 0; } while (0) // 从用例目录走完整编译管线(解析 → 链接 → 类型检查 → 代码生成) +static bool load_cfg(compiler::MachineConfig* cfg) { + std::string err; + if (!cfg->load(std::string(REPO_ROOT) + "/compiler/machine.toml", &err)) { + std::printf("FAIL load machine.toml: %s\n", err.c_str()); + return false; + } + return true; +} + static bool compile_case(const char* dir, std::vector* image, std::string* err) { using namespace compiler; + compiler::MachineConfig cfg; + if (!load_cfg(&cfg)) return false; const std::string toml = std::string(REPO_ROOT) + "/tests/cases/" + dir + "/project.toml"; Project p; if (!parse_project(toml, &p, err)) { @@ -56,7 +67,7 @@ static bool compile_case(const char* dir, std::vector* image, std::stri if (!check_project(p, units, link, err)) { return false; } - return codegen_project(p, units, link, image, err); + return codegen_project(p, units, link, cfg, image, err); } // 从临时工程(globals.st + main.st + 可含 io 绑定)走完整管线 @@ -64,6 +75,8 @@ static bool compile_src(const char* name, const char* io_extra, const char* glob const char* main_st, std::vector* image, std::string* err) { using namespace compiler; + compiler::MachineConfig cfg; + if (!load_cfg(&cfg)) return false; const std::string dir = std::string(REPO_ROOT) + "/build/cg_tmp_" + name; std::filesystem::remove_all(dir); std::filesystem::create_directories(dir); @@ -100,7 +113,7 @@ static bool compile_src(const char* name, const char* io_extra, const char* glob if (!check_project(p, units, link, err)) { return false; } - return codegen_project(p, units, link, image, err); + return codegen_project(p, units, link, cfg, image, err); } // ---- 1. 用例 01:空 MAIN + RET ---- @@ -653,7 +666,9 @@ static bool test_line1() { LinkResult link; CHECK(link_project(p, units, &link, &err)); CHECK(check_project(p, units, link, &err)); - CHECK(codegen_project(p, units, link, &img, &err)); + compiler::MachineConfig cfg; + CHECK(load_cfg(&cfg)); + CHECK(codegen_project(p, units, link, cfg, &img, &err)); const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); diff --git a/tests/src/vm_test.cpp b/tests/src/vm_test.cpp index 01cec55..a968942 100644 --- a/tests/src/vm_test.cpp +++ b/tests/src/vm_test.cpp @@ -55,6 +55,10 @@ static void wslot(vm::Machine& m, int s, int64_t v) { static bool make_machine(const char* dir, vm::Machine* m, std::string* err) { using namespace compiler; + MachineConfig cfg; + if (!cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", err)) { + return false; + } const std::string toml = std::string(REPO_ROOT) + "/" + dir + "/project.toml"; Project p; if (!parse_project(toml, &p, err)) { @@ -76,7 +80,7 @@ static bool make_machine(const char* dir, vm::Machine* m, std::string* err) { return false; } std::vector img; - if (!codegen_project(p, units, link, &img, err)) { + if (!codegen_project(p, units, link, cfg, &img, err)) { return false; } return vm::Machine::create(img, m, err); From e187ed7b036005fe0d5bf00a2ff482d71304c04c Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:16:48 +0800 Subject: [PATCH 56/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20B=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=205=EF=BC=9A=E7=B1=BB=E5=9E=8B=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=85=8D=E7=BD=AE=E5=8C=96=EF=BC=88TypeInfo=EF=BC=89?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TypeInfo.h/cpp:TypeMeta(配置别名 + 内建基元解析),查询大小写不敏感; 提供 width/is_signed/is_float/tag/has_range/value_in_range - Codegen:layout_data 初值按 type_meta 的基元宽度/浮点写(删除手写宽度分支); const_id 的 tag 改查 type_meta - machine_test:+10 断言(BOOL 1B/range[0,1]/tag0、INT 2B 有符号、TIME 8B、大小写不敏感、未定义空) - 验收:line1 224 字节 hash 0xc3fe4ae74ad45900 不变、ctest 12/12 --- compiler/CMakeLists.txt | 3 +- compiler/include/compiler/TypeInfo.h | 41 ++++++++++++++++++++++++++++ compiler/src/Codegen.cpp | 20 +++++++------- compiler/src/TypeInfo.cpp | 41 ++++++++++++++++++++++++++++ tests/src/machine_test.cpp | 33 ++++++++++++++++++++++ 5 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 compiler/include/compiler/TypeInfo.h create mode 100644 compiler/src/TypeInfo.cpp diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 966e3d3..08045cf 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -17,7 +17,8 @@ add_library(compiler STATIC ./src/Codegen.cpp ./src/MachineConfig.cpp ./src/Stb.cpp - ./src/Codec.cpp) + ./src/Codec.cpp + ./src/TypeInfo.cpp) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include) diff --git a/compiler/include/compiler/TypeInfo.h b/compiler/include/compiler/TypeInfo.h new file mode 100644 index 0000000..45157a1 --- /dev/null +++ b/compiler/include/compiler/TypeInfo.h @@ -0,0 +1,41 @@ +/** + * @file TypeInfo.h + * @brief 语言类型元数据(machine.toml 别名 + 内建基元解析) + * @author + * @date 2026-08-21 + * + * @details 类型定义数据化(配置),运算/类型检查语义代码化: + * - 元数据(宽度/符号/浮点/base/range/tag)全部来自 machine.toml + 内建基元表 + * - 查询大小写不敏感(配置名大写,Linker type_name 小写) + */ + +#pragma once + +#include +#include + +#include "compiler/MachineConfig.h" + +namespace compiler { + + // 语言类型元数据 + struct TypeMeta { + const ConfigType* config = nullptr; // 配置类型行(别名 + range + tag) + const PrimType* prim = nullptr; // 内建基元(宽度/符号/浮点) + + uint32_t width() const { return prim ? prim->width : 0; } + bool is_signed() const { return prim ? prim->is_signed : false; } + bool is_float() const { return prim ? prim->is_float : false; } + uint32_t tag() const { return config ? config->tag : 0; } + bool has_range() const { return config ? config->has_range : false; } + bool value_in_range(int64_t v) const { + return !config || !config->has_range || + (v >= config->range_min && v <= config->range_max); + } + + explicit operator bool() const { return config != nullptr && prim != nullptr; } + }; + + // 按语言类型名查元数据(大小写不敏感;未找到返回空 TypeMeta) + TypeMeta type_meta(const MachineConfig& cfg, const std::string& name); +} diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index 7634283..d330815 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -42,6 +42,7 @@ #include "compiler/Codec.h" #include "compiler/MachineConfig.h" #include "compiler/Stb.h" +#include "compiler/TypeInfo.h" namespace compiler { namespace { @@ -589,20 +590,19 @@ namespace { const int64_t v = has_init ? init : 0; const size_t off = static_cast(s.address) * 8; data_.resize(off + 8, 0); - // 初值按基元宽度写(类型元数据来自配置别名 → base) - const ConfigType* ct = cfg_.find_type(uppercase_of(s.type_name)); - if (ct == nullptr) { + // 初值按类型元数据写(配置别名 → 基元宽度/浮点) + const TypeMeta tm = type_meta(cfg_, s.type_name); + if (!tm) { return fail("no type in machine.toml for '" + s.type_name + "'"); } - const PrimType* prim = MachineConfig::find_prim(ct->base); - if (prim == nullptr) { - return fail("no prim '" + ct->base + "' for type '" + ct->name + "'"); + if (tm.is_float()) { + return fail("float initializer not supported yet"); } - if (prim->width == 2 && !prim->is_float) { + if (tm.width() == 2) { const uint16_t iv = static_cast(v); data_[off] = static_cast(iv & 0xFFu); data_[off + 1] = static_cast((iv >> 8) & 0xFFu); - } else if (prim->width >= 8 && !prim->is_float) { + } else if (tm.width() == 8) { const uint64_t tv = static_cast(v); for (int i = 0; i < 8; ++i) { data_[off + i] = static_cast((tv >> (8 * i)) & 0xFFu); @@ -672,8 +672,8 @@ namespace { * @return const_id(u16) */ uint16_t const_id(const char* type_name, int64_t value) { - const ConfigType* ct = cfg_.find_type(type_name); - const uint32_t tag = ct ? ct->tag : 0; + const TypeMeta tm = type_meta(cfg_, type_name); + const uint32_t tag = tm ? tm.tag() : 0; for (size_t i = 0; i < consts_.size(); ++i) { if (consts_[i].tag == tag && consts_[i].value == static_cast(value)) { return static_cast(i); diff --git a/compiler/src/TypeInfo.cpp b/compiler/src/TypeInfo.cpp new file mode 100644 index 0000000..3c43e5c --- /dev/null +++ b/compiler/src/TypeInfo.cpp @@ -0,0 +1,41 @@ +/** + * @file TypeInfo.cpp + * @brief 语言类型元数据(machine.toml 别名 + 内建基元解析) + * @author + * @date 2026-08-21 + */ + +#include "compiler/TypeInfo.h" + +#include + +namespace compiler { + +namespace { + + std::string lower(const std::string& s) { + std::string out = s; + for (char& ch : out) { + ch = static_cast(std::tolower(static_cast(ch))); + } + return out; + } + +} // namespace + +TypeMeta type_meta(const MachineConfig& cfg, const std::string& name) { + TypeMeta m; + const std::string key = lower(name); + for (const ConfigType& t : cfg.types()) { + if (lower(t.name) == key) { + m.config = &t; + break; + } + } + if (m.config != nullptr) { + m.prim = MachineConfig::find_prim(m.config->base); + } + return m; +} + +} // namespace compiler diff --git a/tests/src/machine_test.cpp b/tests/src/machine_test.cpp index 1cdcb3b..1c83929 100644 --- a/tests/src/machine_test.cpp +++ b/tests/src/machine_test.cpp @@ -10,6 +10,7 @@ #include #include "compiler/MachineConfig.h" +#include "compiler/TypeInfo.h" #ifndef REPO_ROOT #define REPO_ROOT "." @@ -252,10 +253,42 @@ static bool test_positive_min() { return true; } +// ---- 4. 类型元数据(TypeInfo)---- + +static bool test_type_meta() { + compiler::MachineConfig cfg; + std::string err; + CHECK(cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &err)); + + // BOOL:base=uint8 → 1 字节、无符号、非浮点、tag 0、range [0,1] + const compiler::TypeMeta b = compiler::type_meta(cfg, "BOOL"); + CHECK(b && b.width() == 1 && !b.is_signed() && !b.is_float() && b.tag() == 0); + CHECK(b.has_range() && b.value_in_range(0) && b.value_in_range(1)); + CHECK(!b.value_in_range(2) && !b.value_in_range(-1)); + // 大小写不敏感(Linker type_name 是小写) + const compiler::TypeMeta b2 = compiler::type_meta(cfg, "bool"); + CHECK(b2 && b2.tag() == 0 && b2.width() == 1); + + // INT:int16 → 2 字节、有符号、tag 1、无 range + const compiler::TypeMeta i = compiler::type_meta(cfg, "int"); + CHECK(i && i.width() == 2 && i.is_signed() && !i.is_float() && i.tag() == 1); + CHECK(!i.has_range() || i.value_in_range(30000)); + + // TIME:int64 → 8 字节、有符号、tag 2 + const compiler::TypeMeta t = compiler::type_meta(cfg, "TIME"); + CHECK(t && t.width() == 8 && t.is_signed() && !t.is_float() && t.tag() == 2); + + // 未定义类型 → 空 + CHECK(!compiler::type_meta(cfg, "REAL")); + CHECK(!compiler::type_meta(cfg, "")); + return true; +} + int main() { if (!test_positive()) return 1; if (!test_negative()) return 1; if (!test_positive_min()) return 1; + if (!test_type_meta()) return 1; std::printf("machine_test: %d checks passed\n", g_checks); return 0; } From a308ae713d962659dfc78da51976c9969194f342 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:20:35 +0800 Subject: [PATCH 57/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20B=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=206=EF=BC=9A.stb=20=E5=86=99=E4=BE=A7=E6=96=B0?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=EF=BC=88=E5=A4=B4=20104=20+=20=E5=9E=8B?= =?UTF-8?q?=E5=8F=B7=E6=A0=87=E8=AF=86=20+=20SHA-256=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=B0=BE=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stb:kHeaderSize 104、kSha256Size 32;SHA-256 实现(FIPS 180-4,含标准测试向量验证); fill_model_id(name+version 拼 32 字节补 '\0');StbView 解析新格式 + model_id()/model_matches()/sha_ok()(数据段不含尾、缺尾报错) - Codegen:assemble_image 写型号标识(偏移 72)+ 文件尾 SHA-256 - main.cpp:--disasm 显示 model/sha;写后自检(ok+sha+型号匹配,失败报错) - 测试:machine_test +SHA-256 已知向量(空串/abc)与型号标识断言(53 断言); codegen_test data_len 断言 +32(SHA 尾);ctest 12/12 - 验证:line1 288 字节 model=STATOR1 sha=ok;篡改一字节 sha=BAD --- Doc/isa/stb文件格式.md | 2 +- compiler/include/compiler/Stb.h | 17 +++- compiler/src/Codegen.cpp | 18 +++- compiler/src/Stb.cpp | 159 +++++++++++++++++++++++++++++++- compiler/src/main.cpp | 17 +++- tests/src/codegen_test.cpp | 12 +-- tests/src/machine_test.cpp | 35 +++++++ 7 files changed, 245 insertions(+), 15 deletions(-) diff --git a/Doc/isa/stb文件格式.md b/Doc/isa/stb文件格式.md index ee4f3ec..554046b 100644 --- a/Doc/isa/stb文件格式.md +++ b/Doc/isa/stb文件格式.md @@ -2,7 +2,7 @@ 编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(224 字节)做真实拆解。 -> **12.13 修订**(尚未落地到代码,本文拆解仍为旧格式):头新增型号标识[32](头 72 → 104 字节,段偏移基准同步后移),文件尾新增 SHA-256[32]。落地后本文全部字节数字需重新生成。 +> **12.13 修订**(编译器写侧已落地,vm 读侧待步骤 8):头新增型号标识[32](头 72 → 104 字节,段偏移基准同步后移),文件尾新增 SHA-256[32](对文件尾之前全部内容计算)。line1 产物现为 288 字节。本文拆解仍为旧格式数字,需在 vm 侧落地后统一重新生成。 ## 概览 diff --git a/compiler/include/compiler/Stb.h b/compiler/include/compiler/Stb.h index af0070d..6ee618f 100644 --- a/compiler/include/compiler/Stb.h +++ b/compiler/include/compiler/Stb.h @@ -19,12 +19,14 @@ namespace compiler { - // ---- .stb 格式常量(契约)---- + // ---- .stb 格式常量(契约;12.13 修订:头 104 = 原 72 + 型号标识[32],文件尾 SHA-256[32])---- static const uint32_t kMagic = 0x43545353u; // "STSC" 小端 static const uint32_t kVersion = 1; - static const size_t kHeaderSize = 72; + static const size_t kHeaderSize = 104; static const size_t kConstEntrySize = 12; static const size_t kFuncRowSize = 12; + static const size_t kSha256Size = 32; + static const size_t kModelIdSize = 32; static const uint64_t kFnvBasis = 0xcbf29ce484222325ull; static const uint64_t kFnvPrime = 0x100000001b3ull; @@ -38,6 +40,12 @@ namespace compiler { uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len); uint64_t fnv1a64(const uint8_t* data, size_t len); + // SHA-256(文件完整性校验,非密码学用途之外的完整性) + void sha256(const uint8_t* data, size_t len, uint8_t out[kSha256Size]); + + // 型号标识:name + version 拼 32 字节 ASCII(如 "STATOR1"),不足补 '\0',超长截断 + void fill_model_id(const std::string& name, uint32_t version, char out[kModelIdSize]); + // 只读视图(--disasm 用;校验魔数/版本/段边界) class StbView { public: @@ -71,6 +79,11 @@ namespace compiler { const uint8_t* data_bytes() const; size_t data_len() const; + // 12.13:型号标识与 SHA-256 校验 + std::string model_id() const; // 头 72..103(补 '\0' 后字符串) + bool model_matches(const std::string& name, uint32_t version) const; + bool sha_ok() const; // 文件尾 32 字节 SHA-256 校验 + private: StbView() : buf_(0), len_(0) {} diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index d330815..a3b6584 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -694,7 +694,8 @@ namespace { code_total += static_cast(f.code.size()) * 4; } const uint32_t off_data = off_code + code_total; - const uint32_t off_end = off_data + static_cast(data_.size()); + const uint32_t off_end = off_data + static_cast(data_.size()) + + static_cast(kSha256Size); std::vector& b = *image_; b.assign(off_end, 0); @@ -728,6 +729,13 @@ namespace { put_le32(b, 64, off_data); put_le32(b, 68, off_data); + // 型号标识[32](偏移 72;12.13 修订) + char mid[kModelIdSize]; + fill_model_id(cfg_.model_name(), cfg_.version(), mid); + for (size_t i = 0; i < kModelIdSize; ++i) { + b[72 + i] = static_cast(mid[i]); + } + for (size_t i = 0; i < consts_.size(); ++i) { const size_t o = off_const + i * kConstEntrySize; put_le32(b, o, consts_[i].tag); @@ -755,6 +763,14 @@ namespace { for (size_t i = 0; i < data_.size(); ++i) { b[off_data + i] = data_[i]; } + + // 文件尾 SHA-256(对文件尾之前全部内容计算) + const size_t content_len = off_data + data_.size(); + uint8_t digest[kSha256Size]; + sha256(&b[0], content_len, digest); + for (size_t i = 0; i < kSha256Size; ++i) { + b[content_len + i] = digest[i]; + } return true; } diff --git a/compiler/src/Stb.cpp b/compiler/src/Stb.cpp index 2b63f84..156fc2f 100644 --- a/compiler/src/Stb.cpp +++ b/compiler/src/Stb.cpp @@ -44,8 +44,129 @@ namespace { return v; } + // ---- SHA-256(FIPS 180-4)---- + + const uint32_t kShaK[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + + inline uint32_t rotr(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } + + struct Sha256 { + uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + uint64_t total = 0; + uint8_t block[64]; + size_t block_len = 0; + + void update(const uint8_t* data, size_t len) { + total += len; + while (len > 0) { + const size_t take = (block_len < 64) ? (64 - block_len) : 0; + const size_t n = len < take ? len : take; + if (n > 0) { + for (size_t i = 0; i < n; ++i) { + block[block_len + i] = data[i]; + } + block_len += n; + data += n; + len -= n; + if (block_len == 64) { + process(); + block_len = 0; + } + } else { + break; + } + } + } + + void process() { + uint32_t w[64]; + for (int i = 0; i < 16; ++i) { + w[i] = get_be32(block + i * 4); + } + for (int i = 16; i < 64; ++i) { + const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3]; + uint32_t e = h[4], f = h[5], g = h[6], hh = h[7]; + for (int i = 0; i < 64; ++i) { + const uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); + const uint32_t ch = (e & f) ^ (~e & g); + const uint32_t t1 = hh + s1 + ch + kShaK[i] + w[i]; + const uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); + const uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + const uint32_t t2 = s0 + maj; + hh = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + } + + void final(uint8_t out[32]) { + const uint64_t bitlen = total * 8; + const uint8_t pad = 0x80; + update(&pad, 1); + const uint8_t zeros[64] = {0}; + // 补零到 block_len == 56(跨块时先补满当前块再补) + while (block_len != 56) { + const size_t n = (block_len < 56) ? (56 - block_len) : (64 - block_len); + update(zeros, n); + } + // 64 位大端比特长度 + for (int i = 0; i < 8; ++i) { + const uint8_t b2[1] = {static_cast((bitlen >> (56 - 8 * i)) & 0xFF)}; + update(b2, 1); + } + for (int i = 0; i < 8; ++i) { + out[i * 4 + 0] = static_cast((h[i] >> 24) & 0xFF); + out[i * 4 + 1] = static_cast((h[i] >> 16) & 0xFF); + out[i * 4 + 2] = static_cast((h[i] >> 8) & 0xFF); + out[i * 4 + 3] = static_cast(h[i] & 0xFF); + } + } + + static uint32_t get_be32(const uint8_t* p) { + return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | static_cast(p[3]); + } + }; + } // namespace +void sha256(const uint8_t* data, size_t len, uint8_t out[kSha256Size]) { + Sha256 s; + s.update(data, len); + s.final(out); +} + +void fill_model_id(const std::string& name, uint32_t version, char out[kModelIdSize]) { + const std::string id = name + std::to_string(version); + for (size_t i = 0; i < kModelIdSize; ++i) { + out[i] = i < id.size() ? id[i] : '\0'; + } +} + StbView StbView::from(const uint8_t* buf, size_t len) { StbView v; v.buf_ = buf; @@ -77,11 +198,15 @@ StbView StbView::from(const uint8_t* buf, size_t len) { v.offset_fb_ = get_le32(buf + 64); v.offset_data_ = get_le32(buf + 68); - // 段校验 + // 段校验(12.13:文件尾 SHA-256[32] 在数据段之后) + if (len < static_cast(v.offset_data_) + kSha256Size) { + v.err_ = "missing sha256 tail"; + return v; + } const uint64_t offs[5] = {get_le32(buf + 52), get_le32(buf + 56), v.offset_code_, v.offset_fb_, v.offset_data_}; for (int i = 0; i < 5; ++i) { - if (offs[i] < kHeaderSize || offs[i] > len) { + if (offs[i] < kHeaderSize || offs[i] > len - kSha256Size) { v.err_ = "segment offset out of range"; return v; } @@ -154,7 +279,35 @@ const uint8_t* StbView::data_bytes() const { } size_t StbView::data_len() const { - return ok_ ? len_ - offset_data_ : 0; + return ok_ ? (len_ - kSha256Size) - offset_data_ : 0; +} + +std::string StbView::model_id() const { + if (!ok_) { + return ""; + } + std::string s(reinterpret_cast(buf_ + 72), kModelIdSize); + const size_t z = s.find('\0'); + if (z != std::string::npos) { + s.resize(z); + } + return s; +} + +bool StbView::model_matches(const std::string& name, uint32_t version) const { + char want[kModelIdSize]; + fill_model_id(name, version, want); + return std::memcmp(buf_ + 72, want, kModelIdSize) == 0; +} + +bool StbView::sha_ok() const { + if (!ok_) { + return false; + } + const size_t content_len = len_ - kSha256Size; + uint8_t digest[kSha256Size]; + sha256(buf_, content_len, digest); + return std::memcmp(buf_ + content_len, digest, kSha256Size) == 0; } bool read_stb_file(const char* path, std::vector* out, std::string* err) { diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index f10e788..27d5169 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -47,8 +47,10 @@ namespace { } std::printf("image: %s (%zu bytes, %u functions, %u globals, entry fn %u)\n", path, bytes.size(), v.n_funcs(), v.n_globals(), v.entry_fn_id()); - std::printf(" dt_ms=%u cycle_limit=%u hash=0x%016llx\n", v.dt_ms(), - v.cycle_limit(), static_cast(v.project_hash())); + std::printf(" dt_ms=%u cycle_limit=%u hash=0x%016llx model=%s sha=%s\n", + v.dt_ms(), v.cycle_limit(), + static_cast(v.project_hash()), + v.model_id().c_str(), v.sha_ok() ? "ok" : "BAD"); if (v.n_consts()) { std::printf("constants:\n"); @@ -191,6 +193,17 @@ int main(int argc, char** argv) { return 1; } + // 写后自检:型号标识与 SHA-256 + const compiler::StbView self = compiler::StbView::from(image); + if (!self.ok() || !self.sha_ok()) { + std::fprintf(stderr, "error: self-check failed: %s\n", self.error().c_str()); + return 1; + } + if (!self.model_matches(cfg.model_name(), cfg.version())) { + std::fprintf(stderr, "error: self-check model mismatch\n"); + return 1; + } + // sidecar:I/O 绑定 var → 槽号 → channel/bit(不进映像,执行器采样用) std::vector bindings; for (const compiler::IoBinding& b : proj.io) { diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 8c6cda1..ade3955 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -187,7 +187,7 @@ static bool test_case09() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); CHECK(v.header().n_globals == 1); - CHECK(v.data_len() == 8); // 8 字节定宽槽 + CHECK(v.data_len() == 8 + 32); // 8 字节定宽槽 + SHA-256 尾 CHECK(v.data_bytes()[0] == 5); // G1 初值 5(小端低位) CHECK(v.data_bytes()[1] == 0); @@ -261,7 +261,7 @@ static bool test_layout() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); CHECK(v.header().n_globals == 3); - CHECK(v.data_len() == 24); // 3 槽 × 8 字节定宽 + CHECK(v.data_len() == 24 + 32); // 3 槽 × 8 字节定宽 + SHA-256 尾 CHECK(v.data_bytes()[0] == 0); // B 无初值 CHECK(v.data_bytes()[8] == 0x2C && v.data_bytes()[9] == 0x01); // I = 300(槽 1) CHECK(v.data_bytes()[16] == 10); // T = 10ms(槽 2) @@ -565,7 +565,7 @@ static bool test_case15() { CHECK(v.ok()); CHECK(v.header().n_funcs == 2); // FB 占位 + MAIN CHECK(v.header().entry_fn_id == 1); - CHECK(v.data_len() == 24); // 实例 3 槽 × 8 字节定宽 + CHECK(v.data_len() == 24 + 32); // 实例 3 槽 × 8 字节定宽 + SHA-256 尾 const isa::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 12); @@ -603,7 +603,7 @@ static bool test_case17() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); - CHECK(v.data_len() == 32); // in@0 pt@8 q@16 et@24 + CHECK(v.data_len() == 32 + 32); // in@0 pt@8 q@16 et@24 + SHA-256 尾 const isa::FuncRow r = v.func_row(0); CHECK(r.nregs == 11); const uint32_t* ins = reinterpret_cast(v.code_bytes()); @@ -632,7 +632,7 @@ static bool test_case18() { const isa::ImageView v = isa::ImageView::from(img); CHECK(v.ok()); - CHECK(v.data_len() == 72); // tf 4 槽 + c 5 槽 = 9 槽 × 8 + CHECK(v.data_len() == 72 + 32); // tf 4 槽 + c 5 槽 = 9 槽 × 8 + SHA-256 尾 const isa::FuncRow r = v.func_row(0); CHECK(r.nregs == 14); const uint32_t* ins = reinterpret_cast(v.code_bytes()); @@ -675,7 +675,7 @@ static bool test_line1() { CHECK(v.header().n_globals == 4); CHECK(v.header().n_funcs == 2); CHECK(v.header().entry_fn_id == 1); - CHECK(v.data_len() == 56); // 7 槽 × 8 字节定宽 + CHECK(v.data_len() == 56 + 32); // 7 槽 × 8 字节定宽 + SHA-256 尾 const isa::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 13); diff --git a/tests/src/machine_test.cpp b/tests/src/machine_test.cpp index 1c83929..a17bfb9 100644 --- a/tests/src/machine_test.cpp +++ b/tests/src/machine_test.cpp @@ -6,10 +6,12 @@ */ #include +#include #include #include #include "compiler/MachineConfig.h" +#include "compiler/Stb.h" #include "compiler/TypeInfo.h" #ifndef REPO_ROOT @@ -284,11 +286,44 @@ static bool test_type_meta() { return true; } +// ---- 5. SHA-256 已知向量 + 型号标识 ---- + +static bool test_sha256() { + // 标准测试向量 + { + uint8_t out[compiler::kSha256Size]; + compiler::sha256(nullptr, 0, out); + const uint8_t want[32] = {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}; + CHECK(std::memcmp(out, want, 32) == 0); + } + { + const uint8_t abc[] = {'a', 'b', 'c'}; + uint8_t out[compiler::kSha256Size]; + compiler::sha256(abc, 3, out); + const uint8_t want[32] = {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, + 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, + 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}; + CHECK(std::memcmp(out, want, 32) == 0); + } + // 型号标识:STATOR + 1 → "STATOR1" 补 '\0' + char mid[compiler::kModelIdSize]; + compiler::fill_model_id("STATOR", 1, mid); + CHECK(std::strncmp(mid, "STATOR1", 7) == 0); + CHECK(mid[7] == '\0'); + CHECK(mid[31] == '\0'); + return true; +} + int main() { if (!test_positive()) return 1; if (!test_negative()) return 1; if (!test_positive_min()) return 1; if (!test_type_meta()) return 1; + if (!test_sha256()) return 1; std::printf("machine_test: %d checks passed\n", g_checks); return 0; } From 5fccbadaa8697727a2561df3d6d3de4d1e9a7d95 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:24:57 +0800 Subject: [PATCH 58/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20C=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=207=EF=BC=9Aisa=20=E6=8B=86=E5=87=BA=20Image=EF=BC=8C?= =?UTF-8?q?vm=20=E4=BE=9D=E8=B5=96=E6=94=B6=E7=AA=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删 isa/Image.h + Image.cpp(ImageView/FNV/写文件/sidecar 不再属于 isa) - vm 自实现只读视图 vm/Image.h/cpp:头 104(12.13)/段校验/缺 SHA 尾报错 (型号匹配与 SHA 校验留步骤 8);Machine 改用 vm::Image + vm::ImageHeader - executor:本地读文件(替换 isa::read_stb_file) - 测试:isa_test 删映像/FNV 部分;codegen_test/vm_test 改用 compiler::StbView / vm::ConstEntry;vm_test 手拼映像适配新格式(头 104 + 尾 32 占位) - 验证:line1 编译 288 字节 → BytecodeExecutor 回放 Q 正确;ctest 12/12 --- executor/src/main.cpp | 17 +- isa/CMakeLists.txt | 3 +- isa/include/isa/Image.h | 125 --------------- isa/src/Image.cpp | 321 ------------------------------------- tests/src/codegen_test.cpp | 134 ++++++++-------- tests/src/isa_test.cpp | 104 ------------ tests/src/vm_test.cpp | 25 ++- vm/CMakeLists.txt | 3 +- vm/include/vm/Image.h | 78 +++++++++ vm/include/vm/Machine.h | 6 +- vm/src/Image.cpp | 151 +++++++++++++++++ vm/src/Machine.cpp | 14 +- 12 files changed, 334 insertions(+), 647 deletions(-) delete mode 100644 isa/include/isa/Image.h delete mode 100644 isa/src/Image.cpp create mode 100644 vm/include/vm/Image.h create mode 100644 vm/src/Image.cpp diff --git a/executor/src/main.cpp b/executor/src/main.cpp index a11e14d..a1ca3a5 100644 --- a/executor/src/main.cpp +++ b/executor/src/main.cpp @@ -21,7 +21,7 @@ #include #include "isa/Encode.h" -#include "isa/Image.h" +#include "vm/Image.h" #include "vm/Machine.h" namespace { @@ -199,8 +199,17 @@ int main(int argc, char** argv) { std::string err; std::vector bytes; - if (!isa::read_stb_file(stb_path.c_str(), &bytes, &err)) { - std::fprintf(stderr, "error: %s\n", err.c_str()); + { + std::ifstream in(stb_path, std::ios::binary); + if (!in) { + std::fprintf(stderr, "error: cannot open: %s\n", stb_path.c_str()); + return 1; + } + bytes.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + const vm::Image img = vm::Image::from(bytes); + if (!img.ok()) { + std::fprintf(stderr, "error: %s\n", img.error().c_str()); return 1; } vm::Machine machine; @@ -233,7 +242,7 @@ int main(int argc, char** argv) { } } - const isa::ImageHeader& h = machine.header(); + const vm::ImageHeader& h = machine.header(); std::printf("image: %s (%zu bytes, dt_ms=%u, cycle_limit=%u)\n", stb_path.c_str(), bytes.size(), h.dt_ms, h.cycle_limit); diff --git a/isa/CMakeLists.txt b/isa/CMakeLists.txt index 508b752..9df22ef 100644 --- a/isa/CMakeLists.txt +++ b/isa/CMakeLists.txt @@ -7,7 +7,6 @@ project(ISA DESCRIPTION "指令集架构") add_library(isa STATIC - ./src/Encode.cpp - ./src/Image.cpp) + ./src/Encode.cpp) target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) \ No newline at end of file diff --git a/isa/include/isa/Image.h b/isa/include/isa/Image.h deleted file mode 100644 index fd1cc47..0000000 --- a/isa/include/isa/Image.h +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @file Image.h - * @brief 映像头、段、FNV-1a 哈希、.stb 文件与 sidecar - * @author - * @date 2026-08-19 - */ - -#pragma once - -#include -#include -#include -#include - -#include "isa/Types.h" - -namespace isa { - - // 魔数 "STSC"(小端 u32)、版本 - static const uint32_t kMagic = 0x43545353u; - static const uint32_t kVersion = 1; - - // 头与表行宽(见 Doc/isa/指令与映像.md) - static const size_t kHeaderSize = 72; - static const size_t kConstEntrySize = 12; - static const size_t kFuncRowSize = 12; - - // FNV-1a 64:basis / prime(见 Doc/isa/指令与映像.md) - static const uint64_t kFnvBasis = 0xcbf29ce484222325ull; - static const uint64_t kFnvPrime = 0x100000001b3ull; - - // FNV-1a 64 增量与一次性 - uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len); - uint64_t fnv1a64(const uint8_t* data, size_t len); - - // 映像头(字节布局见 Doc/isa/指令与映像.md) - struct ImageHeader { - uint32_t cycle_limit; - uint32_t dt_ms; - uint64_t project_hash; - uint32_t entry_fn_id; - uint32_t n_globals; - uint32_t n_i; - uint32_t n_q; - uint32_t n_m; - uint32_t n_consts; - uint32_t n_funcs; - uint32_t offset_const; - uint32_t offset_funcs; - uint32_t offset_code; - uint32_t offset_fb; - uint32_t offset_data; - }; - - // 常量表一行 - struct ConstEntry { - types::TypeTag tag; - uint64_t value; - }; - - // 函数表一行 - struct FuncRow { - uint32_t nregs; - uint32_t code_offset; // 相对字节码段起点 - uint32_t code_len; // 指令条数 - }; - - // I/O 绑定(sidecar 一行) - struct IoBinding { - std::string var; - uint32_t slot; - uint32_t channel; - uint32_t bit; - bool is_input; // true = [[io.input]],false = [[io.output]] - }; - - // 只读视图:校验过的映像 - class ImageView { - public: - // 默认构造为无效态(ok() == false,error() == "uninitialized") - ImageView(); - static ImageView from(const uint8_t* buf, size_t len); - static ImageView from(const std::vector& buf); - - bool ok() const; - const std::string& error() const; - const ImageHeader& header() const; - - const uint8_t* raw() const; - size_t raw_len() const; - - ConstEntry const_entry(size_t i) const; - FuncRow func_row(size_t i) const; - - const uint8_t* code_bytes() const; // 字节码段起点 - size_t code_len() const; // 字节数 - const uint8_t* data_bytes() const; // 数据段起点 - size_t data_len() const; // 字节数 - - private: - // 私有成员与内部构造辅助(from 使用) - - const uint8_t* buf_; - size_t len_; - bool ok_; - std::string err_; - ImageHeader hdr_; - }; - - // 写最小映像:空槽 + MAIN(fn_id=0)+ 一条 RET - std::vector write_ret_main(uint32_t cycle_limit, uint32_t dt_ms, - uint64_t project_hash); - - // .stb 文件读写(纯字节,校验交给 read_image / ImageView) - bool write_stb_file(const char* path, const std::vector& image, - std::string* err); - bool read_stb_file(const char* path, std::vector* image, - std::string* err); - - // sidecar 生成与写文件(格式见 Doc/isa/指令与映像.md) - std::string make_sidecar(const std::vector& bindings); - bool write_sidecar_file(const char* path, - const std::vector& bindings, - std::string* err); -} diff --git a/isa/src/Image.cpp b/isa/src/Image.cpp deleted file mode 100644 index 1d2fd7b..0000000 --- a/isa/src/Image.cpp +++ /dev/null @@ -1,321 +0,0 @@ -/** - * @file Image.cpp - * @brief 映像头、段、FNV-1a 哈希、.stb 文件与 sidecar - * @author - * @date 2026-08-19 - */ - -#include "isa/Image.h" - -#include -#include - -#include "isa/Instr.h" - -namespace isa { - -// ---- 小端读写(只放 .cpp,不把 packed struct 当 ABI)---- - -static void put_le32(std::vector& b, size_t off, uint32_t v) { - b[off + 0] = static_cast(v & 0xFFu); - b[off + 1] = static_cast((v >> 8) & 0xFFu); - b[off + 2] = static_cast((v >> 16) & 0xFFu); - b[off + 3] = static_cast((v >> 24) & 0xFFu); -} - -static uint32_t get_le32(const uint8_t* p) { - return static_cast(p[0]) - | (static_cast(p[1]) << 8) - | (static_cast(p[2]) << 16) - | (static_cast(p[3]) << 24); -} - -static void put_le64(std::vector& b, size_t off, uint64_t v) { - for (int i = 0; i < 8; ++i) { - b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); - } -} - -static uint64_t get_le64(const uint8_t* p) { - uint64_t v = 0; - for (int i = 0; i < 8; ++i) { - v |= static_cast(p[i]) << (8 * i); - } - return v; -} - -// ---- FNV-1a 64 ---- - -uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len) { - for (size_t i = 0; i < len; ++i) { - h ^= data[i]; - h *= kFnvPrime; - } - return h; -} - -uint64_t fnv1a64(const uint8_t* data, size_t len) { - return fnv1a64_update(kFnvBasis, data, len); -} - -// ---- 最小映像:空槽 + MAIN + 一条 RET ---- - -std::vector write_ret_main(uint32_t cycle_limit, uint32_t dt_ms, - uint64_t project_hash) { - std::vector b(kHeaderSize + kFuncRowSize + 4, 0); - - put_le32(b, 0, kMagic); - put_le32(b, 4, kVersion); - put_le32(b, 8, cycle_limit); - put_le32(b, 12, dt_ms); - put_le64(b, 16, project_hash); - put_le32(b, 24, 0); // entry_fn_id = MAIN = 0 - // n_globals / n_i / n_q / n_m = 0 - // n_consts = 0 - put_le32(b, 48, 1); // n_funcs = 1 - - const size_t off_funcs = kHeaderSize; // 72 - const size_t off_code = off_funcs + kFuncRowSize; // 84 - const size_t off_end = off_code + 4; // 88 - - put_le32(b, 52, static_cast(off_funcs)); // offset_const - put_le32(b, 56, static_cast(off_funcs)); // offset_funcs - put_le32(b, 60, static_cast(off_code)); // offset_code - put_le32(b, 64, static_cast(off_end)); // offset_fb - put_le32(b, 68, static_cast(off_end)); // offset_data - - // 函数表一行:MAIN,无寄存器,代码 0 起 1 条 - put_le32(b, off_funcs + 0, 0); // nregs - put_le32(b, off_funcs + 4, 0); // code_offset - put_le32(b, off_funcs + 8, 1); // code_len - - // 字节码:RET - put_le32(b, off_code, pack(Op::RET, 0, 0, 0)); - - return b; -} - -// ---- 只读视图 ---- - -ImageView::ImageView() - : buf_(0), len_(0), ok_(false), err_("uninitialized"), hdr_() {} - -ImageView ImageView::from(const uint8_t* buf, size_t len) { - ImageView v; - v.buf_ = buf; - v.len_ = len; - - if (buf == 0) { - v.err_ = "null buffer"; - return v; - } - if (len < kHeaderSize) { - v.err_ = "image too short"; - return v; - } - if (get_le32(buf + 0) != kMagic) { - v.err_ = "bad magic"; - return v; - } - if (get_le32(buf + 4) != kVersion) { - v.err_ = "bad version"; - return v; - } - - ImageHeader& h = v.hdr_; - h.cycle_limit = get_le32(buf + 8); - h.dt_ms = get_le32(buf + 12); - h.project_hash = get_le64(buf + 16); - h.entry_fn_id = get_le32(buf + 24); - h.n_globals = get_le32(buf + 28); - h.n_i = get_le32(buf + 32); - h.n_q = get_le32(buf + 36); - h.n_m = get_le32(buf + 40); - h.n_consts = get_le32(buf + 44); - h.n_funcs = get_le32(buf + 48); - h.offset_const = get_le32(buf + 52); - h.offset_funcs = get_le32(buf + 56); - h.offset_code = get_le32(buf + 60); - h.offset_fb = get_le32(buf + 64); - h.offset_data = get_le32(buf + 68); - - // 段校验:连续、单调不减、末段终点不越界 - const uint64_t offs[5] = { - h.offset_const, h.offset_funcs, h.offset_code, h.offset_fb, h.offset_data, - }; - for (int i = 0; i < 5; ++i) { - if (offs[i] < kHeaderSize || offs[i] > len) { - v.err_ = "segment offset out of range"; - return v; - } - if (i > 0 && offs[i] < offs[i - 1]) { - v.err_ = "segment offsets not monotonic"; - return v; - } - } - - const uint64_t const_len = h.offset_funcs - h.offset_const; - const uint64_t funcs_len = h.offset_code - h.offset_funcs; - if (const_len != static_cast(h.n_consts) * kConstEntrySize) { - v.err_ = "const table size mismatch"; - return v; - } - if (funcs_len != static_cast(h.n_funcs) * kFuncRowSize) { - v.err_ = "function table size mismatch"; - return v; - } - if ((h.offset_fb - h.offset_code) % 4 != 0) { - v.err_ = "code segment not 4-byte aligned"; - return v; - } - if (h.entry_fn_id >= h.n_funcs && h.n_funcs != 0) { - v.err_ = "entry fn_id out of range"; - return v; - } - - v.ok_ = true; - v.err_.clear(); - return v; -} - -ImageView ImageView::from(const std::vector& buf) { - return from(buf.data(), buf.size()); -} - -bool ImageView::ok() const { return ok_; } -const std::string& ImageView::error() const { return err_; } -const ImageHeader& ImageView::header() const { return hdr_; } -const uint8_t* ImageView::raw() const { return buf_; } -size_t ImageView::raw_len() const { return len_; } - -ConstEntry ImageView::const_entry(size_t i) const { - ConstEntry e; - e.tag = types::Bool; - e.value = 0; - if (ok_ && i < hdr_.n_consts) { - const uint8_t* p = buf_ + hdr_.offset_const + i * kConstEntrySize; - const uint32_t tag = get_le32(p); - if (tag <= static_cast(types::Time)) { - e.tag = static_cast(tag); - } - e.value = get_le64(p + 4); - } - return e; -} - -FuncRow ImageView::func_row(size_t i) const { - FuncRow r = {0, 0, 0}; - if (ok_ && i < hdr_.n_funcs) { - const uint8_t* p = buf_ + hdr_.offset_funcs + i * kFuncRowSize; - r.nregs = get_le32(p + 0); - r.code_offset = get_le32(p + 4); - r.code_len = get_le32(p + 8); - } - return r; -} - -const uint8_t* ImageView::code_bytes() const { - return ok_ ? buf_ + hdr_.offset_code : 0; -} - -size_t ImageView::code_len() const { - return ok_ ? hdr_.offset_fb - hdr_.offset_code : 0; -} - -const uint8_t* ImageView::data_bytes() const { - return ok_ ? buf_ + hdr_.offset_data : 0; -} - -size_t ImageView::data_len() const { - return ok_ ? len_ - hdr_.offset_data : 0; -} - -// ---- .stb 文件读写 ---- - -bool write_stb_file(const char* path, const std::vector& image, - std::string* err) { - FILE* f = std::fopen(path, "wb"); - if (f == 0) { - if (err) { - *err = std::string("cannot open for write: ") + path; - } - return false; - } - const bool ok = image.empty() || std::fwrite(&image[0], 1, image.size(), f) == image.size(); - std::fclose(f); - if (!ok && err) { - *err = std::string("write failed: ") + path; - } - return ok; -} - -bool read_stb_file(const char* path, std::vector* image, - std::string* err) { - FILE* f = std::fopen(path, "rb"); - if (f == 0) { - if (err) { - *err = std::string("cannot open for read: ") + path; - } - return false; - } - std::fseek(f, 0, SEEK_END); - const long size = std::ftell(f); - std::fseek(f, 0, SEEK_SET); - if (size < 0) { - std::fclose(f); - if (err) { - *err = "tell failed"; - } - return false; - } - image->resize(static_cast(size)); - const bool ok = size == 0 || std::fread(&(*image)[0], 1, static_cast(size), f) == static_cast(size); - std::fclose(f); - if (!ok && err) { - *err = std::string("read failed: ") + path; - } - return ok; -} - -// ---- sidecar ---- - -std::string make_sidecar(const std::vector& bindings) { - std::string out; - for (size_t i = 0; i < bindings.size(); ++i) { - const IoBinding& b = bindings[i]; - char buf[64]; - out += b.is_input ? "[[io.input]]\n" : "[[io.output]]\n"; - out += "var = \""; - out += b.var; - out += "\"\n"; - std::snprintf(buf, sizeof buf, "slot = %u\n", static_cast(b.slot)); - out += buf; - std::snprintf(buf, sizeof buf, "channel = %u\n", static_cast(b.channel)); - out += buf; - std::snprintf(buf, sizeof buf, "bit = %u\n", static_cast(b.bit)); - out += buf; - out += "\n"; - } - return out; -} - -bool write_sidecar_file(const char* path, - const std::vector& bindings, - std::string* err) { - FILE* f = std::fopen(path, "wb"); - if (f == 0) { - if (err) { - *err = std::string("cannot open for write: ") + path; - } - return false; - } - const std::string s = make_sidecar(bindings); - const bool ok = s.empty() || std::fwrite(s.data(), 1, s.size(), f) == s.size(); - std::fclose(f); - if (!ok && err) { - *err = std::string("write failed: ") + path; - } - return ok; -} - -} // namespace isa diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index ade3955..4f28b18 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -16,7 +16,7 @@ #include "compiler/Project.h" #include "compiler/Typecheck.h" #include "isa/Encode.h" -#include "isa/Image.h" +#include "compiler/Stb.h" #ifndef REPO_ROOT #define REPO_ROOT "." @@ -123,17 +123,17 @@ static bool test_case01() { std::string err; CHECK(compile_case("01_empty_main", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().cycle_limit == 1000); - CHECK(v.header().dt_ms == 10); - CHECK(v.header().entry_fn_id == 0); - CHECK(v.header().n_funcs == 1); - CHECK(v.header().n_consts == 0); - CHECK(v.header().n_globals == 0); - CHECK(v.header().project_hash != isa::kFnvBasis); + CHECK(v.cycle_limit() == 1000); + CHECK(v.dt_ms() == 10); + CHECK(v.entry_fn_id() == 0); + CHECK(v.n_funcs() == 1); + CHECK(v.n_consts() == 0); + CHECK(v.n_globals() == 0); + CHECK(v.project_hash() != compiler::kFnvBasis); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 8); // 帧基址(调用约定区) CHECK(r.code_len == 1); @@ -150,12 +150,12 @@ static bool test_case02() { std::string err; CHECK(compile_case("02_bool_assign", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_funcs == 1); - CHECK(v.header().n_consts == 2); // TRUE 与 FALSE + CHECK(v.n_funcs() == 1); + CHECK(v.n_consts() == 2); // TRUE 与 FALSE - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 10); // a, b(r8 起) CHECK(r.code_len == 3); @@ -170,10 +170,10 @@ static bool test_case02() { CHECK(std::strcmp(buf, "RET") == 0); // 常量表:0 = BOOL 1,1 = BOOL 0 - const isa::ConstEntry c0 = v.const_entry(0); - CHECK(c0.tag == isa::types::Bool && c0.value == 1); - const isa::ConstEntry c1 = v.const_entry(1); - CHECK(c1.tag == isa::types::Bool && c1.value == 0); + const compiler::ConstEntry c0 = v.const_entry(0); + CHECK(c0.tag == 0 && c0.value == 1); + const compiler::ConstEntry c1 = v.const_entry(1); + CHECK(c1.tag == 0 && c1.value == 0); return true; } @@ -184,14 +184,14 @@ static bool test_case09() { std::string err; CHECK(compile_case("09_gvl_external", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_globals == 1); - CHECK(v.data_len() == 8 + 32); // 8 字节定宽槽 + SHA-256 尾 + CHECK(v.n_globals() == 1); + CHECK(v.data_len() == 8); // 8 字节定宽槽(不含 SHA 尾) CHECK(v.data_bytes()[0] == 5); // G1 初值 5(小端低位) CHECK(v.data_bytes()[1] == 0); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 9); // r8 起:仅局部 x CHECK(r.code_len == 2); @@ -221,11 +221,11 @@ static bool test_io_ops() { "END_PROGRAM\n"; CHECK(compile_src("io", io_extra, globals_st, main_st, &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_globals == 2); + CHECK(v.n_globals() == 2); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 10); // r8 起:q + 1 临时 char buf[64]; @@ -258,10 +258,10 @@ static bool test_layout() { "END_PROGRAM\n"; CHECK(compile_src("layout", "", globals_st, main_st, &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_globals == 3); - CHECK(v.data_len() == 24 + 32); // 3 槽 × 8 字节定宽 + SHA-256 尾 + CHECK(v.n_globals() == 3); + CHECK(v.data_len() == 24); // 3 槽 × 8 字节定宽 CHECK(v.data_bytes()[0] == 0); // B 无初值 CHECK(v.data_bytes()[8] == 0x2C && v.data_bytes()[9] == 0x01); // I = 300(槽 1) CHECK(v.data_bytes()[16] == 10); // T = 10ms(槽 2) @@ -299,9 +299,9 @@ static bool test_case03() { std::string err; CHECK(compile_case("03_short_circuit", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 12); // r8 起:a b x + 1 临时 CHECK(r.code_len == 9); @@ -342,9 +342,9 @@ static bool test_not() { "END_PROGRAM\n"; CHECK(compile_src("not", "", "", main_st, &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 11); // r8 起:a x + 1 临时 CHECK(r.code_len == 3); @@ -367,9 +367,9 @@ static bool test_case04() { std::string err; CHECK(compile_case("04_if_elsif_else", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 13); // r8 起:sel out + 3 临时 CHECK(r.code_len == 14); @@ -417,9 +417,9 @@ static bool test_case05() { std::string err; CHECK(compile_case("05_while_normal", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 12); // r8 起:n + 3 临时(条件结果 r9 + 两操作数) CHECK(r.code_len == 10); @@ -458,9 +458,9 @@ static bool test_case07() { std::string err; CHECK(compile_case("07_int_arith", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 14); // r8 起:a b c eq + 2 临时 char buf[64]; @@ -485,15 +485,15 @@ static bool test_case08() { std::string err; CHECK(compile_case("08_time_literal", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_consts == 2); - const isa::ConstEntry c0 = v.const_entry(0); - CHECK(c0.tag == isa::types::Time && c0.value == 10); // T#10ms - const isa::ConstEntry c1 = v.const_entry(1); - CHECK(c1.tag == isa::types::Time && c1.value == 1250); // T#1s250ms + CHECK(v.n_consts() == 2); + const compiler::ConstEntry c0 = v.const_entry(0); + CHECK(c0.tag == 2 && c0.value == 10); // T#10ms + const compiler::ConstEntry c1 = v.const_entry(1); + CHECK(c1.tag == 2 && c1.value == 1250); // T#1s250ms - const isa::FuncRow r = v.func_row(0); + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 10); // r8 起 char buf[64]; const uint32_t* ins = reinterpret_cast(v.code_bytes()); @@ -511,15 +511,15 @@ static bool test_case13() { std::string err; CHECK(compile_case("13_function_call", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_funcs == 2); - CHECK(v.header().entry_fn_id == 1); // MAIN 是第二个 POU + CHECK(v.n_funcs() == 2); + CHECK(v.entry_fn_id() == 1); // MAIN 是第二个 POU char buf[64]; const uint8_t* base = v.code_bytes(); // Add(fn_id 0):结果 r0、输入 r1(a) r2(b)、临时 r8/r9 - const isa::FuncRow fa = v.func_row(0); + const compiler::StbView::FuncRow fa = v.func_row(0); CHECK(fa.nregs == 10); CHECK(fa.code_len == 4); const uint32_t* ia = reinterpret_cast(base); @@ -533,7 +533,7 @@ static bool test_case13() { CHECK(std::strcmp(buf, "RET") == 0); // MAIN(fn_id 1):x 在 r8,实参临时 r9/r10 - const isa::FuncRow fm = v.func_row(1); + const compiler::StbView::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 11); CHECK(fm.code_len == 7); const uint32_t* im = reinterpret_cast(base) + fm.code_offset / 4; @@ -561,13 +561,13 @@ static bool test_case15() { std::string err; CHECK(compile_case("15_fb_instance", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_funcs == 2); // FB 占位 + MAIN - CHECK(v.header().entry_fn_id == 1); - CHECK(v.data_len() == 24 + 32); // 实例 3 槽 × 8 字节定宽 + SHA-256 尾 + CHECK(v.n_funcs() == 2); // FB 占位 + MAIN + CHECK(v.entry_fn_id() == 1); + CHECK(v.data_len() == 24); // 实例 3 槽 × 8 字节定宽 - const isa::FuncRow fm = v.func_row(1); + const compiler::StbView::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 12); CHECK(fm.code_len == 12); const uint32_t* im = reinterpret_cast(v.code_bytes()) + fm.code_offset / 4; @@ -601,10 +601,10 @@ static bool test_case17() { std::string err; CHECK(compile_case("17_ton", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.data_len() == 32 + 32); // in@0 pt@8 q@16 et@24 + SHA-256 尾 - const isa::FuncRow r = v.func_row(0); + CHECK(v.data_len() == 32); // in@0 pt@8 q@16 et@24 + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 11); const uint32_t* ins = reinterpret_cast(v.code_bytes()); char buf[64]; @@ -630,10 +630,10 @@ static bool test_case18() { std::string err; CHECK(compile_case("18_tof_ctu", &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.data_len() == 72 + 32); // tf 4 槽 + c 5 槽 = 9 槽 × 8 + SHA-256 尾 - const isa::FuncRow r = v.func_row(0); + CHECK(v.data_len() == 72); // tf 4 槽 + c 5 槽 = 9 槽 × 8 + const compiler::StbView::FuncRow r = v.func_row(0); CHECK(r.nregs == 14); const uint32_t* ins = reinterpret_cast(v.code_bytes()); char buf[64]; @@ -670,14 +670,14 @@ static bool test_line1() { CHECK(load_cfg(&cfg)); CHECK(codegen_project(p, units, link, cfg, &img, &err)); - const isa::ImageView v = isa::ImageView::from(img); + const compiler::StbView v = compiler::StbView::from(img); CHECK(v.ok()); - CHECK(v.header().n_globals == 4); - CHECK(v.header().n_funcs == 2); - CHECK(v.header().entry_fn_id == 1); - CHECK(v.data_len() == 56 + 32); // 7 槽 × 8 字节定宽 + SHA-256 尾 + CHECK(v.n_globals() == 4); + CHECK(v.n_funcs() == 2); + CHECK(v.entry_fn_id() == 1); + CHECK(v.data_len() == 56); // 7 槽 × 8 字节定宽 - const isa::FuncRow fm = v.func_row(1); + const compiler::StbView::FuncRow fm = v.func_row(1); CHECK(fm.nregs == 13); CHECK(fm.code_len == 17); const uint32_t* im = reinterpret_cast(v.code_bytes()) + fm.code_offset / 4; diff --git a/tests/src/isa_test.cpp b/tests/src/isa_test.cpp index 6588332..5583c14 100644 --- a/tests/src/isa_test.cpp +++ b/tests/src/isa_test.cpp @@ -12,7 +12,6 @@ #include #include "isa/Encode.h" -#include "isa/Image.h" #include "isa/Instr.h" #include "isa/Op.h" #include "isa/Types.h" @@ -127,114 +126,11 @@ static bool test_encode() { return true; } -// ---- 5. FNV-1a 64 ---- - -static bool test_fnv() { - using namespace isa; - CHECK(fnv1a64(0, 0) == kFnvBasis); - const uint8_t a1[] = {'a'}; - CHECK(fnv1a64(a1, 1) == 0xaf63dc4c8601ec8cull); - return true; -} - -// ---- 6. 映像:write_ret_main → ImageView ---- - -static bool test_image() { - using namespace isa; - const uint64_t hash = 0x123456789abcdef0ull; - std::vector img = write_ret_main(100000, 10, hash); - - ImageView v = ImageView::from(img); - CHECK(v.ok()); - CHECK(v.error().empty()); - CHECK(v.header().cycle_limit == 100000); - CHECK(v.header().dt_ms == 10); - CHECK(v.header().project_hash == hash); - CHECK(v.header().entry_fn_id == 0); - CHECK(v.header().n_funcs == 1); - CHECK(v.header().n_globals == 0); - CHECK(v.header().n_i == 0); - CHECK(v.header().n_q == 0); - CHECK(v.header().n_m == 0); - CHECK(v.header().offset_const == kHeaderSize); - CHECK(v.header().offset_data == kHeaderSize + kFuncRowSize + 4); - CHECK(v.code_len() == 4); - CHECK(v.data_len() == 0); - - const FuncRow r = v.func_row(0); - CHECK(r.nregs == 0); - CHECK(r.code_offset == 0); - CHECK(r.code_len == 1); - - char buf[32]; - disasm(v.code_bytes()[0], buf, sizeof buf); - CHECK(std::strcmp(buf, "RET") == 0); - - // 损坏映像必须拒绝 - std::vector bad = img; - bad[0] = 'X'; - CHECK(!ImageView::from(bad).ok()); - std::vector bad2 = img; - bad2[60] = 255; // offset_code 越界 - CHECK(!ImageView::from(bad2).ok()); - std::vector bad3 = img; - bad3[48] = 2; // n_funcs=2 但表只有 1 行 - CHECK(!ImageView::from(bad3).ok()); - std::vector bad4 = img; - bad4[24] = 1; // entry_fn_id 越界 - CHECK(!ImageView::from(bad4).ok()); - std::vector short_img = img; - short_img.resize(kHeaderSize - 1); - CHECK(!ImageView::from(short_img).ok()); - return true; -} - -// ---- 7. .stb 文件与 sidecar ---- - -static bool test_files() { - using namespace isa; - std::vector img = write_ret_main(100000, 10, 0); - - const char* stb_path = "isa_test_tmp.stb"; - std::string err; - CHECK(write_stb_file(stb_path, img, &err)); - std::vector back; - CHECK(read_stb_file(stb_path, &back, &err)); - CHECK(back == img); - CHECK(ImageView::from(back).ok()); - std::remove(stb_path); - - std::vector bs; - IoBinding b1; - b1.var = "I0_0"; b1.slot = 1; b1.channel = 0; b1.bit = 0; b1.is_input = true; - IoBinding b2; - b2.var = "Q0_0"; b2.slot = 3; b2.channel = 0; b2.bit = 0; b2.is_input = false; - bs.push_back(b1); - bs.push_back(b2); - - const std::string sc = make_sidecar(bs); - CHECK(sc.find("[[io.input]]") == 0); - CHECK(sc.find("var = \"I0_0\"") != std::string::npos); - CHECK(sc.find("slot = 1") != std::string::npos); - CHECK(sc.find("channel = 0") != std::string::npos); - CHECK(sc.find("[[io.output]]") != std::string::npos); - CHECK(sc.find("var = \"Q0_0\"") != std::string::npos); - CHECK(sc.find("slot = 3") != std::string::npos); - - const char* sc_path = "isa_test_tmp.runtime.toml"; - CHECK(write_sidecar_file(sc_path, bs, &err)); - std::remove(sc_path); - return true; -} - int main() { if (!test_sat()) return 1; if (!test_op()) return 1; if (!test_instr()) return 1; if (!test_encode()) return 1; - if (!test_fnv()) return 1; - if (!test_image()) return 1; - if (!test_files()) return 1; std::printf("isa_test: %d checks passed\n", g_checks); return 0; } diff --git a/tests/src/vm_test.cpp b/tests/src/vm_test.cpp index a968942..8a9f77e 100644 --- a/tests/src/vm_test.cpp +++ b/tests/src/vm_test.cpp @@ -15,7 +15,6 @@ #include "compiler/Project.h" #include "compiler/Typecheck.h" #include "isa/Encode.h" -#include "isa/Image.h" #include "isa/Instr.h" #include "isa/Op.h" #include "vm/Machine.h" @@ -93,7 +92,7 @@ struct HFunc { uint32_t nregs = 8; }; -static std::vector hand_image(const std::vector& consts, +static std::vector hand_image(const std::vector& consts, const std::vector& funcs, uint32_t entry = 0, uint32_t cycle_limit = 100000, @@ -102,11 +101,11 @@ static std::vector hand_image(const std::vector& const for (const HFunc& f : funcs) { code_total += f.code.size() * 4; } - const uint32_t off_const = 72; + const uint32_t off_const = 104; const uint32_t off_funcs = off_const + static_cast(consts.size()) * 12; const uint32_t off_code = off_funcs + static_cast(funcs.size()) * 12; const uint32_t off_data = off_code + static_cast(code_total); - const uint32_t off_end = off_data + static_cast(data.size()); + const uint32_t off_end = off_data + static_cast(data.size()) + 32; // SHA 尾占位 std::vector b(off_end, 0); auto put32 = [&](size_t o, uint32_t v) { @@ -120,8 +119,8 @@ static std::vector hand_image(const std::vector& const b[o + i] = static_cast((v >> (8 * i)) & 0xFFu); } }; - put32(0, isa::kMagic); - put32(4, isa::kVersion); + put32(0, 0x43545353u); + put32(4, 1u); put32(8, cycle_limit); put32(12, 10); // dt_ms put32(24, entry); @@ -163,10 +162,10 @@ static std::vector hand_image(const std::vector& const static bool test_hand_scalar() { // r8 := 5;r9 := r8 + 3;槽 0 ← r9;槽 1 → r10;JT 跳过一条 - const std::vector consts = { - {isa::types::Int, 5}, - {isa::types::Int, 3}, - {isa::types::Int, 1}, + const std::vector consts = { + {1, 5}, + {1, 3}, + {1, 1}, }; const std::vector funcs = {{ { @@ -201,9 +200,9 @@ static bool test_hand_scalar() { static bool test_hand_call() { // fn0(入口):r8=5、r2=7、实参 r1←r8;CALL 1;结果 r0 → r8 // fn1:r0 = r1 + r2(r1/r2 来自调用约定区复制) - const std::vector consts = { - {isa::types::Int, 5}, - {isa::types::Int, 7}, + const std::vector consts = { + {1, 5}, + {1, 7}, }; const std::vector funcs = { {{ diff --git a/vm/CMakeLists.txt b/vm/CMakeLists.txt index 6f39576..c7a06ae 100644 --- a/vm/CMakeLists.txt +++ b/vm/CMakeLists.txt @@ -7,7 +7,8 @@ project(VM DESCRIPTION "寄存器虚拟机") add_library(vm STATIC - ./src/Machine.cpp) + ./src/Machine.cpp + ./src/Image.cpp) target_include_directories(vm PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(vm PUBLIC isa) diff --git a/vm/include/vm/Image.h b/vm/include/vm/Image.h new file mode 100644 index 0000000..d899354 --- /dev/null +++ b/vm/include/vm/Image.h @@ -0,0 +1,78 @@ +/** + * @file Image.h + * @brief vm 自带的 .stb 只读视图(执行器侧;compiler 各有实现) + * @author + * @date 2026-08-21 + * + * @details 格式契约见 Doc/isa/指令与映像.md(12.13 修订:头 104 = 72 + 型号标识[32], + * 文件尾 SHA-256[32])。步骤 7:解析头与段;步骤 8:补型号匹配与 SHA-256 校验。 + */ + +#pragma once + +#include +#include +#include +#include + +namespace vm { + + // 映像头(字段与 Doc/isa/指令与映像.md 一致) + struct ImageHeader { + uint32_t cycle_limit = 0; + uint32_t dt_ms = 0; + uint64_t project_hash = 0; + uint32_t entry_fn_id = 0; + uint32_t n_globals = 0; + uint32_t n_i = 0; + uint32_t n_q = 0; + uint32_t n_m = 0; + uint32_t n_consts = 0; + uint32_t n_funcs = 0; + uint32_t offset_const = 0; + uint32_t offset_funcs = 0; + uint32_t offset_code = 0; + uint32_t offset_fb = 0; + uint32_t offset_data = 0; + }; + + struct FuncRow { + uint32_t nregs = 0; + uint32_t code_offset = 0; // 相对字节码段起点(字节) + uint32_t code_len = 0; // 指令条数 + }; + + struct ConstEntry { + uint32_t tag = 0; + uint64_t value = 0; + }; + + // 只读视图:校验过的映像(指向外部缓冲) + class Image { + public: + static Image from(const uint8_t* buf, size_t len); + static Image from(const std::vector& buf); + + bool ok() const { return ok_; } + const std::string& error() const { return err_; } + const ImageHeader& header() const { return hdr_; } + + FuncRow func_row(size_t i) const; + ConstEntry const_entry(size_t i) const; + + const uint8_t* code_bytes() const; // 字节码段起点 + const uint8_t* data_bytes() const; // 数据段起点 + size_t data_len() const; // 字节数 + + public: + // 默认构造为无效态(ok() == false) + Image() : buf_(nullptr), len_(0) {} + + private: + const uint8_t* buf_; + size_t len_; + bool ok_ = false; + std::string err_; + ImageHeader hdr_; + }; +} diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h index 9a63669..a3f4ba5 100644 --- a/vm/include/vm/Machine.h +++ b/vm/include/vm/Machine.h @@ -19,7 +19,7 @@ #include #include -#include "isa/Image.h" +#include "vm/Image.h" #include "isa/Instr.h" namespace vm { @@ -67,7 +67,7 @@ namespace vm { bool ended() const; /** @brief 映像头(dt_ms / cycle_limit 等) */ - const isa::ImageHeader& header() const; + const vm::ImageHeader& header() const; /** @brief 当前指令下标(相对当前函数字节码段) */ uint32_t pc() const; @@ -105,7 +105,7 @@ namespace vm { uint32_t ret_fn_id = 0; // 返回目标函数 }; - isa::ImageView image_; // 映像只读视图 + vm::Image image_; // 映像只读视图(vm 自实现) std::vector data_; // 数据区工作副本(8 字节定宽槽) std::vector frames_; // 调用栈([0] = MAIN,跨周期保留) // 边沿检测上次输入(每槽 2 字节:[slot*2] 第一边沿、[slot*2+1] 第二边沿; diff --git a/vm/src/Image.cpp b/vm/src/Image.cpp new file mode 100644 index 0000000..54bcd4c --- /dev/null +++ b/vm/src/Image.cpp @@ -0,0 +1,151 @@ +/** + * @file Image.cpp + * @brief vm 自带的 .stb 只读视图(执行器侧) + * @author + * @date 2026-08-21 + */ + +#include "vm/Image.h" + +namespace vm { + +namespace { + + const uint32_t kMagic = 0x43545353u; // "STSC" 小端 + const uint32_t kVersion = 1; + const size_t kHeaderSize = 104; + const size_t kConstEntrySize = 12; + const size_t kFuncRowSize = 12; + const size_t kSha256Size = 32; // 文件尾(步骤 8 校验) + + uint32_t get_le32(const uint8_t* p) { + return static_cast(p[0]) + | (static_cast(p[1]) << 8) + | (static_cast(p[2]) << 16) + | (static_cast(p[3]) << 24); + } + + uint64_t get_le64(const uint8_t* p) { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) { + v |= static_cast(p[i]) << (8 * i); + } + return v; + } + +} // namespace + +Image Image::from(const uint8_t* buf, size_t len) { + Image v; + v.buf_ = buf; + v.len_ = len; + if (buf == nullptr) { + v.err_ = "null buffer"; + return v; + } + if (len < kHeaderSize) { + v.err_ = "image too short"; + return v; + } + if (get_le32(buf + 0) != kMagic) { + v.err_ = "bad magic"; + return v; + } + if (get_le32(buf + 4) != kVersion) { + v.err_ = "bad version"; + return v; + } + ImageHeader& h = v.hdr_; + h.cycle_limit = get_le32(buf + 8); + h.dt_ms = get_le32(buf + 12); + h.project_hash = get_le64(buf + 16); + h.entry_fn_id = get_le32(buf + 24); + h.n_globals = get_le32(buf + 28); + h.n_i = get_le32(buf + 32); + h.n_q = get_le32(buf + 36); + h.n_m = get_le32(buf + 40); + h.n_consts = get_le32(buf + 44); + h.n_funcs = get_le32(buf + 48); + h.offset_const = get_le32(buf + 52); + h.offset_funcs = get_le32(buf + 56); + h.offset_code = get_le32(buf + 60); + h.offset_fb = get_le32(buf + 64); + h.offset_data = get_le32(buf + 68); + + // 段校验(数据段之后是 SHA-256 文件尾;步骤 8 做校验,这里仅保证有尾) + if (len < static_cast(h.offset_data) + kSha256Size) { + v.err_ = "missing sha256 tail"; + return v; + } + const uint64_t offs[5] = {h.offset_const, h.offset_funcs, h.offset_code, h.offset_fb, + h.offset_data}; + for (int i = 0; i < 5; ++i) { + if (offs[i] < kHeaderSize || offs[i] > len - kSha256Size) { + v.err_ = "segment offset out of range"; + return v; + } + if (i > 0 && offs[i] < offs[i - 1]) { + v.err_ = "segment offsets not monotonic"; + return v; + } + } + if (offs[1] - offs[0] != static_cast(h.n_consts) * kConstEntrySize) { + v.err_ = "const table size mismatch"; + return v; + } + if (offs[2] - offs[1] != static_cast(h.n_funcs) * kFuncRowSize) { + v.err_ = "function table size mismatch"; + return v; + } + if ((h.offset_fb - h.offset_code) % 4 != 0) { + v.err_ = "code segment not 4-byte aligned"; + return v; + } + if (h.entry_fn_id >= h.n_funcs && h.n_funcs != 0) { + v.err_ = "entry fn_id out of range"; + return v; + } + v.ok_ = true; + v.err_.clear(); + return v; +} + +Image Image::from(const std::vector& buf) { + return from(buf.data(), buf.size()); +} + +FuncRow Image::func_row(size_t i) const { + FuncRow r; + if (ok_ && i < hdr_.n_funcs) { + const uint8_t* p = buf_ + hdr_.offset_funcs + i * kFuncRowSize; + r.nregs = get_le32(p + 0); + r.code_offset = get_le32(p + 4); + r.code_len = get_le32(p + 8); + } + return r; +} + +ConstEntry Image::const_entry(size_t i) const { + ConstEntry e; + if (ok_ && i < hdr_.n_consts) { + const uint8_t* p = buf_ + hdr_.offset_const + i * kConstEntrySize; + e.tag = get_le32(p); + e.value = get_le64(p + 4); + } + return e; +} + +const uint8_t* Image::code_bytes() const { + return ok_ ? buf_ + hdr_.offset_code : nullptr; +} + +const uint8_t* Image::data_bytes() const { + return ok_ ? buf_ + hdr_.offset_data : nullptr; +} + +size_t Image::data_len() const { + // 步骤 7:含 SHA 尾(步骤 8 收紧并校验) + return ok_ ? len_ - hdr_.offset_data : 0; +} + +} // namespace vm diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index 8493be0..2753b58 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -25,7 +25,7 @@ namespace vm { bool Machine::create(const std::vector& image, Machine* out, std::string* err) { - out->image_ = isa::ImageView::from(image); + out->image_ = vm::Image::from(image); if (!out->image_.ok()) { if (err) { *err = out->image_.error(); @@ -39,7 +39,7 @@ bool Machine::create(const std::vector& image, Machine* out, // MAIN 帧(跨周期保留) const uint32_t entry = out->image_.header().entry_fn_id; - const isa::FuncRow main = out->image_.func_row(entry); + const vm::FuncRow main = out->image_.func_row(entry); out->frames_.clear(); Frame f; f.fn_id = entry; @@ -79,7 +79,7 @@ bool Machine::step() { Fault Machine::fault() const { return fault_; } bool Machine::ended() const { return ended_; } -const isa::ImageHeader& Machine::header() const { return image_.header(); } +const vm::ImageHeader& Machine::header() const { return image_.header(); } uint32_t Machine::pc() const { return pc_; } uint32_t Machine::fn_id() const { return cur_frame().fn_id; } uint32_t Machine::cycle_count() const { return cycle_count_; } @@ -94,7 +94,7 @@ uint32_t Machine::nregs() const { } isa::Instr Machine::cur_instr() const { - const isa::FuncRow row = image_.func_row(cur_frame().fn_id); + const vm::FuncRow row = image_.func_row(cur_frame().fn_id); const uint8_t* base = image_.code_bytes() + row.code_offset; if (pc_ >= row.code_len) { return isa::pack(isa::Op::RET, 0, 0, 0); @@ -107,7 +107,7 @@ const Machine::Frame& Machine::cur_frame() const { return frames_.back(); } bool Machine::exec_one() { // ---- 取指 ---- - const isa::FuncRow row = image_.func_row(cur_frame().fn_id); + const vm::FuncRow row = image_.func_row(cur_frame().fn_id); if (pc_ >= row.code_len) { fault_ = Fault::BadOp; return false; @@ -165,7 +165,7 @@ bool Machine::exec_one() { fault_ = Fault::BadConst; return false; } - const isa::ConstEntry c = image_.const_entry(cid); + const vm::ConstEntry c = image_.const_entry(cid); if (c.tag == isa::types::Bool) { regs[rd] = c.value ? 1 : 0; } else if (c.tag == isa::types::Int) { @@ -310,7 +310,7 @@ Fault Machine::do_call(uint32_t fn_id) { if (frames_.size() >= 64) { return Fault::StackOverflow; } - const isa::FuncRow row = image_.func_row(fn_id); + const vm::FuncRow row = image_.func_row(fn_id); Frame f; f.fn_id = fn_id; f.regs.assign(row.nregs, 0); From b13585711a594a5fda5ff6e68b4b59ad30092fe8 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:29:33 +0800 Subject: [PATCH 59/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20C=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=208=EF=BC=9Avm=20=E8=AF=BB=E4=BE=A7=E5=9E=8B=E5=8F=B7?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=20+=20SHA-256=20=E6=A0=A1=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vm/Image.cpp:执行器侧 SHA-256 实现(与 compiler 各一份)、fill_model_id、 model_id()/model_matches()/sha_ok();data_len 不含 SHA 尾 - Machine::create:sha256 不匹配 → 'image sha256 mismatch';型号不匹配 → 'image model mismatch'(内建 kModelName=STATOR/kModelVersion=1),均拒绝 - vm_test:手拼映像填型号(修越界读 bug)+ compiler::sha256 填尾(95 断言); 新增篡改拒绝(sha256 mismatch)与型号不匹配(重算 SHA 后 model mismatch)用例 - 验证:line1 正常执行 Q=1;篡改一字节 → 执行器 'image sha256 mismatch' 拒绝;ctest 12/12 --- tests/src/vm_test.cpp | 70 +++++++++++++++++++ vm/include/vm/Image.h | 11 ++- vm/src/Image.cpp | 155 +++++++++++++++++++++++++++++++++++++++++- vm/src/Machine.cpp | 14 ++++ 4 files changed, 246 insertions(+), 4 deletions(-) diff --git a/tests/src/vm_test.cpp b/tests/src/vm_test.cpp index 8a9f77e..d5df419 100644 --- a/tests/src/vm_test.cpp +++ b/tests/src/vm_test.cpp @@ -13,6 +13,7 @@ #include "compiler/Codegen.h" #include "compiler/Linker.h" #include "compiler/Project.h" +#include "compiler/Stb.h" #include "compiler/Typecheck.h" #include "isa/Encode.h" #include "isa/Instr.h" @@ -155,6 +156,16 @@ static std::vector hand_image(const std::vector& consts for (size_t i = 0; i < data.size(); ++i) { b[off_data + i] = data[i]; } + // 型号标识(STATOR1)+ 真实 SHA-256 尾(用 compiler 实现填) + const char* mid = "STATOR1"; + for (size_t i = 0; i < 32; ++i) { + b[72 + i] = i < 7 ? static_cast(mid[i]) : 0; + } + uint8_t digest[32]; + compiler::sha256(b.data(), b.size() - 32, digest); + for (int i = 0; i < 32; ++i) { + b[b.size() - 32 + i] = digest[i]; + } return b; } @@ -428,6 +439,64 @@ static bool test_bad_const() { return true; } +// ---- 10. 12.13 校验:SHA 篡改拒绝 + 型号不匹配拒绝 ---- + +static bool test_verify() { + std::string err; + vm::Machine m; + + // 正常映像可跑 + CHECK(make_machine("tests/cases/01_empty_main", &m, &err)); + CHECK(m.run_cycle() == vm::Fault::None); + + // SHA 篡改:改一字节 → create 拒绝 + { + const std::string toml = std::string(REPO_ROOT) + "/tests/cases/01_empty_main/project.toml"; + compiler::Project p; + CHECK(compiler::parse_project(toml, &p, &err)); + std::vector units; + for (const std::string& f : compiler::compile_files(p)) { + compiler::SourceUnit u; + CHECK(compiler::load_unit(p.base_dir + "/" + f, &u, &err)); + units.push_back(std::move(u)); + } + compiler::LinkResult link; + CHECK(compiler::link_project(p, units, &link, &err)); + compiler::MachineConfig cfg; + CHECK(cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &err)); + std::vector img; + CHECK(compiler::codegen_project(p, units, link, cfg, &img, &err)); + img[100] ^= 0x01; // 篡改代码段一字节 + vm::Machine bad; + CHECK(!vm::Machine::create(img, &bad, &err)); + CHECK(err.find("sha256 mismatch") != std::string::npos); + } + + // 型号不匹配:手拼映像改型号(重算 SHA,仅型号不一致)→ create 拒绝 + { + const std::vector consts; + const std::vector funcs = {{ + { + isa::enc_ret(), + }, + 8, + }}; + std::vector img = hand_image(consts, funcs); + for (int i = 0; i < 5; ++i) { + img[72 + i] = "OTHER"[i]; + } + uint8_t digest[32]; + compiler::sha256(img.data(), img.size() - 32, digest); + for (int i = 0; i < 32; ++i) { + img[img.size() - 32 + i] = digest[i]; + } + vm::Machine bad; + CHECK(!vm::Machine::create(img, &bad, &err)); + CHECK(err.find("model mismatch") != std::string::npos); + } + return true; +} + int main() { if (!test_hand_scalar()) return 1; if (!test_hand_call()) return 1; @@ -438,6 +507,7 @@ int main() { if (!test_determinism()) return 1; if (!test_step()) return 1; if (!test_bad_const()) return 1; + if (!test_verify()) return 1; std::printf("vm_test: %d checks passed\n", g_checks); return 0; } diff --git a/vm/include/vm/Image.h b/vm/include/vm/Image.h index d899354..e3dac36 100644 --- a/vm/include/vm/Image.h +++ b/vm/include/vm/Image.h @@ -17,6 +17,10 @@ namespace vm { + // 执行器内建支持型号(与 machine.toml [meta] 对齐;.stb 型号不匹配直接拒绝) + static const char* const kModelName = "STATOR"; + static const uint32_t kModelVersion = 1; + // 映像头(字段与 Doc/isa/指令与映像.md 一致) struct ImageHeader { uint32_t cycle_limit = 0; @@ -62,7 +66,12 @@ namespace vm { const uint8_t* code_bytes() const; // 字节码段起点 const uint8_t* data_bytes() const; // 数据段起点 - size_t data_len() const; // 字节数 + size_t data_len() const; // 字节数(不含 SHA 尾) + + // 12.13:型号标识与 SHA-256 校验 + std::string model_id() const; + bool model_matches(const std::string& name, uint32_t version) const; + bool sha_ok() const; // 文件尾 32 字节 SHA-256 校验 public: // 默认构造为无效态(ok() == false) diff --git a/vm/src/Image.cpp b/vm/src/Image.cpp index 54bcd4c..d826c00 100644 --- a/vm/src/Image.cpp +++ b/vm/src/Image.cpp @@ -7,6 +7,9 @@ #include "vm/Image.h" +#include +#include + namespace vm { namespace { @@ -16,7 +19,8 @@ namespace { const size_t kHeaderSize = 104; const size_t kConstEntrySize = 12; const size_t kFuncRowSize = 12; - const size_t kSha256Size = 32; // 文件尾(步骤 8 校验) + const size_t kSha256Size = 32; + const size_t kModelIdSize = 32; uint32_t get_le32(const uint8_t* p) { return static_cast(p[0]) @@ -33,6 +37,124 @@ namespace { return v; } + // ---- SHA-256(执行器侧实现,与 compiler 各一份)---- + + const uint32_t kShaK[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + + inline uint32_t rotr(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } + + struct Sha256 { + uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + uint64_t total = 0; + uint8_t block[64]; + size_t block_len = 0; + + void update(const uint8_t* data, size_t len) { + total += len; + while (len > 0) { + const size_t n = (block_len < 64) ? (64 - block_len) : 0; + const size_t take = len < n ? len : n; + if (take == 0) { + break; + } + for (size_t i = 0; i < take; ++i) { + block[block_len + i] = data[i]; + } + block_len += take; + data += take; + len -= take; + if (block_len == 64) { + process(); + block_len = 0; + } + } + } + + void process() { + uint32_t w[64]; + for (int i = 0; i < 16; ++i) { + w[i] = get_be32(block + i * 4); + } + for (int i = 16; i < 64; ++i) { + const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3]; + uint32_t e = h[4], f = h[5], g = h[6], hh = h[7]; + for (int i = 0; i < 64; ++i) { + const uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); + const uint32_t ch = (e & f) ^ (~e & g); + const uint32_t t1 = hh + s1 + ch + kShaK[i] + w[i]; + const uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); + const uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + const uint32_t t2 = s0 + maj; + hh = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + } + + void final(uint8_t out[32]) { + const uint64_t bitlen = total * 8; + const uint8_t pad = 0x80; + update(&pad, 1); + const uint8_t zeros[64] = {0}; + while (block_len != 56) { + const size_t n = (block_len < 56) ? (56 - block_len) : (64 - block_len); + update(zeros, n); + } + for (int i = 0; i < 8; ++i) { + const uint8_t b2[1] = {static_cast((bitlen >> (56 - 8 * i)) & 0xFF)}; + update(b2, 1); + } + for (int i = 0; i < 8; ++i) { + out[i * 4 + 0] = static_cast((h[i] >> 24) & 0xFF); + out[i * 4 + 1] = static_cast((h[i] >> 16) & 0xFF); + out[i * 4 + 2] = static_cast((h[i] >> 8) & 0xFF); + out[i * 4 + 3] = static_cast(h[i] & 0xFF); + } + } + + static uint32_t get_be32(const uint8_t* p) { + return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | static_cast(p[3]); + } + }; + + void sha256(const uint8_t* data, size_t len, uint8_t out[32]) { + Sha256 s; + s.update(data, len); + s.final(out); + } + + void fill_model_id(const std::string& name, uint32_t version, char out[32]) { + const std::string id = name + std::to_string(version); + for (size_t i = 0; i < 32; ++i) { + out[i] = i < id.size() ? id[i] : '\0'; + } + } + } // namespace Image Image::from(const uint8_t* buf, size_t len) { @@ -144,8 +266,35 @@ const uint8_t* Image::data_bytes() const { } size_t Image::data_len() const { - // 步骤 7:含 SHA 尾(步骤 8 收紧并校验) - return ok_ ? len_ - hdr_.offset_data : 0; + return ok_ ? (len_ - kSha256Size) - hdr_.offset_data : 0; +} + +std::string Image::model_id() const { + if (!ok_) { + return ""; + } + std::string s(reinterpret_cast(buf_ + 72), kModelIdSize); + const size_t z = s.find('\0'); + if (z != std::string::npos) { + s.resize(z); + } + return s; +} + +bool Image::model_matches(const std::string& name, uint32_t version) const { + char want[kModelIdSize]; + fill_model_id(name, version, want); + return std::memcmp(buf_ + 72, want, kModelIdSize) == 0; +} + +bool Image::sha_ok() const { + if (!ok_) { + return false; + } + const size_t content_len = len_ - kSha256Size; + uint8_t digest[32]; + sha256(buf_, content_len, digest); + return std::memcmp(buf_ + content_len, digest, 32) == 0; } } // namespace vm diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index 2753b58..ab869cf 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -32,6 +32,20 @@ bool Machine::create(const std::vector& image, Machine* out, } return false; } + // 12.13:型号匹配与 SHA-256 校验(不匹配/篡改直接拒绝) + if (!out->image_.sha_ok()) { + if (err) { + *err = "image sha256 mismatch"; + } + return false; + } + if (!out->image_.model_matches(kModelName, kModelVersion)) { + if (err) { + *err = "image model mismatch: '" + out->image_.model_id() + + "' (expect " + kModelName + std::to_string(kModelVersion) + ")"; + } + return false; + } // 数据区工作副本(8 字节定宽槽) out->data_.assign(out->image_.data_bytes(), out->image_.data_bytes() + out->image_.data_len()); From a5f49caa94cab0a00c2556eccb6f62649b056d2e Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:30:44 +0800 Subject: [PATCH 60/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20D=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=209=EF=BC=9ACLI=20=E6=8E=A5=E7=BA=BF=E6=94=B6?= =?UTF-8?q?=E5=B0=BE=EF=BC=88--machine=20=E4=BB=85=E7=BC=96=E8=AF=91/?= =?UTF-8?q?=E5=8F=8D=E6=B1=87=E7=BC=96=E5=BF=85=E5=A1=AB=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 打印模式(无 -o)不再要求 --machine(恢复 12.3 行为) - 编译路径与 --disasm 必填 --machine,加载/校验失败报错退出 - 验证:打印/编译/disasm/缺参四种模式行为正确;ctest 12/12 --- compiler/src/main.cpp | 49 +++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index 27d5169..05ed636 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -105,10 +105,13 @@ int main(int argc, char** argv) { // 参数收集 std::string machine_path; + std::string out_path; bool disasm_mode = false; for (int i = 2; i + 1 < argc; ++i) { if (std::strcmp(argv[i], "--machine") == 0) { machine_path = argv[i + 1]; + } else if (std::strcmp(argv[i], "-o") == 0) { + out_path = argv[i + 1]; } } for (int i = 1; i < argc; ++i) { @@ -117,38 +120,32 @@ int main(int argc, char** argv) { } } - // 机器定义(编译与反汇编都需要;加载/校验失败报错退出) - if (machine_path.empty()) { - std::fprintf(stderr, "error: missing --machine \n"); - usage(); - return 1; - } - compiler::MachineConfig cfg; - std::string err; - if (!cfg.load(machine_path, &err)) { - std::fprintf(stderr, "error: %s\n", err.c_str()); - return 1; - } - + // --disasm:需要机器定义(反汇编按 machine.toml 的 format 输出) if (disasm_mode) { + if (machine_path.empty()) { + std::fprintf(stderr, "error: missing --machine \n"); + usage(); + return 1; + } + compiler::MachineConfig cfg; + std::string err; + if (!cfg.load(machine_path, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } return dump_image(argv[1], cfg); } const std::string toml_path = argv[1]; - std::string out_path; - for (int i = 2; i + 1 < argc; ++i) { - if (std::strcmp(argv[i], "-o") == 0) { - out_path = argv[i + 1]; - } - } - compiler::Project proj; + std::string err; if (!compiler::parse_project(toml_path, &proj, &err)) { std::fprintf(stderr, "error: %s\n", err.c_str()); return 1; } const std::vector files = compiler::compile_files(proj); + // 打印模式(无 -o):不需要机器定义 if (out_path.empty()) { uint64_t hash = 0; if (!compiler::compute_project_hash(proj, &hash, &err)) { @@ -164,6 +161,18 @@ int main(int argc, char** argv) { return 0; } + // 编译路径:机器定义必填(编码/类型/FB 布局来自 machine.toml) + if (machine_path.empty()) { + std::fprintf(stderr, "error: missing --machine \n"); + usage(); + return 1; + } + compiler::MachineConfig cfg; + if (!cfg.load(machine_path, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + // 完整管线:读 .st → 链接 → 类型检查 → 寄存器码 std::vector units; for (const std::string& f : files) { From 40bfd175c4722ecb817708d6bedd3be61a59e5dd Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:33:14 +0800 Subject: [PATCH 61/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20D=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=2010=EF=BC=9A=E6=B5=8B=E8=AF=95=E5=9B=BA=E5=8C=96?= =?UTF-8?q?=EF=BC=88CLI=20=E7=BA=A7=20+=20=E5=86=99=E4=BE=A7=E8=87=AA?= =?UTF-8?q?=E6=A3=80=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - codegen_test line1:compiler::StbView 自检(model_matches STATOR1 + sha_ok), 篡改一字节 → sha 校验失败 - CTest 新增:cli_compile(--machine 编译退出 0)、cli_missing_machine(缺参 WILL_FAIL) - 冻结哈希确认:工程哈希只依赖源文件内容(0xc3fe4ae74ad45900 不变,compiler_test 通过) - 全量 ctest 14/14 全绿(新增 2 个 CLI 用例) --- tests/CMakeLists.txt | 10 ++++++++++ tests/src/codegen_test.cpp | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 966d654..e05b6e8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,16 @@ add_test(NAME stcompiler_version add_test(NAME bytecode_executor_version COMMAND BytecodeExecutor) +# CLI 级:编译需 --machine;缺参必须失败(WILL_FAIL) +add_test(NAME cli_compile + COMMAND STCompiler ${CMAKE_SOURCE_DIR}/tests/cases/01_empty_main/project.toml + -o ${CMAKE_CURRENT_BINARY_DIR}/cli_case01.stb + --machine ${CMAKE_SOURCE_DIR}/compiler/machine.toml) +add_test(NAME cli_missing_machine + COMMAND STCompiler ${CMAKE_SOURCE_DIR}/tests/cases/01_empty_main/project.toml + -o ${CMAKE_CURRENT_BINARY_DIR}/cli_nope.stb) +set_tests_properties(cli_missing_machine PROPERTIES WILL_FAIL TRUE) + # isa 合同测试:链接 isa 库,断言覆盖饱和/编解码/disasm/哈希/映像/sidecar add_executable(isa_test ./src/isa_test.cpp) diff --git a/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp index 4f28b18..2146d57 100644 --- a/tests/src/codegen_test.cpp +++ b/tests/src/codegen_test.cpp @@ -698,6 +698,16 @@ static bool test_line1() { CHECK(std::strcmp(buf, "STORE_Q r8, 3") == 0); // Q0_0(io.output) isa::disasm(im[16], buf, sizeof buf); CHECK(std::strcmp(buf, "RET") == 0); + + // 12.13 写侧自检:型号标识 + SHA-256(compiler::StbView) + const compiler::StbView self = compiler::StbView::from(img); + CHECK(self.ok()); + CHECK(self.model_matches("STATOR", 1)); + CHECK(self.sha_ok()); + std::vector tampered = img; + tampered[100] ^= 0x01; // 篡改代码段一字节 + const compiler::StbView bad = compiler::StbView::from(tampered); + CHECK(bad.ok() && !bad.sha_ok()); // 结构可解析但 SHA 校验失败 return true; } From 0ebf193ff9e6e34c04613d3260a0515794718bc4 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:35:20 +0800 Subject: [PATCH 62/69] =?UTF-8?q?=E9=98=B6=E6=AE=B5=20D=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=2011=EF=BC=9A=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5=20+?= =?UTF-8?q?=20=E6=8F=90=E4=BA=A4=EF=BC=88=E6=8C=87=E4=BB=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=85=A8=E6=B5=81=E7=A8=8B=E6=94=B6=E5=B0=BE=EF=BC=89?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stb文件格式.md:288 字节真实拆解(头 104 含型号标识 STATOR1@0x48、函数表 0x68、 代码 0x80 fn0 RET=0x21、数据段 0xC8 7 槽、SHA-256 尾 0x100);工具验证与执行器关系更新 - STCompiler使用说明.md:--machine 参数(编译/disasm 必填、打印模式不需要)、 .stb 加固说明、288 字节示例、当前状态(可编译+可执行+配置驱动) - 执行器入口.md:读取时型号/SHA 校验拒绝 - ctest 14/14 全绿 --- Doc/compiler/STCompiler使用说明.md | 49 +++++++++++++---------- Doc/executor/执行器入口.md | 3 +- Doc/isa/stb文件格式.md | 64 +++++++++++++++++------------- 3 files changed, 66 insertions(+), 50 deletions(-) diff --git a/Doc/compiler/STCompiler使用说明.md b/Doc/compiler/STCompiler使用说明.md index 5f980c0..4a1c268 100644 --- a/Doc/compiler/STCompiler使用说明.md +++ b/Doc/compiler/STCompiler使用说明.md @@ -15,45 +15,52 @@ ctest --test-dir build/gcc-debug # 测试(当前 9 个用例) ## 用法 ```text -STCompiler [-o .stb] -STCompiler .stb --disasm +STCompiler [-o .stb] --machine +STCompiler .stb --disasm --machine ``` | 参数 | 作用 | |---|---| | `` | 工程文件(必填) | -| `-o .stb` | 编译并写出映像文件;**缺省时只打印文件集合与工程哈希**(12.3 阶段行为) | +| `-o .stb` | 编译并写出映像文件(+ sidecar);**缺省时只打印文件集合与工程哈希**(不需 `--machine`) | +| `--machine ` | 机器定义(指令 opcode / 类型 / FB 布局,见 `Doc/isa/指令配置.md`);**编译与 `--disasm` 必填**,加载/校验失败报错退出 | | `.stb --disasm` | **反汇编一个已编译的映像**(不重新编译,见下) | | `--help` | 打印帮助 | -退出码:`0` 成功;`1` 编译失败(错误信息打到 stderr)。 +退出码:`0` 成功;`1` 失败(错误信息打到 stderr)。 ## 反汇编(--disasm) -对已编译的 `.stb` 逐段查看:段摘要、函数表逐条指令(文件绝对偏移)、常量表、数据段 hex。 +对已编译的 `.stb` 逐段查看:段摘要(含型号与 SHA 校验)、函数表逐条指令(文件绝对偏移)、常量表、数据段 hex。 ```bash -$ STCompiler line1.stb --disasm -image: line1.stb (184 bytes, 2 functions, 4 globals, entry fn 1) - dt_ms=10 cycle_limit=100000 hash=0xc3fe4ae74ad45900 +$ STCompiler line1.stb --disasm --machine machine.toml +image: line1.stb (288 bytes, 2 functions, 4 globals, entry fn 1) + dt_ms=10 cycle_limit=100000 hash=0xc3fe4ae74ad45900 model=STATOR1 sha=ok functions: fn 0: nregs=8, offset=0, len=1 - 0x0060 RET + 0x0080 RET fn 1: nregs=13, offset=4, len=17 - 0x0064 LOAD_I r8, 1 - 0x0068 STORE_GLOBAL r8, 8 - 0x006c LOAD_GLOBAL r9, 2 + 0x0084 LOAD_I r8, 1 + 0x0088 STORE_GLOBAL r8, 4 ... -data (16 bytes): - 0x0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +data (56 bytes): + 0x0000: ... ``` 说明: - 指令偏移是**文件绝对字节偏移**,可直接对照 `xxd line1.stb` - 映像里**没有符号名**(函数表只有 fn_id,数据段无名字映射),因此按 `fn 0` / `fn 1`、槽号显示 +- `model=` 显示型号标识(`machine.toml [meta]` 生成);`sha=ok|BAD` 显示文件尾 SHA-256 校验结果 - 文件损坏或打不开 → `error: ...` 退出码 1 +## 编译产物(.stb 加固) + +- **型号标识[32]**:`[meta] name + version` 拼成(如 `"STATOR1"`),写进头偏移 72 +- **SHA-256 文件尾[32]**:对文件尾之前全部内容计算;compiler 写侧与 vm 读侧各实现一份 +- 执行器读取时型号不匹配 / SHA 不符 → 直接拒绝(详见 `Doc/isa/指令与映像.md` 12.13 修订) + ## 示例(line1) ```bash @@ -65,11 +72,11 @@ files: main.st hash: 0xc3fe4ae74ad45900 -$ STCompiler examples/line1/project.toml -o line1.stb -compiled: line1.stb (184 bytes, 2 functions, 4 globals) +$ STCompiler examples/line1/project.toml -o line1.stb --machine compiler/machine.toml +compiled: line1.stb (288 bytes, 2 functions, 4 globals) ``` -产物 `line1.stb`:魔数 `STSC` + 版本 1 的单一映像(指令见 `Doc/isa/指令与映像.md`)。 +产物:`line1.stb`(288 字节:头 104 含型号标识 + 各段 + SHA-256 文件尾)+ `line1.runtime.toml`(sidecar)。指令见 `Doc/isa/指令与映像.md`,结构拆解见 `Doc/isa/stb文件格式.md`。 ## 工程文件(project.toml) @@ -142,10 +149,10 @@ project.toml + .st - `examples/line1/`:最小闭环(MAIN + MotorStarter FB + I/O + 全局),给人看 - `tests/cases/01~20/`:20 个用例(每份 `project.toml` + `.st` + `EXPECTED.md`),编译结果按表 -- `tests/`:CTest(`isa_roundtrip` / `compiler_toml` / `lexer_tokens` / `parser_syntax` / `linker_links` / `typecheck_types` / `codegen_slice1`) +- `tests/`:CTest(`compiler_toml` / `lexer_tokens` / `parser_syntax` / `linker_links` / `typecheck_types` / `codegen_slice1` / `machine_config` / `vm_cycles` / `cases_all` / `cli_*`) ## 当前状态 -- ✅ 可编译:14 个正例用例全部产出 `.stb`;6 个负例按期望拒绝 -- ⏳ 执行:`BytecodeExecutor`(加载 `.stb` 跑扫描周期)在 12.9/12.10 -- ⏳ sidecar(`.runtime.toml`,I/O 绑定)在 12.10 随执行器一起接线 +- ✅ 可编译:14 个正例用例全部产出 `.stb`(含型号标识 + SHA-256);6 个负例按期望拒绝 +- ✅ 执行:`BytecodeExecutor` 加载 `.stb` 跑扫描周期(型号/SHA 校验、回放、单步) +- ✅ 配置驱动:指令 opcode / 类型元数据 / FB 布局来自 `compiler/machine.toml`(`--machine`) diff --git a/Doc/executor/执行器入口.md b/Doc/executor/执行器入口.md index 76de4af..bad9343 100644 --- a/Doc/executor/执行器入口.md +++ b/Doc/executor/执行器入口.md @@ -18,8 +18,9 @@ BytecodeExecutor .stb [--cycles N] [--replay ] [--step] ## 职责 - 加载 `.stb` 映像与 `.runtime.toml` sidecar(I/O 绑定 var → 槽号 → channel/bit) +- 读取时**校验型号标识与 SHA-256**:型号不匹配 / 文件被篡改 → 直接报错拒绝(执行器只接受与自身内建型号一致的映像) - 按 sidecar 采样 I、写回 Q(不创造变量) -- `dt_ms` / `cycle_limit` 从映像头取,推进 `TON` / `TOF` / `CTU` +- `dt_ms` / `cycle_limit` 从映像头取,推进内置 FB(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG) - 单步、看寄存器和映像、按录制的 `I` 回放 ## 边界 diff --git a/Doc/isa/stb文件格式.md b/Doc/isa/stb文件格式.md index 554046b..3f59a55 100644 --- a/Doc/isa/stb文件格式.md +++ b/Doc/isa/stb文件格式.md @@ -1,8 +1,6 @@ # .stb 映像文件结构 -编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(224 字节)做真实拆解。 - -> **12.13 修订**(编译器写侧已落地,vm 读侧待步骤 8):头新增型号标识[32](头 72 → 104 字节,段偏移基准同步后移),文件尾新增 SHA-256[32](对文件尾之前全部内容计算)。line1 产物现为 288 字节。本文拆解仍为旧格式数字,需在 vm 侧落地后统一重新生成。 +编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(288 字节,12.13 修订后)做真实拆解。 ## 概览 @@ -12,7 +10,7 @@ ``` ┌──────────────────────────────────────────────────┐ -│ 头(72 字节,偏移 0) │ +│ 头(104 字节,偏移 0,含型号标识[32]) │ ├──────────────────────────────────────────────────┤ │ 常量表段 offset_const .. offset_funcs │ ├──────────────────────────────────────────────────┤ @@ -22,11 +20,13 @@ ├──────────────────────────────────────────────────┤ │ FB 布局段 offset_fb .. offset_data(v1 为空)│ ├──────────────────────────────────────────────────┤ -│ 数据段 offset_data .. 文件末尾 │ +│ 数据段 offset_data .. offset_data+len │ +├──────────────────────────────────────────────────┤ +│ SHA-256 文件尾(32 字节,对文件尾之前全部内容) │ └──────────────────────────────────────────────────┘ ``` -## 头(72 字节) +## 头(104 字节) | 偏移 | 宽 | 字段 | line1 实际值 | |---|---|---|---| @@ -42,11 +42,15 @@ | 40 | 4 | M 槽数 `n_m` | 0 | | 44 | 4 | 常量数 `n_consts` | 0 | | 48 | 4 | 函数数 `n_funcs` | 2 | -| 52 | 4 | `offset_const` | 72 | -| 56 | 4 | `offset_funcs` | 72 | -| 60 | 4 | `offset_code` | 96 | -| 64 | 4 | `offset_fb` | 168 | -| 68 | 4 | `offset_data` | 168 | +| 52 | 4 | `offset_const` | 104(`0x68`) | +| 56 | 4 | `offset_funcs` | 104 | +| 60 | 4 | `offset_code` | 128(`0x80`) | +| 64 | 4 | `offset_fb` | 200(`0xC8`) | +| 68 | 4 | `offset_data` | 200 | +| 72 | 32 | **型号标识**(定长 ASCII 含版本,补 `'\0'`) | `"STATOR1"`(`0x48..0x67`) | + +> 12.13 修订:型号标识由 `machine.toml [meta] name + version` 拼成(如 `STATOR + 1` → `"STATOR1"`); +> 执行器读取时型号不匹配 → 直接报错。 ## 常量表段(`offset_const` 起) @@ -76,15 +80,15 @@ line1(2 行): - `CALL` 的 `fn_id` = 函数表行下标 - 调用约定:结果 r0、参数 r1..r7、变量与临时 r8 起 -line1 `MAIN` 前 5 条(`0x60` 起,slot 为 8 字节定宽槽号): +line1 `MAIN` 前 5 条(`0x84` 起;`0x80` 是 `fn 0` 的占位 `RET`,slot 为 8 字节定宽槽号): | 偏移 | 字节 | 反汇编 | 含义 | |---|---|---|---| -| 0x64 | `12 08 01 00` | `LOAD_I r8, 1` | I0_0(io.input 绑定 → LOAD_I,槽 1) | -| 0x68 | `17 08 04 00` | `STORE_GLOBAL r8, 4` | starter.start(槽 4)← I0_0 | -| 0x6C | `16 09 02 00` | `LOAD_GLOBAL r9, 2` | I0_1(槽 2) | -| 0x70 | `17 09 05 00` | `STORE_GLOBAL r9, 5` | starter.stop(槽 5)← I0_1 | -| 0x74 | `16 08 04 00` | `LOAD_GLOBAL r8, 4` | 内联体:读 starter.start(槽 4) | +| 0x84 | `12 08 01 00` | `LOAD_I r8, 1` | I0_0(io.input 绑定 → LOAD_I,槽 1) | +| 0x88 | `17 08 04 00` | `STORE_GLOBAL r8, 4` | starter.start(槽 4)← I0_0 | +| 0x8C | `16 09 02 00` | `LOAD_GLOBAL r9, 2` | I0_1(槽 2) | +| 0x90 | `17 09 05 00` | `STORE_GLOBAL r9, 5` | starter.stop(槽 5)← I0_1 | +| 0x94 | `16 08 04 00` | `LOAD_GLOBAL r8, 4` | 内联体:读 starter.start(槽 4) | ## FB 布局段(`offset_fb` 起) @@ -96,24 +100,28 @@ v1 为空(用户 FB 内联展开,字段偏移已算进数据段;内建 8 槽序:**全局块**(`n_globals` 个,声明序)→ **FB 实例块**(POU 收集序、实例声明序,每字段一槽)。 -line1 数据段(56 字节 = 7 槽 × 8B,`0xA8` 起): +line1 数据段(56 字节 = 7 槽 × 8B,`0xC8` 起): | 槽号 | 偏移 | 内容 | |---|---|---| -| 0 | 0xA8 | EmergencyStop(BOOL,初值 0) | -| 1 | 0xB0 | I0_0(BOOL) | -| 2 | 0xB8 | I0_1(BOOL) | -| 3 | 0xC0 | Q0_0(BOOL) | -| 4 | 0xC8 | starter.start(BOOL) | -| 5 | 0xD0 | starter.stop(BOOL) | -| 6 | 0xD8 | starter.q(BOOL) | +| 0 | 0xC8 | EmergencyStop(BOOL,初值 0) | +| 1 | 0xD0 | I0_0(BOOL) | +| 2 | 0xD8 | I0_1(BOOL) | +| 3 | 0xE0 | Q0_0(BOOL) | +| 4 | 0xE8 | starter.start(BOOL) | +| 5 | 0xF0 | starter.stop(BOOL) | +| 6 | 0xF8 | starter.q(BOOL) | + +## SHA-256 文件尾(32 字节) + +数据段之后追加 32 字节 SHA-256:**对文件尾之前全部内容计算**(`0x100..0x11F`)。执行器必须校验通过才运行;任一字节被篡改 → 直接拒绝。compiler 写侧与 vm 读侧**各自实现**一份 SHA-256。 ## 用工具验证 -- `ctest` 的 `isa_roundtrip` / `codegen_slice1` 覆盖编解码往返与映像校验 -- `isa::ImageView` 提供只读视图(校验魔数/版本/段边界/入口 fn_id) +- `ctest`:`codegen_slice1`(写侧 model/sha 自检)、`vm_cycles`(读侧型号/SHA 拒绝)、`machine_config` +- `STCompiler x.stb --disasm --machine machine.toml`:显示 `model=` 与 `sha=ok|BAD` - 手工检查:`xxd line1.stb` 对照上表逐段核对 ## 与执行器的关系 -`BytecodeExecutor`(12.9/12.10 实现)只读本格式:头(`cycle_limit`/`dt_ms`)、函数表(帧 `nregs` + 代码定位)、数据段(全局 + FB 实例状态)。I/O 绑定**不在**映像里,在 sidecar `.runtime.toml`(var → 槽号 → channel/bit)。 +`BytecodeExecutor` 只读本格式:头(`cycle_limit`/`dt_ms`/型号标识)、函数表(帧 `nregs` + 代码定位)、数据段(全局 + FB 实例状态)。读取时**型号匹配 + SHA-256 校验**,任一不符 → 报错拒绝。I/O 绑定**不在**映像里,在 sidecar `.runtime.toml`(var → 槽号 → channel/bit)。 From 6873b5ee0b92ee42aca34bc6d361651e65bbe565 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 22:39:27 +0800 Subject: [PATCH 63/69] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=EF=BC=9Amachine.toml=20=E9=85=8D=E7=BD=AE=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E6=94=B6=E5=B0=BE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md:依赖方向(STCompiler→compiler 不链 isa)、模块表、isa 归执行器侧、 数据流图(machine.toml + 型号/SHA) - isa/README.md:重写——只管指令定义(Types/Op/Instr/Encode),删 Image 部分 - 指令与映像.md:依赖图与 CMake(compiler 不链 isa) - 编译管线.md:管线首步加载 machine.toml、产物含型号/SHA - 初步计划.md:12.10/12.13 修订(--machine、依赖方向) - 扫描周期.md / 指令执行.md:12.9 步骤标注已落地、vm 读校验说明 - compiler/vm/executor README:依赖与边界同步 --- Doc/compiler/编译管线.md | 15 ++--- Doc/isa/指令与映像.md | 12 ++-- Doc/vm/扫描周期.md | 19 +++--- Doc/vm/指令执行.md | 3 + Doc/初步计划.md | 18 +++--- README.md | 28 ++++----- compiler/README.md | 2 +- executor/README.md | 2 +- isa/README.md | 133 +++++++-------------------------------- vm/README.md | 2 +- 10 files changed, 76 insertions(+), 158 deletions(-) diff --git a/Doc/compiler/编译管线.md b/Doc/compiler/编译管线.md index 1bcf34c..4deeda8 100644 --- a/Doc/compiler/编译管线.md +++ b/Doc/compiler/编译管线.md @@ -1,27 +1,28 @@ # 编译管线 -compiler 模块:ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 [`指令与映像.md`](../isa/指令与映像.md)。可执行入口 `STCompiler`(本模块 `src/main.cpp`,链 `compiler` + `isa`)。 +compiler 模块:ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),**不依赖 isa**(指令 opcode / 类型元数据 / FB 布局来自 `compiler/machine.toml`,见 [`指令配置.md`](../isa/指令配置.md))。可执行入口 `STCompiler`(本模块 `src/main.cpp`,链 `compiler`)。 读齐工程后走直通 pass,产出单一映像文件,不上 LLVM。词法、语法、符号表、检查、codegen、链接都留在本库,不再拆 CMake 子库。 -语言规则、toml 字段、链接与第 12 节阶段见 [`初步计划.md`](../初步计划.md)。指令和映像只认 [`指令与映像.md`](../isa/指令与映像.md)。 +语言规则、toml 字段、链接与第 12 节阶段见 [`初步计划.md`](../初步计划.md)。`.stb` 格式契约见 [`指令与映像.md`](../isa/指令与映像.md)(compiler 自带写实现 + 型号标识/SHA-256)。 ## 管线 ```text -读 toml、收齐 .st +加载 machine.toml(--machine,强校验) + → 读 toml、收齐 .st → 词法 / 递归下降语法 → 收集导出符号 → 解析 VAR_EXTERNAL、链接、定地址 → 类型检查 - → 按 PROGRAM / FUNCTION / FB 编寄存器码 + → 按 PROGRAM / FUNCTION / FB 编寄存器码(opcode/类型/FB 布局查配置) → 回填跳转、写映像头与工程哈希 - → 写 .stb(映像)与 .runtime.toml(sidecar:io 绑定) + → 写 .stb(头 104 含型号标识 + 段 + SHA-256 文件尾)与 .runtime.toml(sidecar:io 绑定) ``` ## 边界 - toml **不声明变量**;`VAR_GLOBAL` 第一版只允许出现在 `gvl.file` - `[[io.*]]` 的 `var` 必须已在 GVL 中;同名全局链接失败 -- 第三方 toml 解析放 `third_party/`,本库私有链接 -- 不写扫描周期,不链 `vm` +- 第三方 toml 解析(toml++)放 `third_party/`,本库私有链接 +- 不写扫描周期,不链 `vm`、不链 `isa` diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md index 1805a6a..354fee2 100644 --- a/Doc/isa/指令与映像.md +++ b/Doc/isa/指令与映像.md @@ -2,16 +2,16 @@ isa 模块规范:指令集 + 映像 + 定宽类型。不是编译器,不把 `.st` 编成映像。 -`compiler` 按本文件写出字节,`vm` 只认这些字节。两边不互链。 +`compiler` 写 `.stb`、`vm` 读 `.stb`,两边**各自实现**读写;`isa` 归执行器侧只管指令定义。指令 opcode / 类型 / FB 布局的登记见 [`指令配置.md`](指令配置.md)(`compiler/machine.toml`,与代码强校验)。 全工程定案见 [`初步计划.md`](../初步计划.md)。**类型、指令编码、映像布局以本文为准**,不要另写一份互相打架的表。头文件实现本文,不另当规范。 ```text -STCompiler → compiler → isa -BytecodeExecutor → vm → isa +STCompiler → compiler (编码查 machine.toml;自带 .stb 写) +BytecodeExecutor → vm → isa (isa 只管指令;vm 自带 .stb 读) ``` -CMake 目标:`isa`(`STATIC`),无依赖。公开头:`isa/include/isa/`。 +CMake 目标:`isa`(`STATIC`),无依赖;`vm` PUBLIC 链 `isa`。公开头:`isa/include/isa/`。 --- @@ -240,11 +240,11 @@ isa/ ``` ```cmake -add_library(isa STATIC src/Encode.cpp src/Image.cpp) +add_library(isa STATIC src/Encode.cpp) target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) ``` -`compiler` / `vm`:`target_link_libraries(... PUBLIC isa)`。 +`vm`:`target_link_libraries(vm PUBLIC isa)`;`compiler` 不链 `isa`。 --- diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md index c40ce26..33c0bd1 100644 --- a/Doc/vm/扫描周期.md +++ b/Doc/vm/扫描周期.md @@ -15,7 +15,8 @@ vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [ ## 边界 -- 只认 `isa` 映像;类型与溢出按 [`指令与映像.md`](../isa/指令与映像.md)(`INT` 饱和) +- 只认 `.stb` 映像(`vm` 自带读实现);读取时**型号标识匹配 + SHA-256 校验**,不符/篡改 → 拒绝 +- 类型与溢出按 [`指令与映像.md`](../isa/指令与映像.md)(`INT` 饱和);指令定义归 `isa` - 局部 / `VAR` = 固定寄存器;`nregs` 在函数头 - `CALL` 目标是立即数 `fn_id`,有界调用栈 - 无 GC、无堆、无线程;每周期受 `cycle_limit` 限制 @@ -63,7 +64,7 @@ VM 需要无歧义地访问数据区,当前变宽布局(BOOL 1B / INT 2B / T - `Codegen.cpp` 简化:`width_of`/`align_up` 删除、`layout_data`/`layout_fb_instances` 按槽号布局、指令 slot = 符号表槽号 - `codegen_test` 断言更新(slot 从字节偏移变槽号、数据区字节数变化) -### 步骤 1:`Machine.h`(新建 `vm/include/vm/Machine.h`) +### 步骤 1:`Machine.h`(已落地 `vm/include/vm/Machine.h`) ```cpp namespace vm { @@ -83,32 +84,32 @@ namespace vm { ``` - `Fault` 枚举 + 帧结构 `{ fn_id, regs[nregs], ret_pc, ret_fn_id }`;调用栈深度上限 64 -- 只依赖 `isa`(`ImageView` + 类型/饱和函数) +- 只依赖 `isa`(指令定义)+ 自带 `vm::Image`(.stb 读) -### 步骤 2:`Machine.cpp` 译码 switch +### 步骤 2:`Machine.cpp` 译码 switch(已落地) - 取指:函数表 → `code_offset`(字节)→ 读 u32 → 拆 `op/rd/a/b` - 标量:`MOVE` / `LOADK`(常量表 tag 定值表示)/ `NOT` / `AND` / `OR`(布尔)/ `ADD/SUB/MUL/DIV`(`sat_*`)/ `CMP_xx`(全宽比较) - 跳转:`JMP` / `JT` / `JF`(`pc += off`,相对下一条语义) - 数据区:`LOAD_I/LOAD_M/LOAD_GLOBAL`、`STORE_Q/STORE_M/STORE_GLOBAL`(同一实现,slot × 8) -### 步骤 3:CALL / RET 帧栈 + 故障 +### 步骤 3:CALL / RET 帧栈 + 故障(已落地) - `CALL fn_id`:压新帧(复制 r0..r7)→ pc=0;`RET`:复制 r0..r7 回调用方 → 弹栈(MAIN 的 RET = 周期结束) - `cycle_limit` 超限(用例 6)、栈深 > 64、`BadOp` / `BadSlot` / `BadConst` → 周期中止返回故障 -### 步骤 4:定时器与边沿(8 个 CAL_*) +### 步骤 4:定时器与边沿(已落地) - 执行时用映像 `dt_ms` 推进(每周期一次调用 = 推进一次),不读系统时钟 - TON:in 真 → et += dt(到 pt 停)、q = et ≥ pt;in 假 → et = 0 - TOF:in 真 → q=1、et=0;掉电 → et += dt、et ≥ pt → q=0 - CTU:cu 上升沿(**上次 cu 存 VM 侧 vector**,按实例基槽索引)→ cv+1;r → 复位;q = cv ≥ pv -### 步骤 5:可观察性基础 +### 步骤 5:可观察性基础(已落地) - `step()` / `pc` / `fn_id` / `cycle_count` / `call_depth` / `reg` 落基础实现;`snapshot()` 留 12.11 补全 -### 步骤 6:`vm_test`(`tests/src/vm_test.cpp` + CMake,链 `vm` 库) +### 步骤 6:`vm_test`(已落地) - 手工拼小映像(`isa` 编码函数):赋值、跳转、CALL/RET 帧复制、cycle_limit - 编译器产物全链路:用例 01/02/04/05/07/09/13/15/17/18/20 跑周期断言寄存器与数据区 @@ -116,6 +117,6 @@ namespace vm { - TON:dt=10、pt=30 → 第 3 周期 q=1;CTU:上升沿计数 - **确定性**:同一 I 序列跑两遍,数据区完全一致 -### 步骤 7:验证 + 提交 +### 步骤 7:验证 + 提交(已落地,ctest 14/14) - `cmake --build` + `ctest`(新增 `vm_cycles` 后 10/10 全绿);20 用例扫描:14 正例执行通过 diff --git a/Doc/vm/指令执行.md b/Doc/vm/指令执行.md index 00f810d..f6ade00 100644 --- a/Doc/vm/指令执行.md +++ b/Doc/vm/指令执行.md @@ -5,6 +5,9 @@ ## 执行模型 ```text +加载(Machine::create):解析 .stb(vm 自读实现)→ 型号匹配(内建 STATOR1)→ SHA-256 校验 + 任一不符 → 直接拒绝(不创建机器) + 每个扫描周期(run_cycle): 1. pc = 0,从入口函数(MAIN)开始逐条执行 2. 执行到 MAIN 的 RET → 周期结束 diff --git a/Doc/初步计划.md b/Doc/初步计划.md index 9771ebe..4e9e5ae 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -530,17 +530,21 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom 编译与执行拆成两个可执行,映像 `.stb` 是中间产物: ```text -STCompiler -o .stb # 编译:读 toml + .st → 映像 + sidecar -BytecodeExecutor .stb [--cycles N] # 执行:加载 .stb + sidecar,跑 N 周期 +STCompiler -o .stb --machine +BytecodeExecutor .stb [--cycles N] ``` -- `STCompiler` 链 `compiler` + `isa`;`BytecodeExecutor` 链 `vm` + `isa`,**不链 `compiler`**。 -- 产物两个文件:`.stb`(映像,魔数 `STSC`)+ `.runtime.toml`(sidecar:I/O 绑定 var → 槽号 → channel/bit)。`[[io.*]]` 不进映像,sidecar 由 STCompiler 生成,不创造变量。 +- `STCompiler` 链 `compiler`(**不链 `isa`**,编码查 `machine.toml`);`BytecodeExecutor` 链 `vm` + `isa`,**不链 `compiler`**。 +- 产物两个文件:`.stb`(头 104 含型号标识 + 段 + SHA-256 文件尾)+ `.runtime.toml`(sidecar:I/O 绑定 var → 槽号 → channel/bit)。`[[io.*]]` 不进映像,sidecar 由 STCompiler 生成,不创造变量。 - `dt_ms` / `cycle_limit` 从映像头取,sidecar 不重复配置。 +- 执行器读取时**型号匹配 + SHA-256 校验**,不匹配/篡改 → 拒绝。 - 最小可观测:BytecodeExecutor 打印本周期 I/Q,或把寄存器/映像 dump 出来。 - 回放:读预先录制的 I 序列(文本即可),不接真实硬件。 -完成:`STCompiler examples/line1/project.toml -o line1.stb` 产出两个文件;`BytecodeExecutor line1.stb` 能跑若干周期。 +完成:`STCompiler examples/line1/project.toml -o line1.stb --machine compiler/machine.toml` 产出两个文件;`BytecodeExecutor line1.stb` 能跑若干周期。 + +> 12.13 修订:指令 opcode / 类型 / FB 布局的登记移入 `compiler/machine.toml`(见 [`指令配置.md`](isa/指令配置.md)), +> `STCompiler` 编译路径必填 `--machine`。 --- @@ -575,8 +579,8 @@ BytecodeExecutor .stb [--cycles N] # 执行:加载 .stb + sidecar ### 12.13 依赖方向(全程遵守) ```text -STCompiler → compiler → isa -BytecodeExecutor → vm → isa +STCompiler → compiler (编码查 machine.toml;自带 .stb 写) +BytecodeExecutor → vm → isa (isa 只管指令;vm 自带 .stb 读) ``` - 指令或映像布局有变:先改 `isa` 和编解码测试,再改两端。 diff --git a/README.md b/README.md index 2728cc2..4f7c4d5 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ 方案见 [`Doc/初步计划.md`](Doc/初步计划.md),模块文档见 [`Doc/索引.md`](Doc/索引.md)。 ```text -多个 .st - ↑ project.toml 列出 +多个 .st + machine.toml(指令/类型/FB 布局) + ↑ project.toml 列出源文件;machine.toml 唯一事实来源 ↓ -STCompiler(compiler 库:词法 / 语法 → 符号表 → 类型检查 → 寄存器码 → 链接) +STCompiler(compiler 库:词法 / 语法 → 符号表 → 类型检查 → 寄存器码 → 链接;编码查 machine.toml) ↓ -.stb 映像 + .runtime.toml sidecar +.stb 映像(头 104 含型号标识 + 段 + SHA-256 文件尾)+ .runtime.toml sidecar ↓ -BytecodeExecutor(vm 库按扫描周期执行;喂 I、收 Q、给 Δt、回放) +BytecodeExecutor(vm 库按扫描周期执行;型号/SHA 校验;喂 I、收 Q、给 Δt、回放) ``` ## 目录 @@ -40,33 +40,33 @@ BytecodeExecutor(vm 库按扫描周期执行;喂 I、收 Q、给 Δt、回 ## 模块与 CMake 目标 -三个静态库与两个可执行,与目录一一对应。`compiler` 与 `vm` 不互相链接,合同放在 `isa`。依赖方向:`STCompiler → compiler → isa`、`BytecodeExecutor → vm → isa`。 +三个静态库与两个可执行,与目录一一对应。`compiler` 与 `vm` 不互相链接。指令/类型/FB 布局的**唯一事实来源是 `compiler/machine.toml`**(编译器运行时加载);`isa` 归执行器侧(`vm` 依赖),只管指令定义。依赖方向:`STCompiler → compiler`、`BytecodeExecutor → vm → isa`。 | 目录 | 目标 | 类型 | 依赖 | 职责 | |---|---|---|---|---| -| `isa/` | `isa` | `STATIC` | 无 | 约 20 条指令编码、映像头、工程哈希、`BOOL` / `INT` / `TIME` 宽度 | -| `compiler/` | `compiler` | `STATIC` | `isa` | 读 toml、收齐 `.st`、解析、链接、定址、类型检查、编寄存器码、写映像 | -| `vm/` | `vm` | `STATIC` | `isa` | 一台 VM、一个入口、一份映像;扫描周期、寄存器、FB、有界 `CALL` / `RET` | -| `compiler/` | `STCompiler` | `EXECUTABLE` | `compiler`、`isa` | `STCompiler -o .stb`:编译并写映像 + sidecar;`--disasm` 可反汇编 | +| `isa/` | `isa` | `STATIC` | 无 | 指令定义(Op/Instr/Encode/Types/饱和/disasm),**归执行器侧** | +| `compiler/` | `compiler` | `STATIC` | toml++(私有) | 读 toml、收齐 `.st`、解析、链接、定址、类型检查、编寄存器码、写 `.stb`(指令 opcode/类型/FB 布局全部来自 `machine.toml`) | +| `vm/` | `vm` | `STATIC` | `isa` | 一台 VM、一个入口、一份映像;扫描周期、寄存器、FB、有界 `CALL` / `RET`;自带 `.stb` 读实现 + 型号/SHA-256 校验 | +| `compiler/` | `STCompiler` | `EXECUTABLE` | `compiler` | `STCompiler -o .stb --machine `:编译并写映像 + sidecar;`--disasm` 可反汇编 | | `executor/` | `BytecodeExecutor` | `EXECUTABLE` | `vm`、`isa` | `BytecodeExecutor .stb`:加载映像 + sidecar,采样 I、执行 `PROGRAM MAIN`、写回 Q、用 Δt 推进定时器 / 计数器、回放 | 词法、语法、符号表、检查、codegen、链接都留在 `compiler` 一个库里,不再拆 CMake 子库。 ### `isa` -编译器写映像、虚拟机读映像,两边共用同一套编码。放这里避免循环依赖。 +归执行器侧(`vm` 依赖),**只管指令定义**:Op 枚举 / Instr 打包 / Encode(编解码·disasm)/ Types(饱和)。执行器执行 switch 在 `vm`。 ### `compiler` -toml **不声明变量**,只列出源文件、指定唯一 GVL、可选地把已有全局接到硬件。`VAR_GLOBAL` 第一版只允许出现在 `gvl.file`(如 `globals.st`)。 +toml **不声明变量**,只列出源文件、指定唯一 GVL、可选地把已有全局接到硬件。`VAR_GLOBAL` 第一版只允许出现在 `gvl.file`(如 `globals.st`)。指令 opcode / 类型元数据 / FB 布局**全部来自 `compiler/machine.toml`**(`STCompiler --machine` 加载,强校验后编译)。 ### `vm` -每个扫描周期:采样 I → 从 `PROGRAM MAIN` 的 `pc=0` 执行到 `RET` → 写回 Q → 用本周期 Δt 推进 `TON` / `TOF` / `CTU`。无 GC、无堆、无线程;变量编译期分配。 +每个扫描周期:采样 I → 从 `PROGRAM MAIN` 的 `pc=0` 执行到 `RET` → 写回 Q → 用本周期 Δt 推进内置 FB。无 GC、无堆、无线程;变量编译期分配。读取 `.stb` 时**校验型号标识与 SHA-256**(不匹配/篡改 → 拒绝)。 ### `executor`(BytecodeExecutor) -第一版两个可执行:`STCompiler` 编译出 `.stb`(映像)+ `.runtime.toml`(sidecar:I/O 绑定);`BytecodeExecutor` 加载后按扫描周期跑。不再有"一条命令编译 + 跑"。`dt_ms` / `cycle_limit` 从映像头取。 +`STCompiler` 编译出 `.stb`(含型号标识 + SHA-256)+ `.runtime.toml`(sidecar:I/O 绑定);`BytecodeExecutor` 加载后按扫描周期跑。`dt_ms` / `cycle_limit` 从映像头取。 ### `examples/` 与 `tests/` diff --git a/compiler/README.md b/compiler/README.md index e1fafc1..0b567c3 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -1,6 +1,6 @@ # compiler -ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 `isa`;可执行 `STCompiler`。 +ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),**不依赖 `isa`**——指令 opcode / 类型元数据 / FB 布局来自 `compiler/machine.toml`(见 [`指令配置.md`](../doc/isa/指令配置.md));自带 `.stb` 写实现。可执行入口 `STCompiler`。 - **使用说明**(构建 / 用法 / 工程文件 / ST 子集 / 错误类别):[`doc/compiler/STCompiler使用说明.md`](../doc/compiler/STCompiler使用说明.md) - 边界与管线:[`doc/compiler/编译管线.md`](../doc/compiler/编译管线.md) diff --git a/executor/README.md b/executor/README.md index ca9eb17..eceb121 100644 --- a/executor/README.md +++ b/executor/README.md @@ -1,5 +1,5 @@ # executor -BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`(`EXECUTABLE`),链接 `vm` 和 `isa`,**不链 `compiler`**。 +BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`(`EXECUTABLE`),链接 `vm` 和 `isa`,**不链 `compiler`**。读取 `.stb` 时校验型号标识与 SHA-256。 职责与边界见 [`doc/executor/执行器入口.md`](../doc/executor/执行器入口.md)。 diff --git a/isa/README.md b/isa/README.md index 608797c..137d01b 100644 --- a/isa/README.md +++ b/isa/README.md @@ -1,15 +1,15 @@ # isa -指令集 + 映像 + 定宽类型。CMake 目标:`isa`(`STATIC`),无依赖。 +**归执行器侧**,只管指令定义。CMake 目标:`isa`(`STATIC`),无依赖;被 `vm` 依赖。 -`compiler` 写映像、`vm` 读映像,两边只依赖本目录,不互链。 +`compiler` **不依赖 isa**——指令 opcode / 类型元数据 / FB 布局全部来自 `compiler/machine.toml`(见 [`指令配置.md`](../doc/isa/指令配置.md))。`.stb` 映像读写由 `compiler`(写侧)与 `vm`(读侧)**各自实现**。 ```text -STCompiler → compiler → isa -BytecodeExecutor → vm → isa +STCompiler → compiler (编码查 machine.toml,自带 .stb 写) +BytecodeExecutor → vm → isa (isa 只管指令;vm 自带 .stb 读) ``` -**类型、操作码、指令字、映像布局以** [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md) **为准。** 头文件实现该文,本 README 不另当规范。`.stb` 结构面向使用者的拆解见 [`doc/isa/stb文件格式.md`](../doc/isa/stb文件格式.md)。全工程阶段见 [`doc/初步计划.md`](../doc/初步计划.md) 12.0~12.1。 +**操作码、指令字以** [`doc/isa/指令与映像.md`](../doc/isa/指令与映像.md) **为准。** 头文件实现该文,本 README 不另当规范。`.stb` 结构拆解见 [`doc/isa/stb文件格式.md`](../doc/isa/stb文件格式.md)。指令/类型/FB 登记见 [`doc/isa/指令配置.md`](../doc/isa/指令配置.md)。 公开头:`isa/include/isa/`。 @@ -17,139 +17,48 @@ BytecodeExecutor → vm → isa ## 做 / 不做 -**做:** 操作码与 32-bit 指令、映像头与段、`BOOL`/`INT`/`TIME` 宽度与饱和、编解码、工程哈希、一条指令的文本转储。 +**做:** 操作码与 32-bit 指令字、`BOOL`/`INT`/`TIME` 定宽与饱和、编解码、一条指令的文本转储。 -**不做:** toml、关键字、符号表、AST、VM `switch`、扫描周期、I/O 采样。`AND`/`OR` 只组合已算好的值;短路由 `compiler` 编成跳转。 +**不做:** 映像读写(compiler/vm 各自实现)、toml/关键字/符号表/AST、VM 执行 switch(在 `vm`)、扫描周期、I/O 采样。 --- -## 实现步骤 - -原则:先冻合同,再头文件,再 `.cpp`,再最小测试。每步能编过再进下一步。改编码或映像字段时,先改规范文和本模块,再动 `compiler` / `vm`。 - -目标目录(与规范一致): +## 目录与 CMake ```text isa/ CMakeLists.txt README.md include/isa/ - Types.h # 宽度、饱和、槽值 - Op.h # 操作码枚举 + 助记符 + Types.h # 宽度、饱和 + Op.h # 操作码枚举 + OpFormat/OpClass + kOpDefs 表 + 助记符 Instr.h # 32-bit 打包/拆字段(inline) Encode.h # encode / decode / disasm - Image.h # 头、段、FNV、读写缓冲 src/ Encode.cpp - Image.cpp ``` ```cmake -add_library(isa STATIC src/Encode.cpp src/Image.cpp) +add_library(isa STATIC src/Encode.cpp) target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) ``` -`compiler` / `vm`:`target_link_libraries(... PUBLIC isa)`。 +`vm`:`target_link_libraries(vm PUBLIC isa)`;`compiler` 不链 `isa`。 --- -### 0. CMake 骨架 +## 实现说明 -根 `CMakeLists.txt` 已 `add_subdirectory` 了 `compiler` / `vm` / `executor`。这三个目录若没有 `CMakeLists.txt`,顶层 configure 失败,`isa` 也编不过。 +- **`Types.h`**:`Bool` = `uint8_t`、`Int` = `int16_t`、`Time` = `int64_t`(毫秒,理由见规范文);`sat_add/sub/mul/div`(饱和规则见规范文);`TypeTag`(0/1/2,`.stb` 常量表契约) +- **`Op.h`**:`enum Op : uint8_t`(数值即编码,与 `machine.toml` 强校验一致);`OpFormat`(RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE)与 `OpClass`(Plain/Instance);34 条 `kOpDefs{ name, format, cls }`;`mnemonic/format/op_class` 查表 +- **`Encode.cpp`**:`encode/decode` 往返 + `disasm`(按 format 表驱动,文本与规范文一致) -本步只让工程能配置、能链,不写业务。 +## 测试 -- 写本目录 `CMakeLists.txt`(上面那段即可) -- `compiler` / `vm`:空 `.cpp` + `PUBLIC` 链 `isa` -- `executor`:`BytecodeExecutor`,`main` 打印版本,链 `vm`、`isa`;`STCompiler` 入口在 `compiler/src/main.cpp`,链 `compiler`、`isa` -- 根 CMake:`enable_testing()`(测试文件下一步再加) - -完成:`cmake --build` 过。 - ---- - -### 1. 冻合同(先改规范文) - -[`指令与映像.md`](../doc/isa/指令与映像.md) 已有助记符和 `[ op:8 | rd:8 | a:8 | b:8 ]`,还缺**数值和映像字节**。头文件不得自己发明一份表。 - -把下面这些写进规范文,再写代码(细则以该文最终落笔为准): - -- 指令字:小端 `uint32_t`;`op`/`rd`/`a`/`b` 各 8 bit;`imm16 = a | (b << 8)`;`off16` 为有符号、单位是指令条数 -- 操作码 0..28 连续赋值,以后只追加不插队 -- 各指令操作数字面(`LOADK`/`JMP`/`CALL`/`LOAD_*`/`STORE_*`/`CAL_*`/`RET`) -- `INT` 饱和、`INT_MIN / -1`、除零、`BOOL` 只允许 0/1 -- FNV-1a 64 的 basis/prime;路径只当排序键,对文件内容 update;空集合 = basis -- 映像魔数 `STSC`、版本 1、头字段偏移与长度、常量表/函数表/FB 表一行的字节 -- 第一版可写映像:空槽、空常量表/FB、`MAIN`(`fn_id=0`)、一条 `RET` - -完成:规范文能当唯一合同;本 README 仍只指向它。 - ---- - -### 2. `Types.h` - -`include/isa/Types.h`,inline,无对应 `.cpp`。命名空间 `isa`。C++20;本模块保持简单结构体,不依赖花哨特性。 - -- `Bool` = `uint8_t`,`Int` = `int16_t`,`Time` = `int64_t`(毫秒;理由见规范文) -- `TypeTag`:`Bool` / `Int` / `Time` -- `sat_add` / `sat_sub` / `sat_mul` / `sat_div`(规则见规范文) -- `pack_bool` / `pack_int` / `pack_time` → 4 字节 cell;对应 unpack - -完成:单独包含该头能编译。 - ---- - -### 3. `Op.h` + `Instr.h` - -- `Op.h`:`enum Op : uint8_t`,取值与规范文一致;`mnemonic(Op)` 助记符(已在头内落地) -- `Instr.h`:`typedef uint32_t Instr`;`pack`、取 `op`/`rd`/`a`/`b`、`imm16`/`off16`;按形态 `enc_rr` / `enc_rrr` / `enc_imm` / `enc_jmp` / `enc_jc` - -完成:能打包/拆开一条 32-bit 指令,不依赖 `.cpp`。 - ---- - -### 4. `Encode.h` + `Encode.cpp` - -给 `STCompiler --disasm` 和测试往返打印: - -- `Decoded { Op op; uint8_t rd, a, b; }` -- `Instr encode(Decoded)` / `Decoded decode(Instr)` -- `disasm(Instr, char* out, size_t cap)` - -`disasm` 格式冻在规范文,例如:`RET`、`MOVE r1, r2`、`LOADK r0, 3`、`ADD r1, r2, r3`、`JMP +4`、`JT r0, -1`、`CALL 1`、`LOAD_I r0, 2`、`CAL_TON 0`。 - -完成:一条指令能编、能解、能打出一行文本。 - ---- - -### 5. `Image.h` + `Image.cpp` - -- `fnv1a64` / `fnv1a64_update` -- `ImageView`:只读视图,指向外部缓冲 -- `read_image`:校验魔数、版本、头大小、各段不越界、`entry_fn_id` 落在函数表内 -- `write_ret_main(cycle_limit, dt_ms, hash)` → `std::vector`:空槽 + `MAIN` + 一条 `RET` -- `.stb` 文件读写与 `.runtime.toml` sidecar(I/O 绑定 var → 槽号 → channel/bit) - -小端 `put_*` / `get_*` 只放 `.cpp`,不把 packed struct 当 ABI。本阶段不写完整 `ImageBuilder`(等 codegen)。 - -完成:能写出并读回「空映像 + 一条 `RET`」。 - ---- - -### 6. 最小测试 - -在 `tests/` 加一个只链 `isa` 的 CTest 可执行文件(不是第 12.2 节那 20 个用例): - -- `RET` / `ADD` / `LOADK` / `JMP -1`:encode 后再 decode,字段一致;`disasm` 非空 -- `write_ret_main` 再 `read_image`:魔数、version=1、`n_funcs=1`、入口是 `RET` -- `fnv1a64` 空输入等于 basis - -完成:`ctest` 过上述用例。 - ---- +`tests/isa_test`(链 `isa`):饱和、操作码冻结值、指令打包、encode/decode 往返、disasm 文本、OpClass 分类。 ## 验收 -- `cmake --build` 过;`ctest` 过 isa 往返 -- `compiler` / `vm` 只 `#include `,不互相包含 -- 操作码或映像有变:先改 [`指令与映像.md`](../doc/isa/指令与映像.md) + 本目录 + 本测试,再改两端 +- `cmake --build` 过;`ctest` 过(`isa_roundtrip` 等 14 个用例全绿) +- `vm` 只 `#include `;`compiler` 不引用 `isa` +- 操作码有变:先改 `compiler/machine.toml` + 本目录 + 测试,再改两端 diff --git a/vm/README.md b/vm/README.md index ec97cc3..1136036 100644 --- a/vm/README.md +++ b/vm/README.md @@ -1,5 +1,5 @@ # vm -寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 `isa`。 +寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 `isa`(只管指令定义);自带 `.stb` 读实现 + 型号/SHA-256 校验。 扫描周期与边界见 [`doc/vm/扫描周期.md`](../doc/vm/扫描周期.md)。 From 96607cfe75f8e80e2e16bdfe9104f66bb0d5c2c7 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 23:19:08 +0800 Subject: [PATCH 64/69] =?UTF-8?q?isa=20=E6=A8=A1=E5=9D=97=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=AE=8C=E5=96=84=EF=BC=9ADoxygen=20=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Types.h:定宽类型、TypeTag、饱和四则的 @brief/@param/@return - Op.h:Op 枚举冻结约束、OpFormat/OpClass、OpDef/kOpDefs 表、三个查询函数 - Instr.h:指令字布局、pack/拆字段、按形态 enc_* 系列 - Encode.h/.cpp:Decoded 字段语义、encode/decode/disasm 契约 --- isa/include/isa/Encode.h | 37 +++++++++++--- isa/include/isa/Instr.h | 108 ++++++++++++++++++++++++++++++++++----- isa/include/isa/Op.h | 53 +++++++++++++++---- isa/include/isa/Types.h | 61 ++++++++++++++++++---- isa/src/Encode.cpp | 20 +++++++- 5 files changed, 237 insertions(+), 42 deletions(-) diff --git a/isa/include/isa/Encode.h b/isa/include/isa/Encode.h index ea4d8ad..b4dfcfa 100644 --- a/isa/include/isa/Encode.h +++ b/isa/include/isa/Encode.h @@ -1,6 +1,7 @@ /** * @file Encode.h * @brief 指令编解码与反汇编 + * @details 给 STCompiler --disasm 与测试往返使用;文本格式见 Doc/isa/指令与映像.md。 * @author * @date 2026-08-19 */ @@ -14,18 +15,40 @@ #include "isa/Op.h" namespace isa { - // 解码后的字段视图 + /** + * @brief 解码后的字段视图。 + * @details 各字段是 8 位原始值,含义按操作数形态(OpFormat)解释, + * a|b 可用 imm16 / off16 组合成 16 位视图。 + */ struct Decoded { - Op op; - uint8_t rd; - uint8_t a; - uint8_t b; + Op op; ///< 操作码 + uint8_t rd; ///< 目标寄存器 / 条件寄存器 + uint8_t a; ///< 源寄存器 / 立即数低 8 位 / 偏移低 8 位 + uint8_t b; ///< 源寄存器 / 立即数高 8 位 / 偏移高 8 位 }; - // 编解码往返:encode(decode(w)) == w + /** + * @brief 编码:字段视图打包成一条 32 位指令字。 + * @param d 解码后的字段视图 + * @return 打包后的指令字;满足 encode(decode(w)) == w + */ Instr encode(Decoded d); + + /** + * @brief 解码:指令字拆成字段视图。 + * @param w 32 位指令字 + * @return 各字段视图 + */ Decoded decode(Instr w); - // 反汇编一行文本,写入 out,至多 cap 字节(含 '\0')。格式见 Doc/isa/指令与映像.md。 + /** + * @brief 反汇编一条指令为一行文本。 + * @param w 指令字 + * @param out 输出缓冲 + * @param cap 缓冲容量(含 '\0');cap==0 时直接返回、不写 out + * @details 按操作数形态表驱动输出(如 `RET`、`MOVE r1, r2`、`LOADK r0, 3`、 + * `ADD r1, r2, r3`、`JMP +4`、`JT r0, -1`、`CALL 1`、`CAL_TON 0`); + * 未知操作码输出 `??? 0x`。 + */ void disasm(Instr w, char* out, size_t cap); } diff --git a/isa/include/isa/Instr.h b/isa/include/isa/Instr.h index 72afc27..f9e4a6d 100644 --- a/isa/include/isa/Instr.h +++ b/isa/include/isa/Instr.h @@ -1,6 +1,8 @@ /** * @file Instr.h - * @brief 32-bit 指令打包 / 拆字段 + * @brief 32-bit 指令字打包 / 拆字段 + * @details 指令字布局:`[ op:8 | rd:8 | a:8 | b:8 ]`,整体是一个小端 uint32_t。 + * 字段含义按操作码形态解释,见 Doc/isa/指令与映像.md。 * @author * @date 2026-08-19 */ @@ -12,11 +14,17 @@ #include "isa/Op.h" namespace isa { - // 指令字:[ op:8 | rd:8 | a:8 | b:8 ],整体是一个小端 uint32_t。 - // 字段含义按操作码形态解释,见 Doc/isa/指令与映像.md。 + /// 指令字:`[ op:8 | rd:8 | a:8 | b:8 ]`,小端 uint32_t。 typedef uint32_t Instr; - // 打包 / 拆字段 + /** + * @brief 打包:四个字段拼成一条指令字。 + * @param op 操作码(低 8 位) + * @param rd 目标寄存器 / 条件寄存器(第 8..15 位) + * @param a 源寄存器 / 立即数低 8 位 / 偏移低 8 位(第 16..23 位) + * @param b 源寄存器 / 立即数高 8 位 / 偏移高 8 位(第 24..31 位) + * @return 打包后的指令字 + */ inline Instr pack(Op op, uint8_t rd, uint8_t a, uint8_t b) { return static_cast(static_cast(op)) | (static_cast(rd) << 8) @@ -24,69 +32,143 @@ namespace isa { | (static_cast(b) << 24); } + /** + * @brief 取操作码。 + * @param w 指令字 + * @return 低 8 位解释为 Op + */ inline Op op(Instr w) { return static_cast(w & 0xFFu); } + /** + * @brief 取目标寄存器字段。 + * @param w 指令字 + * @return rd(第 8..15 位) + */ inline uint8_t rd(Instr w) { return static_cast((w >> 8) & 0xFFu); } + /** + * @brief 取 a 字段。 + * @param w 指令字 + * @return a(第 16..23 位) + */ inline uint8_t a(Instr w) { return static_cast((w >> 16) & 0xFFu); } + /** + * @brief 取 b 字段。 + * @param w 指令字 + * @return b(第 24..31 位) + */ inline uint8_t b(Instr w) { return static_cast((w >> 24) & 0xFFu); } - // 组合视图:a|b 拼成 16 位(const_id / fn_id / slot) + /** + * @brief 组合视图:a|b 拼成 16 位无符号数。 + * @param w 指令字 + * @return 小端拼出的 16 位值,用作 const_id / fn_id / slot 号 + */ inline uint16_t imm16(Instr w) { return static_cast(a(w) | (static_cast(b(w)) << 8)); } - // 组合视图:a|b 为有符号相对偏移,单位是指令条数 + /** + * @brief 组合视图:a|b 为有符号相对偏移。 + * @param w 指令字 + * @return 偏移量,单位是指令条数(跳转目标 = 下一条指令 + off) + */ inline int16_t off16(Instr w) { return static_cast(imm16(w)); } - // 按形态的便捷编码 + /** + * @brief 按 RR 形态编码。 + * @param op 操作码(MOVE / NOT) + * @param rd 目标寄存器 + * @param rs 源寄存器(放进 a) + * @return 指令字 + */ inline Instr enc_rr(Op op, uint8_t rd, uint8_t rs) { - return pack(op, rd, rs, 0); // MOVE / NOT:a = rs + return pack(op, rd, rs, 0); } + /** + * @brief 按 RRR 形态编码。 + * @param op 操作码(AND/OR/ADD/.../CMP_xx) + * @param rd 目标寄存器 + * @param ra 第一源寄存器 + * @param rb 第二源寄存器 + * @return 指令字 + */ inline Instr enc_rrr(Op op, uint8_t rd, uint8_t ra, uint8_t rb) { - return pack(op, rd, ra, rb); // AND/OR/ADD/.../CMP_xx + return pack(op, rd, ra, rb); } + /** + * @brief 按 IMM 形态编码。 + * @param op 操作码(LOADK) + * @param rd 目标寄存器 + * @param imm 16 位立即数(const_id),拆成 a|b 存放 + * @return 指令字 + */ inline Instr enc_imm(Op op, uint8_t rd, uint16_t imm) { return pack(op, rd, static_cast(imm & 0xFFu), - static_cast((imm >> 8) & 0xFFu)); // LOADK + static_cast((imm >> 8) & 0xFFu)); } + /** + * @brief 按 JMP 形态编码。 + * @param off 有符号相对偏移(条数),拆成 a|b 存放 + * @return 指令字 + */ inline Instr enc_jmp(int16_t off) { return pack(Op::JMP, 0, static_cast(static_cast(off) & 0xFFu), static_cast((static_cast(off) >> 8) & 0xFFu)); } + /** + * @brief 按 JC 形态编码(条件跳转)。 + * @param op 操作码(JT / JF) + * @param r 条件寄存器(放进 rd) + * @param off 有符号相对偏移(条数),放进 a|b + * @return 指令字 + */ inline Instr enc_jc(Op op, uint8_t r, int16_t off) { - // JT / JF:条件寄存器在 rd,偏移在 a|b return pack(op, r, static_cast(static_cast(off) & 0xFFu), static_cast((static_cast(off) >> 8) & 0xFFu)); } + /** + * @brief 按 SLOT 形态编码。 + * @param op 操作码(LOAD_I / STORE_Q / LOAD_M / STORE_M / LOAD_GLOBAL / STORE_GLOBAL / CAL_*) + * @param rd 目标寄存器(CAL_* 时置 0) + * @param slot 槽号 / 实例号,放进 a|b + * @return 指令字 + */ inline Instr enc_slot(Op op, uint8_t rd, uint16_t slot) { - // LOAD_I / STORE_Q / LOAD_M / STORE_M / LOAD_GLOBAL / STORE_GLOBAL / CAL_* - // 槽号 / 实例号在 a|b return enc_imm(op, rd, slot); } + /** + * @brief 按 CALL 形态编码。 + * @param fn_id 函数表下标,放进 a|b + * @return 指令字 + */ inline Instr enc_call(uint16_t fn_id) { return enc_imm(Op::CALL, 0, fn_id); } + /** + * @brief 按 NONE 形态编码(RET)。 + * @return 指令字 + */ inline Instr enc_ret() { return pack(Op::RET, 0, 0, 0); } diff --git a/isa/include/isa/Op.h b/isa/include/isa/Op.h index a28440d..a3684ac 100644 --- a/isa/include/isa/Op.h +++ b/isa/include/isa/Op.h @@ -1,6 +1,8 @@ /** * @file Op.h - * @brief 操作码枚举 + 助记符 + * @brief 操作码枚举、操作数形态与编译期指令表 + * @details 操作码数值即 .stb 指令编码。执行器(vm)只依赖本表; + * 编译器侧的登记见 Doc/isa/指令配置.md(compiler/machine.toml,与之强校验)。 * @author * @date 2026-08-19 */ @@ -10,8 +12,11 @@ #include namespace isa { - // 操作码一次列全。数值即编码:冻结后不得改顺序、不得插值, - // 增加指令只能追加到末尾,并同步 Doc/isa/指令与映像.md 与测试。 + /** + * @brief 操作码枚举。 + * @details 数值即编码:冻结后不得改顺序、不得插值,增加指令只能追加到末尾, + * 并同步 Doc/isa/指令与映像.md、compiler/machine.toml 与测试。 + */ enum class Op : uint8_t { MOVE = 0, // MOVE rd, rs LOADK = 1, // LOADK rd, const_id @@ -50,10 +55,13 @@ namespace isa { RET = 33, // RET }; - // 操作码总数 + /// 操作码总数(= 最大 opcode + 1,kOpDefs 的下标上界)。 static const int kOpCount = 34; - // 操作数形态(三层分类第二层,见 Doc/isa/指令配置.md) + /** + * @brief 操作数形态(三层分类第二层,见 Doc/isa/指令配置.md)。 + * @details 决定操作数 a|b 的解释方式与反汇编文本格式。 + */ enum class OpFormat : uint8_t { RR, // 两寄存器:rd rs(MOVE / NOT) RRR, // 三寄存器:rd ra rb(逻辑 / 算术 / 比较) @@ -66,19 +74,28 @@ namespace isa { NONE, // 无操作数(RET) }; - // 操作对象大类(三层分类第一层) + /** + * @brief 操作对象大类(三层分类第一层)。 + */ enum class OpClass : uint8_t { Plain, // 无实例:寄存器 / 立即数 / 槽 / 分支 / 调用 Instance, // 有实例:操作数据区实例块(内建 FB) }; - // 编译期指令表:下标与 Op 数值一一对应(ops.txt 强校验的基准) + /** + * @brief 编译期指令表条目。 + * @details 一条指令的名称、操作数形态与大类;下标与 Op 数值一一对应。 + */ struct OpDef { - const char* name; - OpFormat format; - OpClass cls; + const char* name; ///< 助记符(与 .stb / machine.toml 一致) + OpFormat format; ///< 操作数形态 + OpClass cls; ///< 操作对象大类 }; + /** + * @brief 编译期指令表:下标与 Op 数值一一对应(machine.toml 强校验的基准)。 + * @details 表与 Op 枚举同步维护;查询入口见 mnemonic / format / op_class。 + */ static const OpDef kOpDefs[kOpCount] = { {"MOVE", OpFormat::RR, OpClass::Plain}, // 0 {"LOADK", OpFormat::IMM, OpClass::Plain}, // 1 @@ -116,17 +133,31 @@ namespace isa { {"RET", OpFormat::NONE, OpClass::Plain}, // 33 }; - // 助记符 / 格式 / 大类:查表(越界返回哨兵) + /** + * @brief 查助记符。 + * @param op 操作码 + * @return 对应助记符;越界返回哨兵 "???" + */ inline const char* mnemonic(Op op) { const int idx = static_cast(op); return (idx >= 0 && idx < kOpCount) ? kOpDefs[idx].name : "???"; } + /** + * @brief 查操作数形态。 + * @param op 操作码 + * @return 对应 OpFormat;越界返回 OpFormat::NONE + */ inline OpFormat format(Op op) { const int idx = static_cast(op); return (idx >= 0 && idx < kOpCount) ? kOpDefs[idx].format : OpFormat::NONE; } + /** + * @brief 查操作对象大类。 + * @param op 操作码 + * @return 对应 OpClass;越界返回 OpClass::Plain + */ inline OpClass op_class(Op op) { const int idx = static_cast(op); return (idx >= 0 && idx < kOpCount) ? kOpDefs[idx].cls : OpClass::Plain; diff --git a/isa/include/isa/Types.h b/isa/include/isa/Types.h index 2fa1efb..104e783 100644 --- a/isa/include/isa/Types.h +++ b/isa/include/isa/Types.h @@ -1,6 +1,8 @@ /** * @file Types.h - * @brief 类型定义 + * @brief 定宽类型与饱和算术 + * @details 虚拟机字长与类型契约的唯一来源。compiler 与 vm 共用同一套规则, + * 详见 Doc/isa/指令与映像.md。 * @author * @date 2026-08-18 */ @@ -12,15 +14,28 @@ namespace isa { namespace types { - // 定宽类型。整数溢出规则:饱和(夹到 INT 最小/最大), - // 由 compiler 与 vm 按同一规则实现,见 Doc/isa/指令与映像.md。 - using BOOL = uint8_t; // 只允许 0/1 - using INT = int16_t; // IEC INT - // 毫秒,有符号。64 位理由见 Doc/isa/指令与映像.md: - // 差值可负、为 DATE_AND_TIME 与 64 位定时器预留。槽位按 8 字节对齐。 + /** + * @brief 布尔值:8 位,只允许 0/1(0=FALSE,1=TRUE)。 + */ + using BOOL = uint8_t; + + /** + * @brief IEC INT:16 位有符号整数,溢出按饱和规则处理。 + */ + using INT = int16_t; + + /** + * @brief 时间:64 位有符号整数,单位毫秒。 + * @details 取 64 位的理由见 Doc/isa/指令与映像.md:差值可负、 + * 为 DATE_AND_TIME 与 64 位定时器预留。槽位按 8 字节对齐。 + */ using TIME = int64_t; - // 槽 / 常量表类型标记,数值冻结:0=BOOL、1=INT、2=TIME(见映像规范) + /** + * @brief 槽 / 常量表类型标记。 + * @details 数值冻结并写入 .stb 常量表:0=BOOL、1=INT、2=TIME(见映像规范), + * 不得改顺序、不得插值。 + */ enum TypeTag : uint8_t { Bool = 0, Int = 1, @@ -29,6 +44,13 @@ namespace isa { // 饱和算术:夹到 INT 最小/最大,编译期与 VM 共用同一规则。 + /** + * @brief 饱和加法。 + * @param a 加数 + * @param b 加数 + * @return a+b,超出 INT 范围时夹到最小/最大值 + * @details 中间量用 int32 计算,避免 int16 溢出未定义行为。 + */ inline INT sat_add(INT a, INT b) { const int32_t sum = static_cast(a) + static_cast(b); return static_cast(sum < std::numeric_limits::min() ? std::numeric_limits::min() @@ -36,6 +58,13 @@ namespace isa { : sum); } + /** + * @brief 饱和减法。 + * @param a 被减数 + * @param b 减数 + * @return a-b,超出 INT 范围时夹到最小/最大值 + * @details 中间量用 int32 计算,避免 int16 溢出未定义行为。 + */ inline INT sat_sub(INT a, INT b) { const int32_t diff = static_cast(a) - static_cast(b); return static_cast(diff < std::numeric_limits::min() ? std::numeric_limits::min() @@ -43,6 +72,13 @@ namespace isa { : diff); } + /** + * @brief 饱和乘法。 + * @param a 乘数 + * @param b 乘数 + * @return a*b,超出 INT 范围时夹到最小/最大值 + * @details 中间量用 int32 计算,避免 int16 溢出未定义行为。 + */ inline INT sat_mul(INT a, INT b) { const int32_t prod = static_cast(a) * static_cast(b); return static_cast(prod < std::numeric_limits::min() ? std::numeric_limits::min() @@ -50,12 +86,17 @@ namespace isa { : prod); } + /** + * @brief 饱和除法。 + * @param a 被除数 + * @param b 除数 + * @return a/b;b==0 视为除以 1 返回 a;INT_MIN / -1 溢出饱和到 INT_MAX + * @details b==0 返回 a 是为避免未定义行为;除零的语义由 compiler/VM 层报错。 + */ inline INT sat_div(INT a, INT b) { - // b == 0 视为除以 1,避免未定义行为;除零的语义由 compiler/VM 层报错 if (b == 0) { return a; } - // INT_MIN / -1 溢出:饱和到 INT_MAX if (a == std::numeric_limits::min() && b == -1) { return std::numeric_limits::max(); } diff --git a/isa/src/Encode.cpp b/isa/src/Encode.cpp index fdd74a9..db74a9a 100644 --- a/isa/src/Encode.cpp +++ b/isa/src/Encode.cpp @@ -1,6 +1,6 @@ /** * @file Encode.cpp - * @brief 指令编解码与反汇编 + * @brief 指令编解码与反汇编实现 * @author * @date 2026-08-19 */ @@ -11,10 +11,20 @@ namespace isa { +/** + * @brief 编码:字段视图打包成一条 32 位指令字。 + * @param d 解码后的字段视图 + * @return 打包后的指令字;满足 encode(decode(w)) == w + */ Instr encode(Decoded d) { return pack(d.op, d.rd, d.a, d.b); } +/** + * @brief 解码:指令字拆成字段视图。 + * @param w 32 位指令字 + * @return 各字段视图 + */ Decoded decode(Instr w) { Decoded d; d.op = op(w); @@ -24,6 +34,14 @@ Decoded decode(Instr w) { return d; } +/** + * @brief 反汇编一条指令为一行文本。 + * @param w 指令字 + * @param out 输出缓冲 + * @param cap 缓冲容量(含 '\0');cap==0 时直接返回、不写 out + * @details 按操作数形态表驱动输出,文本与 Doc/isa/指令与映像.md 一致; + * 未知操作码输出 `??? 0x`。 + */ void disasm(Instr w, char* out, size_t cap) { if (cap == 0) { return; From fff7aef96877c2eb8d28f43657005cfd64f8a0ea Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 23:21:51 +0800 Subject: [PATCH 65/69] =?UTF-8?q?vm=20=E6=A8=A1=E5=9D=97=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E5=AE=8C=E5=96=84=EF=BC=9ADoxygen=20=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Image.h/.cpp:头 104 布局逐字段、SHA-256 增量状态机(update/process/final)、 校验项清单(from)、型号标识契约、func_row/const_entry/data_len 等访问器语义 - Machine.h/.cpp:Fault 枚举逐项、create 校验顺序(解析→SHA→型号)、 调用约定(r0 结果/r1..r7 实参)、exec_one 取指/计数/分发流程、 do_call/do_ret 帧语义、do_cal 八 FB 字段布局、slot_get/set 边界契约 --- vm/include/vm/Image.h | 130 +++++++++++++++++++++++++++++----------- vm/include/vm/Machine.h | 111 +++++++++++++++++++++------------- vm/src/Image.cpp | 100 ++++++++++++++++++++++++++++--- vm/src/Machine.cpp | 88 +++++++++++++++++++++++++-- 4 files changed, 341 insertions(+), 88 deletions(-) diff --git a/vm/include/vm/Image.h b/vm/include/vm/Image.h index e3dac36..33bb5bd 100644 --- a/vm/include/vm/Image.h +++ b/vm/include/vm/Image.h @@ -5,7 +5,7 @@ * @date 2026-08-21 * * @details 格式契约见 Doc/isa/指令与映像.md(12.13 修订:头 104 = 72 + 型号标识[32], - * 文件尾 SHA-256[32])。步骤 7:解析头与段;步骤 8:补型号匹配与 SHA-256 校验。 + * 文件尾 SHA-256[32])。解析头与段、补型号匹配与 SHA-256 校验都在本模块完成。 */ #pragma once @@ -17,71 +17,131 @@ namespace vm { - // 执行器内建支持型号(与 machine.toml [meta] 对齐;.stb 型号不匹配直接拒绝) + /// 执行器内建支持型号(与 machine.toml [meta] 对齐;.stb 型号不匹配直接拒绝)。 static const char* const kModelName = "STATOR"; + /// 执行器内建支持型号版本(与 machine.toml [meta] 对齐)。 static const uint32_t kModelVersion = 1; - // 映像头(字段与 Doc/isa/指令与映像.md 一致) + /** + * @brief 映像头字段(104 字节;字段顺序与 Doc/isa/指令与映像.md 一致)。 + * @details 全部为小端值;型号标识 32 字节 @72 不在此结构内(见 model_id())。 + */ struct ImageHeader { - uint32_t cycle_limit = 0; - uint32_t dt_ms = 0; - uint64_t project_hash = 0; - uint32_t entry_fn_id = 0; - uint32_t n_globals = 0; - uint32_t n_i = 0; - uint32_t n_q = 0; - uint32_t n_m = 0; - uint32_t n_consts = 0; - uint32_t n_funcs = 0; - uint32_t offset_const = 0; - uint32_t offset_funcs = 0; - uint32_t offset_code = 0; - uint32_t offset_fb = 0; - uint32_t offset_data = 0; + uint32_t cycle_limit = 0; ///< 每周期指令数上限(超出 → Fault::CycleLimit) + uint32_t dt_ms = 0; ///< 周期时长(毫秒,定时器/计数器推进步长) + uint64_t project_hash = 0; ///< 工程哈希(FNV-1a 64,只读展示) + uint32_t entry_fn_id = 0; ///< 入口函数(PROGRAM MAIN)在函数表的下标 + uint32_t n_globals = 0; ///< 全局变量槽数(数据段最前段) + uint32_t n_i = 0; ///< 输入变量(I)槽数 + uint32_t n_q = 0; ///< 输出变量(Q)槽数 + uint32_t n_m = 0; ///< 中间变量(M)槽数 + uint32_t n_consts = 0; ///< 常量表条目数 + uint32_t n_funcs = 0; ///< 函数表行数 + uint32_t offset_const = 0; ///< 常量表段起点(相对文件头) + uint32_t offset_funcs = 0; ///< 函数表段起点 + uint32_t offset_code = 0; ///< 字节码段起点 + uint32_t offset_fb = 0; ///< FB 表段起点 + uint32_t offset_data = 0; ///< 数据段起点(之后为 SHA-256 文件尾) }; + /** + * @brief 函数表一行(12 字节)。 + */ struct FuncRow { - uint32_t nregs = 0; - uint32_t code_offset = 0; // 相对字节码段起点(字节) - uint32_t code_len = 0; // 指令条数 + uint32_t nregs = 0; ///< 本函数寄存器数(函数头 nregs) + uint32_t code_offset = 0; ///< 相对字节码段起点(字节) + uint32_t code_len = 0; ///< 指令条数 }; + /** + * @brief 常量表一项(12 字节)。 + */ struct ConstEntry { - uint32_t tag = 0; - uint64_t value = 0; + uint32_t tag = 0; ///< 类型标记(isa::types::TypeTag:0=BOOL、1=INT、2=TIME) + uint64_t value = 0; ///< 值(LOADK 时按 tag 解释) }; - // 只读视图:校验过的映像(指向外部缓冲) + /** + * @brief 只读视图:指向外部缓冲的校验过的 .stb 映像。 + * @details from() 完成魔数/版本/头/各段/尺寸一致性校验;型号匹配与 SHA-256 + * 由 model_matches() / sha_ok() 另行查询(Machine::create 里一起做)。 + * 所有方法在校验失败(ok() == false)时返回安全默认值。 + */ class Image { public: + /** + * @brief 从原始字节构造只读视图(不拷贝,调用方保证生命周期)。 + * @param buf 映像缓冲;可为 nullptr + * @param len 缓冲字节数 + * @return 校验结果;ok() 判成功,error() 取失败原因 + * @details 校验项:非空、长度 ≥ 头、魔数 STSC、版本 1、五个段偏移单调且在 + * SHA-256 尾之前、常量表/函数表尺寸一致、代码段 4 字节对齐、 + * 入口 fn_id 在函数表内。 + */ static Image from(const uint8_t* buf, size_t len); + + /** + * @brief 从 vector 构造只读视图。 + * @param buf 映像缓冲(引用 data(),调用方保证生命周期) + * @return 同 from(const uint8_t*, size_t) + */ static Image from(const std::vector& buf); + /// @return 校验是否通过。 bool ok() const { return ok_; } + + /// @return 校验失败原因;成功时为空串。 const std::string& error() const { return err_; } + + /// @return 映像头字段。 const ImageHeader& header() const { return hdr_; } + /** + * @brief 读函数表一行。 + * @param i 函数下标 + * @return 该行字段;越界(或校验失败)时全 0 + */ FuncRow func_row(size_t i) const; + + /** + * @brief 读常量表一项。 + * @param i 常量下标 + * @return 该条目;越界(或校验失败)时全 0 + */ ConstEntry const_entry(size_t i) const; - const uint8_t* code_bytes() const; // 字节码段起点 - const uint8_t* data_bytes() const; // 数据段起点 - size_t data_len() const; // 字节数(不含 SHA 尾) + /// @return 字节码段起点;校验失败返回 nullptr。 + const uint8_t* code_bytes() const; - // 12.13:型号标识与 SHA-256 校验 + /// @return 数据段起点;校验失败返回 nullptr。 + const uint8_t* data_bytes() const; + + /// @return 数据段字节数(不含 SHA-256 尾);校验失败返回 0。 + size_t data_len() const; + + /// @return 型号标识字符串(头 @72 的 32 字节,去 '\0' 截断)。 std::string model_id() const; + + /** + * @brief 型号标识是否匹配。 + * @param name 期望型号名(如 "STATOR") + * @param version 期望版本(如 1) + * @return 头内 32 字节与 "name + version" 补零后逐字节相等 + */ bool model_matches(const std::string& name, uint32_t version) const; - bool sha_ok() const; // 文件尾 32 字节 SHA-256 校验 + + /// @return 文件尾 32 字节是否为内容(去尾后)的 SHA-256。 + bool sha_ok() const; public: - // 默认构造为无效态(ok() == false) + /// 默认构造为无效态(ok() == false)。 Image() : buf_(nullptr), len_(0) {} private: - const uint8_t* buf_; - size_t len_; - bool ok_ = false; - std::string err_; - ImageHeader hdr_; + const uint8_t* buf_; ///< 外部缓冲指针(不持有) + size_t len_; ///< 缓冲长度 + bool ok_ = false; ///< 校验是否通过 + std::string err_; ///< 失败原因 + ImageHeader hdr_; ///< 解析出的头字段 }; } diff --git a/vm/include/vm/Machine.h b/vm/include/vm/Machine.h index a3f4ba5..289b90d 100644 --- a/vm/include/vm/Machine.h +++ b/vm/include/vm/Machine.h @@ -5,7 +5,8 @@ * @date 2026-08-21 * * @details 设计说明(详见 Doc/vm/扫描周期.md 与 Doc/vm/指令执行.md): - * - 只认 isa 映像(ImageView);无 GC、无堆、无线程 + * - 只认 .stb 映像(vm 自带读实现;Machine::create 做型号/SHA-256 校验); + * 无 GC、无堆、无线程 * - 扫描周期:MAIN pc=0 → RET;MAIN 帧跨周期保留,调用栈深度上限 64 * - 帧:{ fn_id, regs[nregs], ret_pc, ret_fn_id };CALL 压帧复制 r0..r7, * RET 复制回调用方(r0=结果) @@ -24,30 +25,41 @@ namespace vm { - // 周期/指令故障 + /** + * @brief 周期/指令故障码。 + */ enum class Fault { - None, // 正常 - CycleLimit, // 本周期指令数超过映像头 cycle_limit(用例 6) - StackOverflow, // 调用栈深度超过 64 - BadOp, // 非法操作码(op ≥ 29) - BadSlot, // slot × 8 + 8 越出数据区 - BadConst, // const_id 越出常量表 + None, ///< 正常 + CycleLimit, ///< 本周期指令数超过映像头 cycle_limit(用例 6) + StackOverflow, ///< 调用栈深度超过 64 + BadOp, ///< 非法操作码 / 寄存器越界 / CALL 目标越出函数表 + BadSlot, ///< slot × 8 + 8 越出数据区 + BadConst, ///< const_id 越出常量表 }; + /** + * @brief 寄存器虚拟机:按扫描周期执行 .stb 映像。 + * @details 一个实例 = 一台机器:持有映像只读视图 + 数据区工作副本 + 调用栈。 + * MAIN 帧跨周期保留(PROGRAM 变量状态持久);每周期从 pc=0 执行到 + * MAIN 的 RET,期间调用栈可进可出(上限 64 帧)。 + * I 采样写入 / Q 读回由外部(executor)经 data() 完成,VM 只管数据区。 + */ class Machine { public: /** - * @brief 从映像字节构建机器(校验 + 拷贝数据段 + 建 MAIN 帧) + * @brief 从映像字节构建机器(校验 + 拷贝数据段 + 建 MAIN 帧)。 * @param image .stb 映像字节 * @param out 输出 Machine(未初始化) * @param err 错误输出;可为 nullptr(静默) * @return true 成功;false(映像非法,err 已写) + * @details 校验顺序:解析 .stb(Image::from)→ SHA-256 → 型号标识匹配 + * (内建 kModelName + kModelVersion),任一不符直接拒绝。 */ static bool create(const std::vector& image, Machine* out, std::string* err); /** - * @brief 跑一个扫描周期:MAIN pc=0 执行到 RET + * @brief 跑一个扫描周期:MAIN pc=0 执行到 RET。 * @return 故障(None = 正常完成);调用栈清空但 MAIN 帧保留 */ Fault run_cycle(); @@ -55,76 +67,93 @@ namespace vm { // ---- 可观察性(v1 预留,12.11 供单步/回放/TUI)---- /** - * @brief 执行一条指令,停在指令边界 + * @brief 执行一条指令,停在指令边界。 * @return true 继续;false 周期结束(MAIN 的 RET)或发生故障 */ bool step(); - /** @brief 最近一次故障(step 返回 false 后查询) */ + /// @brief 最近一次故障(step 返回 false 后查询)。 Fault fault() const; - /** @brief 周期是否已结束(MAIN 的 RET 后为 true) */ + /// @brief 周期是否已结束(MAIN 的 RET 后为 true)。 bool ended() const; - /** @brief 映像头(dt_ms / cycle_limit 等) */ + /// @brief 映像头(dt_ms / cycle_limit 等)。 const vm::ImageHeader& header() const; - /** @brief 当前指令下标(相对当前函数字节码段) */ + /// @brief 当前指令下标(相对当前函数字节码段)。 uint32_t pc() const; - /** @brief 当前函数 fn_id */ + /// @brief 当前函数 fn_id。 uint32_t fn_id() const; - /** @brief 本周期已执行指令数 */ + /// @brief 本周期已执行指令数。 uint32_t cycle_count() const; - /** @brief 调用栈深度(MAIN 帧 = 1) */ + /// @brief 调用栈深度(MAIN 帧 = 1)。 uint32_t call_depth() const; - /** @brief 数据区(I 采样写入 / Q 读回由外部做;8 字节定宽槽) */ + /// @brief 数据区(I 采样写入 / Q 读回由外部做;8 字节定宽槽)。 uint8_t* data(); const uint8_t* data() const; - /** @brief 数据区字节数 */ + /// @brief 数据区字节数。 size_t data_len() const; - /** @brief 当前帧寄存器值(i < nregs) */ + /// @brief 当前帧寄存器值(i < nregs)。 int64_t reg(uint8_t i) const; - /** @brief 当前帧寄存器数(函数头 nregs) */ + /// @brief 当前帧寄存器数(函数头 nregs)。 uint32_t nregs() const; - /** @brief 当前指令(pc_ 处;单步/观察用) */ + /// @brief 当前指令(pc_ 处;单步/观察用;pc 越界时返回 RET 哨兵)。 isa::Instr cur_instr() const; private: + /** + * @brief 调用帧。 + */ struct Frame { - uint32_t fn_id = 0; // 本帧函数 - std::vector regs; // 寄存器文件(大小 = 函数头 nregs) - uint32_t ret_pc = 0; // CALL 的下一条(非 MAIN 帧有意义) - uint32_t ret_fn_id = 0; // 返回目标函数 + uint32_t fn_id = 0; ///< 本帧函数 + std::vector regs; ///< 寄存器文件(大小 = 函数头 nregs) + uint32_t ret_pc = 0; ///< CALL 的下一条(非 MAIN 帧有意义) + uint32_t ret_fn_id = 0; ///< 返回目标函数 }; - vm::Image image_; // 映像只读视图(vm 自实现) - std::vector data_; // 数据区工作副本(8 字节定宽槽) - std::vector frames_; // 调用栈([0] = MAIN,跨周期保留) - // 边沿检测上次输入(每槽 2 字节:[slot*2] 第一边沿、[slot*2+1] 第二边沿; - // CTU/CTD/R_TRIG/F_TRIG 用第一、CTUD 用两个),跨周期保留 + vm::Image image_; ///< 映像只读视图(vm 自实现,指向外部缓冲) + std::vector data_; ///< 数据区工作副本(8 字节定宽槽) + std::vector frames_; ///< 调用栈([0] = MAIN,跨周期保留) + /** + * @brief 边沿检测上次输入(每槽 2 字节:[slot*2] 第一边沿、[slot*2+1] 第二边沿; + * CTU/CTD/R_TRIG/F_TRIG 用第一、CTUD 用两个),跨周期保留。 + */ std::vector edge_prev_; - uint32_t pc_ = 0; // 当前 pc(相对本帧函数字节码段) - uint32_t cycle_count_ = 0; // 本周期指令数 - Fault fault_ = Fault::None; // 最近一次故障 - bool ended_ = false; // step 后周期是否已结束 + uint32_t pc_ = 0; ///< 当前 pc(相对本帧函数字节码段) + uint32_t cycle_count_ = 0; ///< 本周期指令数 + Fault fault_ = Fault::None;///< 最近一次故障 + bool ended_ = false; ///< step 后周期是否已结束 + /// @brief 当前(栈顶)帧引用。 Frame& cur_frame(); + /// @brief 当前(栈顶)帧常量引用。 const Frame& cur_frame() const; - bool exec_one(); // 译码执行一条(12.9 步骤 2/3/4 填充) - Fault do_call(uint32_t fn_id); // 压帧;BadFn 用 BadOp 表示 - Fault do_ret(); // 弹帧 / 周期结束 - Fault do_cal(uint8_t op, uint16_t slot); // TON/TOF/CTU(12.9 步骤 4) - // 数据区槽读写(8 字节定宽;越界返回 false) + /// @brief 译码执行一条;false = 故障或周期结束。 + bool exec_one(); + + /// @brief 压帧(复制 r0..r7);fn_id 越界以 BadOp 表示。 + Fault do_call(uint32_t fn_id); + + /// @brief 弹帧 / 周期结束(复制 r0..r7 回调用方)。 + Fault do_ret(); + + /// @brief 执行内置 FB(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG)。 + Fault do_cal(uint8_t op, uint16_t slot); + + /// @brief 读数据区槽(8 字节定宽;越界返回 false)。 bool slot_get(uint16_t slot, int64_t* out) const; + + /// @brief 写数据区槽(8 字节定宽;越界返回 false)。 bool slot_set(uint16_t slot, int64_t v); }; } diff --git a/vm/src/Image.cpp b/vm/src/Image.cpp index d826c00..66bb53f 100644 --- a/vm/src/Image.cpp +++ b/vm/src/Image.cpp @@ -1,6 +1,6 @@ /** * @file Image.cpp - * @brief vm 自带的 .stb 只读视图(执行器侧) + * @brief vm 自带的 .stb 只读视图实现(执行器侧;compiler 各有实现) * @author * @date 2026-08-21 */ @@ -14,14 +14,22 @@ namespace vm { namespace { - const uint32_t kMagic = 0x43545353u; // "STSC" 小端 + /// 映像魔数:"STSC" 小端。 + const uint32_t kMagic = 0x43545353u; + /// 映像格式版本。 const uint32_t kVersion = 1; + /// 映像头字节数(72 原字段 + 型号标识[32] @72,12.13 修订)。 const size_t kHeaderSize = 104; + /// 常量表一行字节数(tag:4 + value:8)。 const size_t kConstEntrySize = 12; + /// 函数表一行字节数(nregs/code_offset/code_len 各 4)。 const size_t kFuncRowSize = 12; + /// SHA-256 摘要长度(文件尾)。 const size_t kSha256Size = 32; + /// 型号标识长度(头内 @72,不足补 '\0')。 const size_t kModelIdSize = 32; + /// 小端读 32 位。 uint32_t get_le32(const uint8_t* p) { return static_cast(p[0]) | (static_cast(p[1]) << 8) @@ -29,6 +37,7 @@ namespace { | (static_cast(p[3]) << 24); } + /// 小端读 64 位。 uint64_t get_le64(const uint8_t* p) { uint64_t v = 0; for (int i = 0; i < 8; ++i) { @@ -39,6 +48,7 @@ namespace { // ---- SHA-256(执行器侧实现,与 compiler 各一份)---- + /// SHA-256 轮常量 K[0..63]。 const uint32_t kShaK[64] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, @@ -53,15 +63,26 @@ namespace { 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, }; + /// 循环右移。 inline uint32_t rotr(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } + /** + * @brief SHA-256 增量状态机。 + * @details 标准 FIPS 180-4 实现:update() 吸收任意长度字节流,final() 输出 + * 32 字节大端摘要。按 64 字节块 process()。 + */ struct Sha256 { uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, - 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; - uint64_t total = 0; - uint8_t block[64]; - size_t block_len = 0; + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; ///< 初始哈希值 + uint64_t total = 0; ///< 已吸收字节数(final 时编码进长度域) + uint8_t block[64]; ///< 当前块缓冲 + size_t block_len = 0; ///< 块缓冲已用字节数 + /** + * @brief 吸收数据。 + * @param data 输入字节 + * @param len 字节数 + */ void update(const uint8_t* data, size_t len) { total += len; while (len > 0) { @@ -83,6 +104,7 @@ namespace { } } + /// 压缩一个满块(64 字节,w[0..63] 展开 + 64 轮)。 void process() { uint32_t w[64]; for (int i = 0; i < 16; ++i) { @@ -115,6 +137,10 @@ namespace { h[4] += e; h[5] += f; h[6] += g; h[7] += hh; } + /** + * @brief 结束并输出摘要。 + * @param out 32 字节输出缓冲(大端) + */ void final(uint8_t out[32]) { const uint64_t bitlen = total * 8; const uint8_t pad = 0x80; @@ -136,18 +162,31 @@ namespace { } } + /// 大端读 32 位。 static uint32_t get_be32(const uint8_t* p) { return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) | (static_cast(p[2]) << 8) | static_cast(p[3]); } }; + /** + * @brief 一次性 SHA-256。 + * @param data 输入字节 + * @param len 字节数 + * @param out 32 字节摘要输出 + */ void sha256(const uint8_t* data, size_t len, uint8_t out[32]) { Sha256 s; s.update(data, len); s.final(out); } + /** + * @brief 按型号标识约定填充 32 字节:name + version(如 "STATOR1"),余下补 '\0'。 + * @param name 型号名 + * @param version 版本号 + * @param out 32 字节输出缓冲 + */ void fill_model_id(const std::string& name, uint32_t version, char out[32]) { const std::string id = name + std::to_string(version); for (size_t i = 0; i < 32; ++i) { @@ -157,6 +196,12 @@ namespace { } // namespace +/** + * @brief 从原始字节构造只读视图(不拷贝,调用方保证生命周期)。 + * @param buf 映像缓冲;可为 nullptr + * @param len 缓冲字节数 + * @return 校验结果;ok() 判成功,error() 取失败原因 + */ Image Image::from(const uint8_t* buf, size_t len) { Image v; v.buf_ = buf; @@ -194,7 +239,7 @@ Image Image::from(const uint8_t* buf, size_t len) { h.offset_fb = get_le32(buf + 64); h.offset_data = get_le32(buf + 68); - // 段校验(数据段之后是 SHA-256 文件尾;步骤 8 做校验,这里仅保证有尾) + // 段校验(数据段之后是 SHA-256 文件尾;这里仅保证有尾,sha_ok() 做完整性校验) if (len < static_cast(h.offset_data) + kSha256Size) { v.err_ = "missing sha256 tail"; return v; @@ -232,10 +277,20 @@ Image Image::from(const uint8_t* buf, size_t len) { return v; } +/** + * @brief 从 vector 构造只读视图。 + * @param buf 映像缓冲(引用 data(),调用方保证生命周期) + * @return 同 from(const uint8_t*, size_t) + */ Image Image::from(const std::vector& buf) { return from(buf.data(), buf.size()); } +/** + * @brief 读函数表一行。 + * @param i 函数下标 + * @return 该行字段;越界(或校验失败)时全 0 + */ FuncRow Image::func_row(size_t i) const { FuncRow r; if (ok_ && i < hdr_.n_funcs) { @@ -247,6 +302,11 @@ FuncRow Image::func_row(size_t i) const { return r; } +/** + * @brief 读常量表一项。 + * @param i 常量下标 + * @return 该条目;越界(或校验失败)时全 0 + */ ConstEntry Image::const_entry(size_t i) const { ConstEntry e; if (ok_ && i < hdr_.n_consts) { @@ -257,18 +317,34 @@ ConstEntry Image::const_entry(size_t i) const { return e; } +/** + * @brief 字节码段起点。 + * @return 段指针;校验失败返回 nullptr + */ const uint8_t* Image::code_bytes() const { return ok_ ? buf_ + hdr_.offset_code : nullptr; } +/** + * @brief 数据段起点。 + * @return 段指针;校验失败返回 nullptr + */ const uint8_t* Image::data_bytes() const { return ok_ ? buf_ + hdr_.offset_data : nullptr; } +/** + * @brief 数据段字节数。 + * @return 段长度(不含 SHA-256 尾);校验失败返回 0 + */ size_t Image::data_len() const { return ok_ ? (len_ - kSha256Size) - hdr_.offset_data : 0; } +/** + * @brief 型号标识字符串。 + * @return 头 @72 起 32 字节,去 '\0' 截断;校验失败返回空串 + */ std::string Image::model_id() const { if (!ok_) { return ""; @@ -281,12 +357,22 @@ std::string Image::model_id() const { return s; } +/** + * @brief 型号标识是否匹配。 + * @param name 期望型号名(如 "STATOR") + * @param version 期望版本(如 1) + * @return 头内 32 字节与 "name + version" 补零后逐字节相等 + */ bool Image::model_matches(const std::string& name, uint32_t version) const { char want[kModelIdSize]; fill_model_id(name, version, want); return std::memcmp(buf_ + 72, want, kModelIdSize) == 0; } +/** + * @brief 文件尾 SHA-256 完整性校验。 + * @return 对 len-32 字节内容算摘要,与文件尾 32 字节相等 + */ bool Image::sha_ok() const { if (!ok_) { return false; diff --git a/vm/src/Machine.cpp b/vm/src/Machine.cpp index ab869cf..e247d31 100644 --- a/vm/src/Machine.cpp +++ b/vm/src/Machine.cpp @@ -1,15 +1,13 @@ /** * @file Machine.cpp - * @brief 寄存器虚拟机(12.9,步骤 2:译码 switch) + * @brief 寄存器虚拟机实现(12.9:译码 switch + CALL/RET 帧栈 + CAL_* 定时器) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/vm/指令执行.md): - * - 步骤 2 范围:create / run_cycle / step + 标量指令(MOVE/LOADK/NOT/AND/OR/算术/CMP)、 - * 跳转(JMP/JT/JF)、LOAD/STORE(slot × 8);RET 仅处理 MAIN(周期结束) - * - 步骤 3 补 CALL/RET 帧栈与栈深/坏操作码故障;步骤 4 补 CAL_* 定时器 * - 取指:函数表 → code_offset(字节)→ u32;执行后 pc 先 +1,跳转再 pc += off * - 数据区 8 字节定宽槽:偏移 = slot × 8 + * - 寄存器越界检查仅对把字段当寄存器的指令;a|b 作偏移/槽号/常量 id 时跳过 */ #include "vm/Machine.h" @@ -23,6 +21,16 @@ namespace vm { +/** + * @brief 从映像字节构建机器(校验 + 拷贝数据段 + 建 MAIN 帧)。 + * @param image .stb 映像字节 + * @param out 输出 Machine(未初始化) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(映像非法,err 已写) + * @details 校验顺序:解析 .stb(Image::from)→ SHA-256 → 型号标识匹配; + * 通过后拷贝数据段为工作副本,初始化边沿检测缓冲(每槽 2 字节), + * 建 MAIN 帧(跨周期保留)并把执行状态复位。 + */ bool Machine::create(const std::vector& image, Machine* out, std::string* err) { out->image_ = vm::Image::from(image); @@ -67,6 +75,12 @@ bool Machine::create(const std::vector& image, Machine* out, return true; } +/** + * @brief 跑一个扫描周期:MAIN pc=0 执行到 RET。 + * @return 故障(None = 正常完成) + * @details 循环执行 exec_one() 直到周期结束或故障;结束/故障后清空调用栈 + * (保留 MAIN 帧,PROGRAM 变量状态跨周期持久)。 + */ Fault Machine::run_cycle() { cycle_count_ = 0; ended_ = false; @@ -84,6 +98,10 @@ Fault Machine::run_cycle() { return fault_; } +/** + * @brief 执行一条指令,停在指令边界。 + * @return true 继续;false 周期结束(MAIN 的 RET)或发生故障 + */ bool Machine::step() { if (ended_ || fault_ != Fault::None) { return false; @@ -103,10 +121,19 @@ const uint8_t* Machine::data() const { return data_.data(); } size_t Machine::data_len() const { return data_.size(); } int64_t Machine::reg(uint8_t i) const { return cur_frame().regs[i]; } +/** + * @brief 当前帧寄存器数。 + * @return 栈顶帧 regs 大小(函数头 nregs) + */ uint32_t Machine::nregs() const { return static_cast(cur_frame().regs.size()); } +/** + * @brief 当前指令(pc_ 处)。 + * @return 指令字;pc_ 越出本函数字节码段时返回 RET 哨兵 + * @details 供单步/观察用;不修改任何执行状态。 + */ isa::Instr Machine::cur_instr() const { const vm::FuncRow row = image_.func_row(cur_frame().fn_id); const uint8_t* base = image_.code_bytes() + row.code_offset; @@ -116,9 +143,20 @@ isa::Instr Machine::cur_instr() const { return reinterpret_cast(base)[pc_]; } +/// @brief 当前(栈顶)帧引用。 Machine::Frame& Machine::cur_frame() { return frames_.back(); } + +/// @brief 当前(栈顶)帧常量引用。 const Machine::Frame& Machine::cur_frame() const { return frames_.back(); } +/** + * @brief 译码执行一条指令。 + * @return true 继续;false 故障(fault_ 已置)或周期结束 + * @details 流程:取指(越界 → BadOp)→ 周期计数(超 cycle_limit → CycleLimit)→ + * pc 先 +1(跳转按"相对下一条"叠加)→ 寄存器越界检查 → 按操作码分发。 + * 标量指令用 isa::types 饱和算术;LOAD/STORE 走 slot_get/slot_set; + * CALL/RET 走 do_call/do_ret;CAL_* 走 do_cal。 + */ bool Machine::exec_one() { // ---- 取指 ---- const vm::FuncRow row = image_.func_row(cur_frame().fn_id); @@ -180,10 +218,11 @@ bool Machine::exec_one() { return false; } const vm::ConstEntry c = image_.const_entry(cid); + // 按类型标记解释常量值:BOOL 归一化 0/1,INT 符号扩展,TIME 全 64 位 if (c.tag == isa::types::Bool) { regs[rd] = c.value ? 1 : 0; } else if (c.tag == isa::types::Int) { - regs[rd] = static_cast(c.value); // 符号扩展 + regs[rd] = static_cast(c.value); } else { regs[rd] = static_cast(c.value); } @@ -293,6 +332,12 @@ bool Machine::exec_one() { } } +/** + * @brief 读数据区槽(8 字节定宽,小端)。 + * @param slot 槽号 + * @param out 输出值 + * @return false = slot × 8 + 8 越出数据区 + */ bool Machine::slot_get(uint16_t slot, int64_t* out) const { const size_t off = static_cast(slot) * 8; if (off + 8 > data_.size()) { @@ -306,6 +351,12 @@ bool Machine::slot_get(uint16_t slot, int64_t* out) const { return true; } +/** + * @brief 写数据区槽(8 字节定宽,小端)。 + * @param slot 槽号 + * @param v 要写入的值 + * @return false = slot × 8 + 8 越出数据区 + */ bool Machine::slot_set(uint16_t slot, int64_t v) { const size_t off = static_cast(slot) * 8; if (off + 8 > data_.size()) { @@ -317,6 +368,14 @@ bool Machine::slot_set(uint16_t slot, int64_t v) { return true; } +/** + * @brief 调用:压新帧。 + * @param fn_id 目标函数(函数表下标) + * @return Fault(fn_id 越界 → BadOp;栈深 ≥ 64 → StackOverflow;成功 → None) + * @details 新帧寄存器清零,ret_pc = 当前 pc(已指向 CALL 下一条); + * 按调用约定把调用方 r0..r7 复制进新帧(实参已由调用点 MOVE 进 r1..r7); + * pc 复位到新函数起点。 + */ Fault Machine::do_call(uint32_t fn_id) { if (fn_id >= image_.header().n_funcs) { return Fault::BadOp; @@ -340,6 +399,13 @@ Fault Machine::do_call(uint32_t fn_id) { return Fault::None; } +/** + * @brief 返回:弹帧 / 周期结束。 + * @return Fault(成功 → None) + * @details MAIN 帧的 RET:ended_ = true(周期结束,不弹帧)。 + * 非 MAIN 帧:按调用约定把 r0..r7 复制回调用方(r0 = 结果,r1..r7 原样), + * pc 恢复为 ret_pc,弹帧。 + */ Fault Machine::do_ret() { if (frames_.size() <= 1) { ended_ = true; // MAIN 的 RET:周期结束 @@ -357,6 +423,18 @@ Fault Machine::do_ret() { return Fault::None; } +/** + * @brief 执行内置 FB(CAL_*)。 + * @param op 操作码(CAL_TON..CAL_F_TRIG,8 个) + * @param slot 实例起始槽号 + * @return Fault(槽越界 → BadSlot;非 CAL_* → BadOp;成功 → None) + * @details 内建 FB 布局冻结(Doc/compiler/符号表与链接.md): + * - TON/TOF/TP:in/pt/q/et(字段 0/1/2/3) + * - CTU:cu/r/pv/q/cv(0/1/2/3/4);CTD:cd/ld/pv/q/cv(同) + * - CTUD:cu/cd/r/lu/pv/qu/qd/cv(0..7) + * - R_TRIG / F_TRIG:clk/q(0/1) + * 字段偏移 = 字段序号 × 8(8 字节定宽槽);边沿存 edge_prev_(每槽 2 字节)。 + */ Fault Machine::do_cal(uint8_t op, uint16_t slot) { // 内建 FB 布局冻结(Doc/compiler/符号表与链接.md): // TON/TOF/TP:in/pt/q/et(字段 0/1/2/3) From b0aebe264e6f5a14a14ce778af499499ee7021f7 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 23:29:15 +0800 Subject: [PATCH 66/69] =?UTF-8?q?compiler=20=E6=A8=A1=E5=9D=97=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=AE=8C=E5=96=84=EF=BC=9ADoxygen=20=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=EF=BC=88Lexer/Parser/Linker/Typecheck/Project/Machine?= =?UTF-8?q?Config/TypeInfo/Codec/Codegen/Stb/main=20=E5=85=B1=2021=20?= =?UTF-8?q?=E4=B8=AA=E6=96=87=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compiler/include/compiler/Codec.h | 33 +++- compiler/include/compiler/Codegen.h | 17 +- compiler/include/compiler/Lexer.h | 158 +++++++++------- compiler/include/compiler/Linker.h | 115 ++++++++---- compiler/include/compiler/MachineConfig.h | 136 ++++++++++---- compiler/include/compiler/Parser.h | 197 ++++++++++++++------ compiler/include/compiler/Project.h | 75 +++++--- compiler/include/compiler/Stb.h | 148 ++++++++++++--- compiler/include/compiler/TypeInfo.h | 29 ++- compiler/include/compiler/Typecheck.h | 23 ++- compiler/src/Codec.cpp | 34 ++++ compiler/src/Codegen.cpp | 212 +++++++++++++++++++--- compiler/src/Lexer.cpp | 26 +-- compiler/src/Linker.cpp | 14 +- compiler/src/MachineConfig.cpp | 92 +++++++++- compiler/src/Parser.cpp | 19 +- compiler/src/Project.cpp | 182 +++++++++++++++++-- compiler/src/Stb.cpp | 111 ++++++++++- compiler/src/TypeInfo.cpp | 16 ++ compiler/src/Typecheck.cpp | 8 +- compiler/src/main.cpp | 28 ++- 21 files changed, 1332 insertions(+), 341 deletions(-) diff --git a/compiler/include/compiler/Codec.h b/compiler/include/compiler/Codec.h index f62abc7..3a38dfc 100644 --- a/compiler/include/compiler/Codec.h +++ b/compiler/include/compiler/Codec.h @@ -18,19 +18,42 @@ namespace compiler { - // 指令字:[op:8 | rd:8 | a:8 | b:8],小端 u32 + /// 指令字:`[ op:8 | rd:8 | a:8 | b:8 ]`,小端 u32。 typedef uint32_t Instr; + /** + * @brief 打包:opcode + 三个 8 位字段拼成一条指令字。 + * @param opcode 操作码(低 8 位) + * @param rd 目标寄存器 / 条件寄存器(第 8..15 位) + * @param a 源寄存器 / 立即数低 8 位 / 偏移低 8 位(第 16..23 位) + * @param b 源寄存器 / 立即数高 8 位 / 偏移高 8 位(第 24..31 位) + * @return 打包后的指令字(小端 u32) + * @details 布局与执行器 isa 一致,保证编译器产出的字节可被执行。 + */ Instr pack(uint8_t opcode, uint8_t rd, uint8_t a, uint8_t b); + /// @brief 取操作码(低 8 位)。 uint8_t op_of(Instr w); + /// @brief 取 rd 字段(第 8..15 位)。 uint8_t rd_of(Instr w); + /// @brief 取 a 字段(第 16..23 位)。 uint8_t a_of(Instr w); + /// @brief 取 b 字段(第 24..31 位)。 uint8_t b_of(Instr w); - uint16_t imm16_of(Instr w); // a|b 拼 u16(const_id / slot / fn_id) - int16_t off16_of(Instr w); // a|b 有符号偏移(相对下一条) + /// @brief a|b 拼 16 位无符号数(const_id / slot / fn_id)。 + uint16_t imm16_of(Instr w); + /// @brief a|b 为有符号相对偏移(单位:指令条数,相对下一条指令)。 + int16_t off16_of(Instr w); - // 配置驱动反汇编:按 machine.toml 的 format 输出一行文本。 - // 未知操作码 → "??? 0x"。输出格式与 Doc/isa/指令与映像.md 一致。 + /** + * @brief 配置驱动反汇编:按 machine.toml 的 format 输出一行文本。 + * @param cfg 机器配置(opcode 名 / format / 参数名来源) + * @param w 指令字 + * @param out 输出缓冲 + * @param cap 缓冲容量(含 '\0');cap==0 时直接返回、不写 out + * @details 文本格式与 Doc/isa/指令与映像.md 一致(如 `MOVE r1, r2`、 + * `LOADK r0, 3`、`JMP +4`、`JT r0, -1`、`CALL 1`、`CAL_TON 0`、`RET`); + * 未知操作码输出 `??? 0x`。 + */ void disasm(const MachineConfig& cfg, Instr w, char* out, size_t cap); } diff --git a/compiler/include/compiler/Codegen.h b/compiler/include/compiler/Codegen.h index 1312f20..1a52a78 100644 --- a/compiler/include/compiler/Codegen.h +++ b/compiler/include/compiler/Codegen.h @@ -3,6 +3,9 @@ * @brief 寄存器码生成(12.8,切片 1:MOVE / LOADK / RET) * @author * @date 2026-08-21 + * + * @details 代码生成:输入 Project + Unit(AST)+ LinkResult,输出 .stb 映像字节。 + * 寄存器分配:r0 结果、r1..r7 参数、r8+ 变量/临时;跳转偏移相对下一条。 */ #pragma once @@ -18,9 +21,17 @@ namespace compiler { - // 编译工程为 .stb 映像字节(在链接 + 类型检查成功后调用)。 - // 指令 opcode / 类型 tag / FB 布局全部来自 machine.toml(cfg)。 - // 失败返回 false,err 前缀 "codegen error"。 + /** + * @brief 编译工程为 .stb 映像字节(在链接 + 类型检查成功后调用)。 + * @param proj 工程定义(cycle_limit / dt_ms / 哈希) + * @param units 全部源文件的 AST + * @param link 链接结果(POU 顺序 / 全局符号 / FB 布局) + * @param cfg 机器定义(machine.toml;指令 opcode / 类型 tag / FB 布局) + * @param image 输出映像字节 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "codegen error") + * @details 指令 opcode / 类型 tag / FB 布局全部来自 machine.toml(cfg)。 + */ bool codegen_project(const Project& proj, const std::vector& units, const LinkResult& link, const MachineConfig& cfg, std::vector* image, std::string* err); diff --git a/compiler/include/compiler/Lexer.h b/compiler/include/compiler/Lexer.h index d66517f..371e085 100644 --- a/compiler/include/compiler/Lexer.h +++ b/compiler/include/compiler/Lexer.h @@ -1,6 +1,9 @@ /** * @file Lexer.h * @brief ST 子集词法分析 + * @details 本文件是词法层对外接口:定义 token 类型(Tok)、token 载体 + * (Token)与两个入口(lex / lex_file)。扫描器实现见 Lexer.cpp, + * 关键字与 token 集见 Doc/compiler/词法.md。 * @author * @date 2026-08-21 */ @@ -13,84 +16,105 @@ namespace compiler { - // token 类型。关键字与 token 集见 Doc/compiler/词法.md。 + /** + * @brief token 类型枚举。 + * @details 关键字与 token 集见 Doc/compiler/词法.md;关键字共 25 个, + * 与 12.1 冻结表一致(12.5 修订补入 THEN/DO)。顺序冻结后 + * 不得插值改动,只能追加到末尾。 + */ enum class Tok { // 关键字(25 个,与 12.1 冻结表一致) - PROGRAM, - FUNCTION, - FUNCTION_BLOCK, - END_PROGRAM, - END_FUNCTION, - END_FUNCTION_BLOCK, - VAR, - VAR_INPUT, - VAR_OUTPUT, - VAR_GLOBAL, - VAR_EXTERNAL, - END_VAR, - BOOL, - INT, - TIME, - IF, - ELSIF, - ELSE, - END_IF, - WHILE, - END_WHILE, - THEN, // 12.5 修订:IF/WHILE 语法需要,补入关键字表 - DO, // 同上 - AND, - OR, - NOT, - TRUE, - FALSE, - TON, - TOF, - TP, - CTU, - CTD, - CTUD, - R_TRIG, - F_TRIG, + PROGRAM, ///< PROGRAM 声明起始 + FUNCTION, ///< FUNCTION 声明起始 + FUNCTION_BLOCK, ///< FUNCTION_BLOCK 声明起始 + END_PROGRAM, ///< PROGRAM 结束 + END_FUNCTION, ///< FUNCTION 结束 + END_FUNCTION_BLOCK, ///< FUNCTION_BLOCK 结束 + VAR, ///< 局部变量段(VAR)起始 + VAR_INPUT, ///< 输入变量段起始 + VAR_OUTPUT, ///< 输出变量段起始 + VAR_GLOBAL, ///< 全局变量段起始 + VAR_EXTERNAL, ///< 外部变量段起始 + END_VAR, ///< 变量段结束 + BOOL, ///< 布尔类型 + INT, ///< 整数类型 + TIME, ///< 时间类型 + IF, ///< IF 关键字 + ELSIF, ///< ELSIF 关键字 + ELSE, ///< ELSE 关键字 + END_IF, ///< IF 结束 + WHILE, ///< WHILE 关键字 + END_WHILE, ///< WHILE 结束 + THEN, ///< IF/ELSIF 分支引导(12.5 修订补入关键字表) + DO, ///< WHILE 循环体引导(12.5 修订补入关键字表) + AND, ///< 逻辑与 + OR, ///< 逻辑或 + NOT, ///< 逻辑非(前缀) + TRUE, ///< 布尔真字面量 + FALSE, ///< 布尔假字面量 + TON, ///< 内建 FB 类型名 + TOF, ///< 内建 FB 类型名 + TP, ///< 内建 FB 类型名 + CTU, ///< 内建 FB 类型名 + CTD, ///< 内建 FB 类型名 + CTUD, ///< 内建 FB 类型名 + R_TRIG, ///< 内建 FB 类型名 + F_TRIG, ///< 内建 FB 类型名 // 字面量 - IDENT, // text = 小写名 - INT_LIT, // int_value - TIME_LIT, // int_value = 毫秒 + IDENT, ///< 标识符(text = 小写名) + INT_LIT, ///< 整数字面量(int_value) + TIME_LIT, ///< 时间字面量(int_value = 毫秒) // 符号 - ASSIGN, // := - EQ, // = - NE, // <> - LT, // < - LE, // <= - GT, // > - GE, // >= - PLUS, // + - MINUS, // - - STAR, // * - SLASH, // / - LPAREN, // ( - RPAREN, // ) - COMMA, // , - SEMI, // ; - COLON, // : - DOT, // . - END, // 文件结束 + ASSIGN, ///< :=(赋值) + EQ, ///< =(等于) + NE, ///< <>(不等) + LT, ///< <(小于) + LE, ///< <=(小于等于) + GT, ///< >(大于) + GE, ///< >=(大于等于) + PLUS, ///< +(加) + MINUS, ///< -(减) + STAR, ///< *(乘) + SLASH, ///< /(除) + LPAREN, ///< ((左括号) + RPAREN, ///< )(右括号) + COMMA, ///< ,(逗号) + SEMI, ///< ;(分号) + COLON, ///< :(冒号) + DOT, ///< .(点,FB 字段访问) + END, ///< 文件结束哨兵 }; + /** + * @brief 单个 token:类型 + 文本/字面量值 + 源位置。 + * @details line/col 从 1 起,记录 token 起始字符的位置。 + */ struct Token { - Tok type; - std::string text; // 标识符/关键字的小写文本;字面量原文 - int64_t int_value; // INT_LIT / TIME_LIT(毫秒) - std::string source_file; - uint32_t line; // 从 1 起 - uint32_t col; // 从 1 起 + Tok type; ///< token 类型 + std::string text; ///< 标识符/关键字的小写文本;字面量原文 + int64_t int_value; ///< INT_LIT / TIME_LIT(毫秒)的字面量值 + std::string source_file; ///< 来源文件名 + uint32_t line; ///< 起始行(从 1 起) + uint32_t col; ///< 起始列(从 1 起) }; - // 对源文本 lex 成 token 流(含末尾 END)。失败返回 false, - // err 前缀 "lex error",带文件与行列。 + /** + * @brief 对源文本做词法分析,产出 token 流(含末尾 END)。 + * @param source_file 源文件名(写入每个 token 的 source_file 并用于报错) + * @param content 源文本 + * @param out 输出 token 流 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "lex error",带文件与行列) + */ bool lex(const std::string& source_file, const std::string& content, std::vector* out, std::string* err); - // 读文件后 lex(文件不存在 / 读取失败也算 lex error) + /** + * @brief 读取文件后做词法分析。 + * @param path 文件路径 + * @param out 输出 token 流 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(文件不存在 / 读取失败也算 lex error) + */ bool lex_file(const std::string& path, std::vector* out, std::string* err); } diff --git a/compiler/include/compiler/Linker.h b/compiler/include/compiler/Linker.h index 34b4976..65338f1 100644 --- a/compiler/include/compiler/Linker.h +++ b/compiler/include/compiler/Linker.h @@ -1,6 +1,13 @@ /** * @file Linker.h * @brief 符号表与链接 + * @details 本文件定义链接阶段的输入 / 输出数据结构与对外接口(实现见 Linker.cpp): + * - Symbol / FbField / FbLayout / SourceUnit / LinkResult:符号与布局数据结构 + * - link_project:链接主入口(校验 GVL、编全局槽号、解析 VAR_EXTERNAL、 + * FB 实例与字段、函数循环调用检测、io 校验),失败 err 前缀 "link error" + * - load_unit:读取并解析一个 .st 文件成 SourceUnit + * - dump_symbols:符号表文本输出(全局表 + 各 POU 局部表) + * 流程与规则详见 Doc/compiler/符号表与链接.md。 * @author * @date 2026-08-21 */ @@ -17,66 +24,106 @@ namespace compiler { - // 符号种类(与 Doc/初步计划.md 12.1 一致) + /** + * @brief 符号种类(与 Doc/初步计划.md 12.1 一致)。 + * @details 各成员含义: + * - Global:全局变量(仅 gvl 顶层,编全局槽号) + * - External:VAR_EXTERNAL 引用(接全局同一槽,类型必须一致) + * - Local:POU 内部局部变量(12.8 分配槽) + * - Input / Output:POU 输入 / 输出参数 + * - Pou:POU 登记(PROGRAM / FUNCTION / FUNCTION_BLOCK) + * - FbInstance:FB 实例(带布局,内建或用户类型) + * - Const:常量 + */ enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const }; + /** + * @brief 一个符号(全局槽表项或 POU 局部符号)。 + */ struct Symbol { - SymbolKind kind = SymbolKind::Local; - std::string name; // 小写 - std::string type_name; // bool / int / time / fb 类型名(小写) - uint32_t address = 0; // 全局槽号(Global/External);局部暂 0(12.8 分配) - std::string source_file; - uint32_t line = 0; - uint32_t col = 0; + SymbolKind kind = SymbolKind::Local; ///< 符号种类 + std::string name; ///< 符号名(小写) + std::string type_name; ///< 类型名:bool / int / time / fb 类型名(小写) + uint32_t address = 0; ///< 全局槽号(Global/External);局部暂 0(12.8 分配) + std::string source_file; ///< 声明所在源文件 + uint32_t line = 0; ///< 声明行号 + uint32_t col = 0; ///< 声明列号 }; - // FB 类型字段(实例布局用;v1 字段类型仅 BOOL/INT/TIME) + /** + * @brief FB 类型字段(实例布局用;v1 字段类型仅 BOOL/INT/TIME)。 + */ struct FbField { - std::string name; - TypeKind type = TypeKind::Bool; + std::string name; ///< 字段名 + TypeKind type = TypeKind::Bool; ///< 字段类型(仅标量) }; + /** + * @brief 一个 FB 类型的字段布局(供实例分配与字段引用解析)。 + */ struct FbLayout { - std::string type_name; // 类型名(小写) - std::vector fields; // 段序:input → output → 内部 var + std::string type_name; ///< 类型名(小写) + std::vector fields; ///< 段序:input → output → 内部 var }; - // 一个源文件的 AST(供链接输入) + /** + * @brief 一个源文件的 AST(供链接输入)。 + */ struct SourceUnit { - std::string path; - Unit ast; + std::string path; ///< 源文件路径 + Unit ast; ///< 解析出的 AST }; - // 链接结果 + /** + * @brief 链接结果:全局槽表 + FB 类型布局 + 各 POU 作用域 + 函数表顺序。 + */ struct LinkResult { - // 全局槽表:下标即槽号(声明顺序) - std::vector globals; - std::map global_index; // 名 → 槽号 + std::vector globals; ///< 全局槽表:下标即槽号(声明顺序) + std::map global_index; ///< 名 → 槽号 - // 用户 FB 类型布局(按类型名) - std::map fb_types; + std::map fb_types; ///< 用户 FB 类型布局(按类型名) - // 每 POU 的局部符号与 FB 实例 + /** + * @brief 每 POU 的局部符号与 FB 实例。 + */ struct PouScope { - std::string name; - PouKind kind = PouKind::Program; - std::vector syms; // local/input/output/external/fb_instance - std::map fb_instances; // 实例名 → 布局 + std::string name; ///< POU 名 + PouKind kind = PouKind::Program; ///< POU 种类 + std::vector syms; ///< local/input/output/external/fb_instance + std::map fb_instances; ///< 实例名 → 布局 }; - std::vector scopes; + std::vector scopes; ///< 全部 POU 作用域 - // 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序) - std::vector fn_order; + std::vector fn_order; ///< 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序) }; - // 链接一个工程:校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、 - // 函数循环调用检测、io 校验。失败返回 false,err 前缀 "link error"。 + /** + * @brief 链接一个工程。 + * @details 校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、函数循环调用检测、 + * io 校验。失败返回 false,err 前缀 "link error"。 + * @param proj 工程定义(toml 解析结果) + * @param units 全部源文件的 AST(compile_files 集合逐一 load_unit 得到) + * @param out 链接结果(各字段先清空后填充) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 已写) + */ bool link_project(const Project& proj, const std::vector& units, LinkResult* out, std::string* err); - // 读取并解析一个 .st 文件成 SourceUnit + /** + * @brief 读取并解析一个 .st 文件成 SourceUnit。 + * @param path .st 文件路径 + * @param out 输出 SourceUnit(path + ast) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(透传 lex/syntax error) + */ bool load_unit(const std::string& path, SourceUnit* out, std::string* err); - // 符号表文本:每行 "name kind type address file"(全局表 + 各 POU 局部表) + /** + * @brief 符号表文本。 + * @details 每行 "name kind type address file"(全局表 + 各 POU 局部表)。 + * @param r 链接结果 + * @return 多行文本:全局表逐行;每个 POU 以 "[名字]" 开头后接其局部符号行 + */ std::string dump_symbols(const LinkResult& r); } diff --git a/compiler/include/compiler/MachineConfig.h b/compiler/include/compiler/MachineConfig.h index 52f7600..5bf8fce 100644 --- a/compiler/include/compiler/MachineConfig.h +++ b/compiler/include/compiler/MachineConfig.h @@ -19,74 +19,146 @@ namespace compiler { - // 内建基元(编译器内建,C 语义) + /** + * @brief 内建基元(编译器内建,C 语义)。 + * @details 元数据写在代码而非 machine.toml;配置类型的 base 必须命中本表。 + */ struct PrimType { - const char* name; - uint32_t width; // 字节 - bool is_signed; - bool is_float; + const char* name; ///< 基元名(bit / int8..uint64 / float32 / float64) + uint32_t width; ///< 宽度(字节) + bool is_signed; ///< 是否带符号 + bool is_float; ///< 是否浮点 }; - // 配置类型 = 基元别名(+ 值域约束 + .stb 契约 tag) + /** + * @brief 配置类型 = 基元别名(+ 值域约束 + .stb 契约 tag)。 + * @details 对应 machine.toml [[type]] 行:base 必为内建基元名;range 可选且 + * min ≤ max;tag 与 .stb 常量表契约一致(0=BOOL、1=INT、2=TIME,唯一)。 + */ struct ConfigType { - std::string name; - std::string base; - bool has_range = false; - int64_t range_min = 0; - int64_t range_max = 0; - uint32_t tag = 0; + std::string name; ///< 配置类型名(大写,如 BOOL/INT/TIME) + std::string base; ///< 基元名(命中内建基元表) + bool has_range = false; ///< 是否有值域约束 + int64_t range_min = 0; ///< 值域下界(含) + int64_t range_max = 0; ///< 值域上界(含) + uint32_t tag = 0; ///< .stb 常量表契约 tag }; - // 配置指令 + /** + * @brief 配置指令。 + * @details 对应 machine.toml [[op]] 行:opcode 唯一且 0..255;class 与 format + * 互锁(INSTANCE ↔ CAL);params 数量与 format 期望一致。 + */ struct ConfigOp { - std::string name; - uint32_t opcode = 0; - bool is_instance = false; // class: instance / plain - std::string format; // RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE - std::vector params; - bool enabled = false; + std::string name; ///< 助记符(与 .stb / isa 一致) + uint32_t opcode = 0; ///< 操作码(0..255,表内唯一) + bool is_instance = false; ///< class:instance / plain + std::string format; ///< 操作数形态:RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE + std::vector params; ///< 参数名(数量与 format 一致) + bool enabled = false; ///< 是否启用 }; + /** + * @brief 内建 FB 的一个字段。 + */ struct ConfigFbField { - std::string name; - std::string type; // 配置类型名(BOOL/INT/TIME) + std::string name; ///< 字段名(如 in/pt/q/et/cu/cv) + std::string type; ///< 配置类型名(BOOL/INT/TIME) }; + /** + * @brief 内建功能块(FB)布局登记。 + * @details 对应 machine.toml [[fb]] 行;opcode 必须命中 instance 类 op 行。 + */ struct ConfigFb { - std::string name; - uint32_t opcode = 0; - std::vector fields; + std::string name; ///< FB 名(小写,如 ton/ctu) + uint32_t opcode = 0; ///< 对应 op 行 opcode(CAL_*) + std::vector fields; ///< 字段列表(名称唯一,类型命中 type 表) }; + /** + * @brief 机器定义:加载 machine.toml 并做强校验。 + * @details 编译器指令集 / 类型的唯一事实来源;校验失败时 ok() 为 false, + * err 前缀 "machine error"。vm 不读配置(内建全指令集)。 + */ class MachineConfig { public: - // 加载 + 强校验;失败返回 false 并写 err(前缀 "machine error") + /** + * @brief 加载 + 强校验。 + * @param path machine.toml 路径 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err 已写,前缀 "machine error") + * @details 强校验项:字段必填 / type.base 命中内建基元·range 合法·tag 契约 / + * op opcode 唯一·类别-格式互锁 / fb.opcode 与 op 一致·字段类型命中 + * type 表(清单见文件头)。 + */ bool load(const std::string& path, std::string* err); + /// @brief 加载是否成功(校验通过)。 bool ok() const { return ok_; } + /// @brief 型号名(meta.name,参与 .stb 型号标识)。 const std::string& model_name() const { return model_name_; } + /// @brief 指令集版本(meta.version,参与 .stb 型号标识)。 uint32_t version() const { return version_; } + /// @brief 配置类型表([[type]])。 const std::vector& types() const { return types_; } + /// @brief 配置指令表([[op]])。 const std::vector& ops() const { return ops_; } + /// @brief 内建 FB 表([[fb]])。 const std::vector& fbs() const { return fbs_; } + /** + * @brief 按配置类型名查类型行。 + * @param name 配置类型名 + * @return 命中指针;未找到返回 nullptr + */ const ConfigType* find_type(const std::string& name) const; + + /** + * @brief 按助记符查指令行。 + * @param name 助记符(如 "MOVE") + * @return 命中指针;未找到返回 nullptr + */ const ConfigOp* find_op(const std::string& name) const; + + /** + * @brief 按 opcode 查指令行。 + * @param opcode 操作码(0..255) + * @return 命中指针;未找到返回 nullptr + */ const ConfigOp* find_op_by_code(uint32_t opcode) const; + + /** + * @brief 查询某 opcode 是否启用。 + * @param opcode 操作码 + * @return 指令存在且 enabled 为 true;未知 opcode 返回 false + */ bool op_enabled(uint32_t opcode) const; + + /** + * @brief 按名称查内建 FB。 + * @param name FB 名(小写,如 "ton") + * @return 命中指针;未找到返回 nullptr + */ const ConfigFb* find_fb(const std::string& name) const; - // 内建基元表 + /// @brief 内建基元表(bit / int8..uint64 / float32 / float64)。 static const std::vector& prims(); + + /** + * @brief 按名称查内建基元。 + * @param name 基元名 + * @return 命中指针;未找到返回 nullptr + */ static const PrimType* find_prim(const std::string& name); private: - bool ok_ = false; - std::string model_name_; - uint32_t version_ = 0; - std::vector types_; - std::vector ops_; - std::vector fbs_; + bool ok_ = false; ///< 加载成功标志 + std::string model_name_; ///< 型号名(meta.name) + uint32_t version_ = 0; ///< 指令集版本(meta.version) + std::vector types_; ///< 配置类型表 + std::vector ops_; ///< 配置指令表 + std::vector fbs_; ///< 内建 FB 表 }; } diff --git a/compiler/include/compiler/Parser.h b/compiler/include/compiler/Parser.h index 7295b86..24b7d8c 100644 --- a/compiler/include/compiler/Parser.h +++ b/compiler/include/compiler/Parser.h @@ -1,6 +1,9 @@ /** * @file Parser.h * @brief ST 子集递归下降语法分析(AST 定义 + 入口) + * @details 本文件定义语法层全部公开类型(AST 节点与相关枚举)与两个 + * 解析入口(parse_pous / parse_pous_file)。解析器实现见 + * Parser.cpp,文法详见 Doc/compiler/语法.md。 * @author * @date 2026-08-21 */ @@ -16,118 +19,196 @@ namespace compiler { // ---- 类型引用 ---- + /** + * @brief 类型种类。 + * @details 枚举值:Bool=内建 BOOL;Int=内建 INT;Time=内建 TIME; + * FbBuiltin=内建 FB 类型(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG); + * FbUser=用户 FB 类型(存在性由 12.6 链接层校验)。 + */ enum class TypeKind { Bool, Int, Time, FbBuiltin, FbUser }; + /** + * @brief 类型引用:种类 + (用户 FB 的)类型名。 + */ struct TypeRef { - TypeKind kind = TypeKind::Bool; - std::string name; // FbUser 时为用户 FB 类型名(小写) + TypeKind kind = TypeKind::Bool; ///< 类型种类 + std::string name; ///< FbUser 时为用户 FB 类型名(小写) }; // ---- 变量段 ---- + /** + * @brief 单个变量声明。 + * @details 初值可选(v1 仅字面量);line/col 记录声明处位置(报错用)。 + */ struct VarDecl { - std::string name; - TypeRef type; - bool has_init = false; // 可选初值(v1 仅字面量) - int64_t init_value = 0; // BOOL 0/1、INT 值、TIME 毫秒 - uint32_t line = 0; - uint32_t col = 0; + std::string name; ///< 变量名(小写) + TypeRef type; ///< 类型引用 + bool has_init = false; ///< 是否有可选初值(v1 仅字面量) + int64_t init_value = 0; ///< BOOL 0/1、INT 值、TIME 毫秒 + uint32_t line = 0; ///< 声明起始行 + uint32_t col = 0; ///< 声明起始列 }; + /** + * @brief 变量段种类。 + * @details 枚举值:Local=VAR、Input=VAR_INPUT、Output=VAR_OUTPUT、 + * Global=VAR_GLOBAL、External=VAR_EXTERNAL。 + */ enum class VarSection { Local, Input, Output, Global, External }; + /** + * @brief 一段 VAR_* … END_VAR:段种类 + 声明列表。 + */ struct VarBlock { - VarSection section = VarSection::Local; - std::vector vars; + VarSection section = VarSection::Local; ///< 段种类 + std::vector vars; ///< 段内声明(按出现顺序) }; // ---- 表达式 ---- + /** + * @brief 表达式节点种类。 + * @details 二目节点(And/Or/Cmp/Add/Sub/Mul/Div)用 lhs/rhs/op; + * 一元节点(Not/Neg)用 operand;AND/OR 的短路语义保留在 + * AST 中,由 12.8 编译为跳转。 + */ enum class ExprKind { - LitBool, // int_value 0/1 - LitInt, // int_value - LitTime, // int_value 毫秒 - VarRef, // name - Field, // name.field - Not, // operand - And, Or, // lhs, rhs(短路语义保留到 12.8) - Cmp, Add, Sub, Mul, Div, // lhs, rhs,op - Neg, // operand(一元负号) - Call, // name + args(函数调用,有返回值) + LitBool, ///< 布尔字面量(int_value 0/1) + LitInt, ///< 整数字面量(int_value) + LitTime, ///< 时间字面量(int_value 毫秒) + VarRef, ///< 变量引用(name) + Field, ///< FB 字段访问(name.field) + Not, ///< 逻辑非(operand) + And, Or, ///< 逻辑与 / 逻辑或(lhs, rhs;短路语义保留到 12.8) + Cmp, Add, Sub, Mul, Div, ///< 二目:比较 / 加减 / 乘除(lhs, rhs,op 为运算符) + Neg, ///< 一元负号(operand) + Call, ///< 函数调用(name + args,有返回值) }; + /** + * @brief 二目运算符种类。 + * @details 枚举值:Eq/Ne/Lt/Le/Gt/Ge=比较符(= <> < <= > >=); + * Add/Sub/Mul/Div=算术符(+ - * /)。 + */ enum class BinOp { Eq, Ne, Lt, Le, Gt, Ge, Add, Sub, Mul, Div }; + /** + * @brief 表达式 AST 节点(带 kind 标签的联合式结构)。 + * @details 按 kind 解释字段:字面量用 int_value;VarRef/Field/Call 用 + * name(+field、args);二目用 lhs/rhs/op;一元 Not/Neg 用 + * operand。 + */ struct Expr { - ExprKind kind = ExprKind::LitBool; - BinOp op = BinOp::Eq; // 二目运算 - std::string name; // VarRef / Field 对象 / Call 函数名 - std::string field; // Field 字段名 - int64_t int_value = 0; // 字面量值 - std::unique_ptr lhs; - std::unique_ptr rhs; - std::unique_ptr operand; // Not / Neg - std::vector> args; // Call 实参 + ExprKind kind = ExprKind::LitBool; ///< 节点种类 + BinOp op = BinOp::Eq; ///< 二目运算(Cmp/Add/Sub/Mul/Div 用) + std::string name; ///< VarRef / Field 对象 / Call 函数名 + std::string field; ///< Field 字段名 + int64_t int_value = 0; ///< 字面量值(BOOL 0/1、INT、TIME 毫秒) + std::unique_ptr lhs; ///< 二目左操作数 + std::unique_ptr rhs; ///< 二目右操作数 + std::unique_ptr operand; ///< Not / Neg 的操作数 + std::vector> args; ///< Call 实参 }; // ---- 语句 ---- + /** + * @brief 语句节点种类。 + * @details 枚举值:Assign=赋值;If=IF 语句(含 ELSIF/ELSE); + * While=WHILE 语句;FbCall=FB 调用语句(无返回值)。 + */ enum class StmtKind { Assign, If, While, FbCall }; - struct Stmt; // 前置声明(IfBranch / Stmt 相互引用) + /** @brief 语句节点(前置声明;Stmt 与 IfBranch 相互引用)。 */ + struct Stmt; + /** + * @brief FB 调用实参:命名参数(名 := 表达式)。 + */ struct FbArg { - std::string name; - std::unique_ptr value; + std::string name; ///< 形参名(小写) + std::unique_ptr value; ///< 实参表达式 }; + /** + * @brief IF 语句的一个 ELSIF 分支:条件 + 分支体。 + */ struct IfBranch { - std::unique_ptr cond; - std::vector body; + std::unique_ptr cond; ///< 分支条件 + std::vector body; ///< 分支体(条件成立时执行) }; - // 前置声明:Stmt 内部自引用 + /** + * @brief 语句 AST 节点:按 kind 解释各字段组。 + * @details Assign 用 target(+field);If/While 用 cond/body + * (+elsifs、else_body);FbCall 用 instance/args。 + * fb.field 左值 v1 禁止赋值,在语法层拒绝。 + */ struct Stmt { - StmtKind kind = StmtKind::Assign; + StmtKind kind = StmtKind::Assign; ///< 语句种类 // Assign - std::string target; // 左值标识符 - bool target_is_field = false; // 左值 fb.field(v1 禁止赋值,语法层拒绝) - std::string field; - std::unique_ptr value; + std::string target; ///< 左值标识符 + bool target_is_field = false; ///< 左值 fb.field(v1 禁止赋值,语法层拒绝) + std::string field; ///< 左值字段名 + std::unique_ptr value; ///< 右值表达式 // If / While - std::unique_ptr cond; - std::vector body; - std::vector elsifs; - std::vector else_body; + std::unique_ptr cond; ///< 条件表达式 + std::vector body; ///< 主分支体 + std::vector elsifs; ///< ELSIF 分支列表 + std::vector else_body; ///< ELSE 分支体 // FbCall - std::string instance; - std::vector args; + std::string instance; ///< FB 实例名 + std::vector args; ///< 命名实参列表 }; // ---- POU ---- + /** + * @brief POU 种类。 + * @details 枚举值:Program=PROGRAM;Function=FUNCTION(可有返回类型); + * FunctionBlock=FUNCTION_BLOCK。 + */ enum class PouKind { Program, Function, FunctionBlock }; + /** + * @brief 程序组织单元(POU):外壳 + 变量段 + 语句体。 + */ struct POU { - PouKind kind = PouKind::Program; - std::string name; - TypeRef result_type; // FUNCTION 才有 - std::vector blocks; - std::vector body; - std::string source_file; + PouKind kind = PouKind::Program; ///< POU 种类 + std::string name; ///< POU 名(小写) + TypeRef result_type; ///< 返回类型(仅 FUNCTION 才有) + std::vector blocks; ///< 变量段(按声明顺序) + std::vector body; ///< 语句体 + std::string source_file; ///< 来源文件名 }; - // 一个 .st 文件的解析结果:GVL 文件形态(无 POU)时只有 globals 非空 + /** + * @brief 一个 .st 文件的解析结果。 + * @details GVL 文件形态(无 POU)时只有 globals 非空。 + */ struct Unit { - std::vector globals; // 顶层 VAR_GLOBAL 段(仅 GVL 文件) - std::vector pous; + std::vector globals; ///< 顶层 VAR_GLOBAL 段(仅 GVL 文件) + std::vector pous; ///< POU 列表(按出现顺序) }; - // 解析一个 .st 文件的全部内容(顶层 VAR_GLOBAL 段或 POU)。 - // 失败返回 false,err 前缀 "syntax error",带文件与行列。 + /** + * @brief 解析一个 .st 文件的全部内容(顶层 VAR_GLOBAL 段或 POU)。 + * @param source_file 源文件名(报错用) + * @param content 源文本 + * @param out 输出 Unit(globals / pous) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "syntax error",带文件与行列) + */ bool parse_pous(const std::string& source_file, const std::string& content, Unit* out, std::string* err); - // 读文件后解析 + /** + * @brief 读取文件后解析。 + * @param path 文件路径 + * @param out 输出 Unit(globals / pous) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(文件打不开也算 syntax error) + */ bool parse_pous_file(const std::string& path, Unit* out, std::string* err); } diff --git a/compiler/include/compiler/Project.h b/compiler/include/compiler/Project.h index 81bbe4e..fe6882e 100644 --- a/compiler/include/compiler/Project.h +++ b/compiler/include/compiler/Project.h @@ -1,6 +1,13 @@ /** * @file Project.h * @brief 工程定义与 project.toml 解析(schema 校验) + * @details 本文件定义工程数据结构与解析接口(实现见 Project.cpp): + * - IoBinding / Project:project.toml 的解析结果(I/O 绑定不创造变量, + * slot 由链接结果解析) + * - parse_project:解析并校验 project.toml,错误带稳定类别前缀 + 行号 + * - compile_files:编译文件集合(files.st ∪ gvl.file,去重) + * - compute_project_hash:校验文件存在并计算工程哈希,缺文件前缀 "file missing" + * 字段约定与 Doc/初步计划.md 12.1 的 toml 字段表一一对应。 * @author * @date 2026-08-21 */ @@ -13,37 +20,61 @@ namespace compiler { - // I/O 绑定(project.toml [[io.*]];不创造变量,slot 由链接结果解析) + /** + * @brief I/O 绑定(project.toml [[io.*]];不创造变量,slot 由链接结果解析)。 + */ struct IoBinding { - std::string var; - uint32_t slot = 0; - uint32_t channel = 0; - uint32_t bit = 0; - bool is_input = false; // true = [[io.input]],false = [[io.output]] + std::string var; ///< 绑定的全局变量名(须已在 GVL 声明) + uint32_t slot = 0; ///< 全局槽号(12.6 链接阶段解析,此处填 0) + uint32_t channel = 0; ///< 通道号 + uint32_t bit = 0; ///< 位号 + bool is_input = false; ///< true = [[io.input]],false = [[io.output]] }; - // 与 Doc/初步计划.md 12.1 的 toml 字段表一一对应。 - // 字段白名单 / 必填 / io 完整性在 parse_project 内校验。 + /** + * @brief 工程定义。 + * @details 与 Doc/初步计划.md 12.1 的 toml 字段表一一对应; + * 字段白名单 / 必填 / io 完整性在 parse_project 内校验。 + */ struct Project { - std::string name; // [project] 必填 - std::string entry; // 必填,第一版必须 "program MAIN" - uint32_t cycle_limit = 0; // 必填 > 0 - uint32_t dt_ms = 0; // 必填 > 0 - std::vector files_st; // [files] 必填非空 - std::string gvl_file; // [gvl] 可选;无则空串 - std::vector io; // [[io.*]] 可选;slot 12.6 才解析,此处填 0 - std::string base_dir; // toml 所在目录(解析相对路径用) + std::string name; ///< [project] 必填 + std::string entry; ///< 必填,第一版必须 "program MAIN" + uint32_t cycle_limit = 0; ///< 必填 > 0 + uint32_t dt_ms = 0; ///< 必填 > 0 + std::vector files_st; ///< [files] 必填非空 + std::string gvl_file; ///< [gvl] 可选;无则空串 + std::vector io; ///< [[io.*]] 可选;slot 12.6 才解析,此处填 0 + std::string base_dir; ///< toml 所在目录(解析相对路径用) }; - // 解析并校验 project.toml。成功返回 true;失败返回 false 并写 err, - // err 带稳定类别前缀(unknown key / missing field / invalid value / bad entry)+ 行号。 + /** + * @brief 解析并校验 project.toml。 + * @details 成功返回 true;失败返回 false 并写 err,err 带稳定类别前缀 + * (parse error / unknown key / missing field / invalid value / bad entry) + * + 行号。 + * @param toml_path project.toml 路径 + * @param out 输出 Project(base_dir 先填,其余字段由各段解析填充) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 已写) + */ bool parse_project(const std::string& toml_path, Project* out, std::string* err); - // 编译文件集合:files.st ∪ gvl.file,去重(gvl 已在 files.st 则跳过)。 - // 保持 files.st 顺序,gvl 追加在后;相对路径以 base_dir 为基准解析。 + /** + * @brief 编译文件集合:files.st ∪ gvl.file,去重(gvl 已在 files.st 则跳过)。 + * @details 保持 files.st 顺序,gvl 追加在后;相对路径以 base_dir 为基准解析。 + * @param p 工程定义 + * @return 编译文件路径集合 + */ std::vector compile_files(const Project& p); - // 校验全部文件存在并计算工程哈希:集合按路径排序,对内容做 FNV-1a 64 增量; - // 空集合 = basis(Stb::kFnvBasis)。缺文件报错前缀 "file missing"。 + /** + * @brief 校验全部文件存在并计算工程哈希。 + * @details 集合按路径排序,对内容做 FNV-1a 64 增量;空集合 = basis(Stb::kFnvBasis)。 + * 缺文件报错前缀 "file missing"。 + * @param p 工程定义 + * @param hash 输出工程哈希 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 文件缺失(err 已写) + */ bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err); } diff --git a/compiler/include/compiler/Stb.h b/compiler/include/compiler/Stb.h index 6ee618f..0143493 100644 --- a/compiler/include/compiler/Stb.h +++ b/compiler/include/compiler/Stb.h @@ -5,7 +5,8 @@ * @date 2026-08-21 * * @details 执行器(vm)各有自己的读写实现;格式契约见 Doc/isa/指令与映像.md。 - * 12.13 修订(型号标识[32] 头 104 字节 + SHA-256 文件尾)在后续步骤落地。 + * 12.13 修订:头 104 = 原 72 + 型号标识[32] @72,文件尾 SHA-256[32]。 + * 常量表 tag 契约:0=BOOL、1=INT、2=TIME;工程哈希用 FNV-1a 64。 */ #pragma once @@ -20,97 +21,186 @@ namespace compiler { // ---- .stb 格式常量(契约;12.13 修订:头 104 = 原 72 + 型号标识[32],文件尾 SHA-256[32])---- - static const uint32_t kMagic = 0x43545353u; // "STSC" 小端 + /// 映像魔数:"STSC" 小端。 + static const uint32_t kMagic = 0x43545353u; + /// 映像格式版本。 static const uint32_t kVersion = 1; + /// 映像头字节数(原 72 字段 + 型号标识[32] @72)。 static const size_t kHeaderSize = 104; + /// 常量表一行字节数(tag:4 + value:8)。 static const size_t kConstEntrySize = 12; + /// 函数表一行字节数(nregs / code_offset / code_len 各 4)。 static const size_t kFuncRowSize = 12; + /// SHA-256 摘要长度(文件尾)。 static const size_t kSha256Size = 32; + /// 型号标识长度(头内 @72,不足补 '\0')。 static const size_t kModelIdSize = 32; + /// FNV-1a 64 哈希基值。 static const uint64_t kFnvBasis = 0xcbf29ce484222325ull; + /// FNV-1a 64 哈希素数。 static const uint64_t kFnvPrime = 0x100000001b3ull; - // 常量表一行(tag:0=BOOL、1=INT、2=TIME,契约) + /** + * @brief 常量表一行。 + * @details tag 契约:0=BOOL、1=INT、2=TIME。 + */ struct ConstEntry { - uint32_t tag = 0; - uint64_t value = 0; + uint32_t tag = 0; ///< 类型 tag(0=BOOL、1=INT、2=TIME) + uint64_t value = 0; ///< 常量值 }; - // FNV-1a 64(工程哈希,非密码学) + /** + * @brief FNV-1a 64 增量哈希更新(工程哈希,非密码学)。 + * @param h 当前哈希值(首轮传 kFnvBasis) + * @param data 输入字节 + * @param len 字节数 + * @return 更新后的哈希值 + */ uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len); + /** + * @brief FNV-1a 64 一次性哈希(工程哈希)。 + * @param data 输入字节 + * @param len 字节数 + * @return 哈希值 + */ uint64_t fnv1a64(const uint8_t* data, size_t len); - // SHA-256(文件完整性校验,非密码学用途之外的完整性) + /** + * @brief 一次性 SHA-256(文件完整性校验)。 + * @param data 输入字节 + * @param len 字节数 + * @param out 32 字节摘要输出(大端) + */ void sha256(const uint8_t* data, size_t len, uint8_t out[kSha256Size]); - // 型号标识:name + version 拼 32 字节 ASCII(如 "STATOR1"),不足补 '\0',超长截断 + /** + * @brief 型号标识:name + version 拼 32 字节 ASCII(如 "STATOR1")。 + * @param name 型号名 + * @param version 版本号 + * @param out 32 字节输出缓冲 + * @details 不足补 '\0',超长截断。 + */ void fill_model_id(const std::string& name, uint32_t version, char out[kModelIdSize]); - // 只读视图(--disasm 用;校验魔数/版本/段边界) + /** + * @brief .stb 映像只读视图(--disasm 用)。 + * @details from() 时校验魔数/版本/段边界,之后各访问器只读; + * 失败时 ok() == false,error() 取原因。 + */ class StbView { public: + /// @brief 函数表一行(nregs / code_offset / code_len)。 struct FuncRow { - uint32_t nregs = 0; - uint32_t code_offset = 0; - uint32_t code_len = 0; + uint32_t nregs = 0; ///< 帧寄存器数(含变量与临时) + uint32_t code_offset = 0; ///< 字节码段内偏移(4 字节对齐) + uint32_t code_len = 0; ///< 指令条数 }; + /// 从原始字节构造(不拷贝,调用方保证生命周期)。 static StbView from(const uint8_t* buf, size_t len); + /// 从 vector 构造(转发 from(buf.data(), buf.size()))。 static StbView from(const std::vector& buf); + /// 解析是否成功。 bool ok() const { return ok_; } + /// 失败原因(成功时为空串)。 const std::string& error() const { return err_; } + /// 周期上限(头 @8)。 uint32_t cycle_limit() const { return cycle_limit_; } + /// 扫描周期 dt_ms(头 @12)。 uint32_t dt_ms() const { return dt_ms_; } + /// 工程哈希(FNV-1a 64,头 @16)。 uint64_t project_hash() const { return project_hash_; } + /// 入口函数 fn_id(头 @24)。 uint32_t entry_fn_id() const { return entry_fn_id_; } + /// 全局槽数(头 @28)。 uint32_t n_globals() const { return n_globals_; } + /// 常量表行数(头 @44)。 uint32_t n_consts() const { return n_consts_; } + /// 函数表行数(头 @48)。 uint32_t n_funcs() const { return n_funcs_; } + /// 字节码段偏移(头 @60)。 uint32_t offset_code() const { return offset_code_; } + /// 数据段(FB 区)偏移(头 @64;即字节码段终点)。 uint32_t offset_fb() const { return offset_fb_; } + /// 数据段偏移(头 @68)。 uint32_t offset_data() const { return offset_data_; } + /// 读常量表一行;越界时返回全 0。 ConstEntry const_entry(size_t i) const; + /// 读函数表一行;越界时返回全 0。 FuncRow func_row(size_t i) const; + /// 字节码段起点。 const uint8_t* code_bytes() const; + /// 字节码段字节数。 size_t code_len() const; + /// 数据段起点。 const uint8_t* data_bytes() const; + /// 数据段字节数(不含文件尾 SHA-256)。 size_t data_len() const; - // 12.13:型号标识与 SHA-256 校验 + /// 型号标识字符串(头 72..103,截断到首个 '\0')。 std::string model_id() const; // 头 72..103(补 '\0' 后字符串) + /// 型号标识是否匹配 name + version。 bool model_matches(const std::string& name, uint32_t version) const; + /// 文件尾 32 字节 SHA-256 校验(对文件尾之前全部内容重算)。 bool sha_ok() const; // 文件尾 32 字节 SHA-256 校验 private: + /// 私有构造(只能经 from() 创建)。 StbView() : buf_(0), len_(0) {} + /// 常量表偏移(from 已校验的段起点)。 uint32_t offs_of_const() const; // offset_const(from 已校验段起点) - const uint8_t* buf_; - size_t len_; - bool ok_ = false; - std::string err_; - uint32_t cycle_limit_ = 0; - uint32_t dt_ms_ = 0; - uint64_t project_hash_ = 0; - uint32_t entry_fn_id_ = 0; - uint32_t n_globals_ = 0; - uint32_t n_consts_ = 0; - uint32_t n_funcs_ = 0; - uint32_t offset_code_ = 0; - uint32_t offset_fb_ = 0; - uint32_t offset_data_ = 0; + const uint8_t* buf_; ///< 映像缓冲(不拥有) + size_t len_; ///< 缓冲字节数 + bool ok_ = false; ///< 解析成功标志 + std::string err_; ///< 失败原因 + uint32_t cycle_limit_ = 0; ///< 周期上限 + uint32_t dt_ms_ = 0; ///< 扫描周期 + uint64_t project_hash_ = 0; ///< 工程哈希 + uint32_t entry_fn_id_ = 0; ///< 入口函数 fn_id + uint32_t n_globals_ = 0; ///< 全局槽数 + uint32_t n_consts_ = 0; ///< 常量表行数 + uint32_t n_funcs_ = 0; ///< 函数表行数 + uint32_t offset_code_ = 0; ///< 字节码段偏移 + uint32_t offset_fb_ = 0; ///< 数据段(FB 区)偏移 + uint32_t offset_data_ = 0; ///< 数据段偏移 }; - // 文件读写(纯字节;校验交给 StbView) + /** + * @brief 读整个文件为字节(纯字节;校验交给 StbView)。 + * @param path 文件路径 + * @param out 输出字节 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err "cannot open for read: ") + */ bool read_stb_file(const char* path, std::vector* out, std::string* err); + /** + * @brief 写字节为文件(纯字节)。 + * @param path 文件路径 + * @param img 映像字节 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err "cannot open for write: " / "write failed: ") + */ bool write_stb_file(const char* path, const std::vector& img, std::string* err); - // sidecar 生成与写文件(I/O 绑定 var → 槽号 → channel/bit) + /** + * @brief 生成 sidecar 文本(TOML)。 + * @param bindings I/O 绑定列表 + * @return TOML 文本(每个绑定一节 [[io.input]] / [[io.output]]) + * @details I/O 绑定 var → 槽号 → channel/bit(不进映像,执行器采样用)。 + */ std::string make_sidecar(const std::vector& bindings); + /** + * @brief 生成并写 sidecar 文件。 + * @param path 文件路径 + * @param bindings I/O 绑定列表 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err "cannot open for write: " / "write failed: ") + */ bool write_sidecar_file(const char* path, const std::vector& bindings, std::string* err); } diff --git a/compiler/include/compiler/TypeInfo.h b/compiler/include/compiler/TypeInfo.h index 45157a1..f71d912 100644 --- a/compiler/include/compiler/TypeInfo.h +++ b/compiler/include/compiler/TypeInfo.h @@ -18,24 +18,45 @@ namespace compiler { - // 语言类型元数据 + /** + * @brief 语言类型元数据。 + * @details 由配置类型行 + 内建基元合成;config 或 prim 未命中时 + * 对应访问器取安全默认值,operator bool 为 false。 + */ struct TypeMeta { - const ConfigType* config = nullptr; // 配置类型行(别名 + range + tag) - const PrimType* prim = nullptr; // 内建基元(宽度/符号/浮点) + const ConfigType* config = nullptr; ///< 配置类型行(别名 + range + tag) + const PrimType* prim = nullptr; ///< 内建基元(宽度/符号/浮点) + /// @brief 宽度(字节);无基元返回 0。 uint32_t width() const { return prim ? prim->width : 0; } + /// @brief 是否带符号;无基元返回 false。 bool is_signed() const { return prim ? prim->is_signed : false; } + /// @brief 是否浮点;无基元返回 false。 bool is_float() const { return prim ? prim->is_float : false; } + /// @brief .stb 契约 tag;无配置返回 0。 uint32_t tag() const { return config ? config->tag : 0; } + /// @brief 是否有值域约束;无配置返回 false。 bool has_range() const { return config ? config->has_range : false; } + /** + * @brief 值是否落在配置值域内。 + * @param v 待查值 + * @return 无配置或无 range 时为 true;否则 min ≤ v ≤ max + */ bool value_in_range(int64_t v) const { return !config || !config->has_range || (v >= config->range_min && v <= config->range_max); } + /// @brief 是否有效(config 与 prim 都已命中)。 explicit operator bool() const { return config != nullptr && prim != nullptr; } }; - // 按语言类型名查元数据(大小写不敏感;未找到返回空 TypeMeta) + /** + * @brief 按语言类型名查元数据。 + * @param cfg 机器配置(类型表来源) + * @param name 语言类型名 + * @return 对应 TypeMeta;未找到返回空 TypeMeta(operator bool 为 false) + * @details 查询大小写不敏感(配置名大写,Linker type_name 小写)。 + */ TypeMeta type_meta(const MachineConfig& cfg, const std::string& name); } diff --git a/compiler/include/compiler/Typecheck.h b/compiler/include/compiler/Typecheck.h index 0f40ce9..c8dfca4 100644 --- a/compiler/include/compiler/Typecheck.h +++ b/compiler/include/compiler/Typecheck.h @@ -1,6 +1,10 @@ /** * @file Typecheck.h * @brief 类型检查 + * @details 本文件定义类型检查的求值类型与对外入口(实现见 Typecheck.cpp): + * - TType:表达式求值类型(v1 三型,无隐式宽化) + * - check_project:对工程做类型检查(在链接成功后调用),失败 err 前缀 "type error" + * 规则详见 Doc/compiler/类型检查.md。 * @author * @date 2026-08-21 */ @@ -16,11 +20,24 @@ namespace compiler { - // 表达式求值类型(v1 三型,无隐式宽化) + /** + * @brief 表达式求值类型(v1 三型,无隐式宽化)。 + * @details 各成员含义: + * - Bool:布尔量(逻辑运算 / 条件表达式) + * - Int:整数(算术运算) + * - Time:时间量(仅比较,无算术);INT 与 TIME 不混用 + */ enum class TType { Bool, Int, Time }; - // 对工程做类型检查(在链接成功后调用)。 - // 规则见 Doc/compiler/类型检查.md。失败返回 false,err 前缀 "type error"。 + /** + * @brief 对工程做类型检查(在链接成功后调用)。 + * @details 规则见 Doc/compiler/类型检查.md。失败返回 false,err 前缀 "type error"。 + * @param proj 工程定义 + * @param units 全部源文件的 AST + * @param link 链接结果(符号 / 布局已解析,只读) + * @param err 错误输出;可为 nullptr(静默) + * @return true 全部通过;false 失败(err 已写) + */ bool check_project(const Project& proj, const std::vector& units, const LinkResult& link, std::string* err); } diff --git a/compiler/src/Codec.cpp b/compiler/src/Codec.cpp index 0947b4c..96f7c65 100644 --- a/compiler/src/Codec.cpp +++ b/compiler/src/Codec.cpp @@ -3,6 +3,9 @@ * @brief 指令字编解码 + 配置驱动反汇编(编译器侧) * @author * @date 2026-08-21 + * + * @details pack / op_of / imm16_of / off16_of 与执行器 isa 字节布局一致; + * disasm 由 MachineConfig 驱动(opcode 名 / format / 参数名来自配置)。 */ #include "compiler/Codec.h" @@ -11,6 +14,14 @@ namespace compiler { +/** + * @brief 打包:四个字段拼成一条指令字。 + * @param opcode 操作码(低 8 位) + * @param rd 目标寄存器 / 条件寄存器(第 8..15 位) + * @param a 源寄存器 / 立即数低 8 位 / 偏移低 8 位(第 16..23 位) + * @param b 源寄存器 / 立即数高 8 位 / 偏移高 8 位(第 24..31 位) + * @return 打包后的指令字(小端 u32) + */ Instr pack(uint8_t opcode, uint8_t rd, uint8_t a, uint8_t b) { return static_cast(opcode) | (static_cast(rd) << 8) @@ -18,17 +29,40 @@ Instr pack(uint8_t opcode, uint8_t rd, uint8_t a, uint8_t b) { | (static_cast(b) << 24); } +/// @brief 取操作码(低 8 位)。 uint8_t op_of(Instr w) { return static_cast(w & 0xFFu); } +/// @brief 取 rd 字段(第 8..15 位)。 uint8_t rd_of(Instr w) { return static_cast((w >> 8) & 0xFFu); } +/// @brief 取 a 字段(第 16..23 位)。 uint8_t a_of(Instr w) { return static_cast((w >> 16) & 0xFFu); } +/// @brief 取 b 字段(第 24..31 位)。 uint8_t b_of(Instr w) { return static_cast((w >> 24) & 0xFFu); } +/** + * @brief a|b 拼成 16 位无符号数。 + * @param w 指令字 + * @return 小端拼出的 16 位值(const_id / slot / fn_id) + */ uint16_t imm16_of(Instr w) { return static_cast(a_of(w) | (static_cast(b_of(w)) << 8)); } +/** + * @brief a|b 为有符号相对偏移。 + * @param w 指令字 + * @return 偏移量(单位:指令条数,相对下一条指令) + */ int16_t off16_of(Instr w) { return static_cast(imm16_of(w)); } +/** + * @brief 配置驱动反汇编:按 machine.toml 的 format 输出一行文本。 + * @param cfg 机器配置(opcode 名 / format / 参数名来源) + * @param w 指令字 + * @param out 输出缓冲 + * @param cap 缓冲容量(含 '\0');cap==0 时直接返回、不写 out + * @details 未知操作码输出 `??? 0x%08x`;format 分支 RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE + * 输出文本与 Doc/isa/指令与映像.md 一致。 + */ void disasm(const MachineConfig& cfg, Instr w, char* out, size_t cap) { if (cap == 0) { return; diff --git a/compiler/src/Codegen.cpp b/compiler/src/Codegen.cpp index a3b6584..721a6d2 100644 --- a/compiler/src/Codegen.cpp +++ b/compiler/src/Codegen.cpp @@ -48,7 +48,11 @@ namespace compiler { namespace { /** - * @brief 代码生成器 + * @brief 代码生成器(寄存器码)。 + * @details 流程:布局数据区 → 布局 FB 实例 → 逐 POU 建函数 → 拼映像。 + * 寄存器分配:r0 结果、r1..r7 参数(调用约定区,输入只读)、r8+ 变量/临时; + * 跳转偏移相对下一条(目标 = 当前 + 1 + off)。失败统一经 fail() 写 + * err(前缀 "codegen error")。 */ class Builder { public: @@ -107,6 +111,11 @@ namespace { } private: + /** + * @brief 记录错误并返回失败。 + * @param msg 错误消息(自动加前缀 "codegen error: ") + * @return 恒 false(便于 return fail(...) 连写) + */ bool fail(const std::string& msg) { if (err_) { *err_ = "codegen error: " + msg; @@ -125,32 +134,45 @@ namespace { } // ---- 指令发射(配置 opcode)---- + /// 双寄存器指令(rd, rs)。 Instr E_rr(const char* name, uint8_t rd, uint8_t rs) { return pack(opc(name), rd, rs, 0); } + /// 三寄存器指令(rd, ra, rb)。 Instr E_rrr(const char* name, uint8_t rd, uint8_t ra, uint8_t rb) { return pack(opc(name), rd, ra, rb); } + /// 立即数指令(imm 拆低/高字节入字段)。 Instr E_imm(const char* name, uint8_t rd, uint16_t imm) { return pack(opc(name), rd, static_cast(imm & 0xFFu), static_cast((imm >> 8) & 0xFFu)); } + /// 槽号指令(slot 复用 imm 字段,u16)。 Instr E_slot(const char* name, uint8_t rd, uint16_t slot) { return E_imm(name, rd, slot); } + /// 无条件跳转(off 相对下一条指令)。 Instr E_jmp(int16_t off) { const uint16_t u = static_cast(off); return pack(opc("JMP"), 0, static_cast(u & 0xFFu), static_cast((u >> 8) & 0xFFu)); } + /// 条件跳转(寄存器 r 为真则跳;off 相对下一条指令)。 Instr E_jc(const char* name, uint8_t r, int16_t off) { const uint16_t u = static_cast(off); return pack(opc(name), r, static_cast(u & 0xFFu), static_cast((u >> 8) & 0xFFu)); } + /// CALL 指令(fn_id 为函数表下标)。 Instr E_call(uint16_t fn_id) { return E_imm("CALL", 0, fn_id); } + /// RET 指令。 Instr E_ret() { return pack(opc("RET"), 0, 0, 0); } + /** + * @brief 按名查 POU AST。 + * @param name POU 名 + * @return 找到返回指针;否则 nullptr + */ const POU* find_pou(const std::string& name) const { for (const SourceUnit& u : units_) { for (const POU& p : u.ast.pous) { @@ -163,27 +185,41 @@ namespace { } // 每函数的编译态 + /** + * @brief 单个函数的编译状态。 + * @details 寄存器分配(调用约定):结果 r0、输入 r1..r7、变量与临时 r8+。 + */ struct FuncCtx { - std::string name; - std::string pou_name; // 所属 POU 名(实例查找键) - std::vector code; // 字节码 - std::map regs; // 变量名 → 帧寄存器 - uint8_t nlocals = 0; // 变量区终点 = 临时寄存器起始 - uint8_t nregs = 0; // 峰值(变量 + 临时) - uint8_t temp_used = 0; // 本语句已用临时数(语句结束清零) - bool is_function = false; // FUNCTION(结果 r0 / 输入只读) - std::string result_name; // FUNCTION 名(结果寄存器映射) + std::string name; ///< 函数名(函数表行用) + std::string pou_name; ///< 所属 POU 名(实例查找键) + std::vector code; ///< 字节码 + std::map regs; ///< 变量名 → 帧寄存器 + uint8_t nlocals = 0; ///< 变量区终点 = 临时寄存器起始 + uint8_t nregs = 0; ///< 寄存器峰值(变量 + 临时) + uint8_t temp_used = 0; ///< 本语句已用临时数(语句结束清零) + bool is_function = false; ///< FUNCTION(结果 r0 / 输入只读) + std::string result_name; ///< FUNCTION 名(结果寄存器映射) }; // FB 实例:字段名 → 数据区槽号 + /** + * @brief FB 实例布局。 + * @details 字段槽号 = 实例基槽 + 字段序号(跨周期持久)。 + */ struct InstFields { - std::map field_addr; - uint32_t base = 0; - std::string type_name; // 实例的 FB 类型名(内建 / 用户) + std::map field_addr; ///< 字段名 → 数据区槽号 + uint32_t base = 0; ///< 实例基槽 + std::string type_name; ///< 实例的 FB 类型名(内建 / 用户) }; + /// 语句开始:清零临时寄存器计数。 void begin_stmt(FuncCtx& f) { f.temp_used = 0; } + /** + * @brief 分配一个临时寄存器。 + * @param f 函数编译态 + * @return 临时寄存器号(nlocals + 已用数;随用抬高 nregs 峰值) + */ uint8_t alloc_temp(FuncCtx& f) { const uint8_t r = f.nlocals + f.temp_used; ++f.temp_used; @@ -248,6 +284,15 @@ namespace { return true; } + /** + * @brief 编译一条语句。 + * @param f 函数编译态 + * @param st 语句 AST + * @return true 成功;false(err 已写) + * @details 支持 IF / WHILE / FB 调用 / 赋值;其他语句报 + * "statement not supported"。内联 FB 体内左值可能是实例字段 + * (走 STORE_GLOBAL);写 FUNCTION 输入报 "cannot write function input"。 + */ bool compile_stmt(FuncCtx& f, const Stmt& st) { if (st.kind == StmtKind::If) { return compile_if(f, st); @@ -283,6 +328,15 @@ namespace { return compile_expr(f, *st.value, it->second); } + /** + * @brief 编译 FB 调用语句。 + * @param f 函数编译态 + * @param st FB 调用语句 AST + * @return true 成功;false(err 已写) + * @details 实参逐项编译后 STORE_GLOBAL 写实例字段;内建 FB + * 发 CAL_ <实例基槽>;用户 FB 调用点内联展开。 + * 错误:"no instance" / "unknown input" / "no FB type"。 + */ bool compile_fb_call(FuncCtx& f, const Stmt& st) { const InstFields* inst = instance_of(f.pou_name, st.instance); if (inst == nullptr) { @@ -315,6 +369,7 @@ namespace { return compile_fb_inline(f, *fb, *inst); } + /// 转大写(内建 FB 名 → 操作码名)。 static std::string uppercase_of(const std::string& s) { std::string out = s; for (char& ch : out) { @@ -323,6 +378,15 @@ namespace { return out; } + /** + * @brief 用户 FB 调用点内联展开。 + * @param f 函数编译态 + * @param fb 用户 FB 的 POU AST + * @param inst 实例布局 + * @return true 成功;false(err 已写) + * @details 临时置 inline_fields_ 使体内变量引用改查实例字段 + * (编译结束后恢复)。 + */ bool compile_fb_inline(FuncCtx& f, const POU& fb, const InstFields& inst) { const InstFields* saved = inline_fields_; inline_fields_ = &inst; @@ -336,6 +400,14 @@ namespace { return true; } + /** + * @brief 编译 WHILE 循环。 + * @param f 函数编译态 + * @param st WHILE 语句 AST + * @return true 成功;false(err 已写) + * @details 结构:条件 → JF 跳出 → 体 → JMP 回条件;偏移经 + * patch_jump 回填(相对下一条)。 + */ bool compile_while(FuncCtx& f, const Stmt& st) { const size_t loop = f.code.size(); const uint8_t t = alloc_temp(f); @@ -357,6 +429,14 @@ namespace { return true; } + /** + * @brief 编译 IF / ELSIF / ELSE。 + * @param f 函数编译态 + * @param st IF 语句 AST + * @return true 成功;false(err 已写) + * @details 每条分支:条件 → JF 跳下一条 → 体;分支间 JMP 跳 + * 公共结束点;偏移经 patch_jump 回填。 + */ bool compile_if(FuncCtx& f, const Stmt& st) { std::vector*>> branches; branches.push_back({st.cond.get(), &st.body}); @@ -401,6 +481,17 @@ namespace { return true; } + /** + * @brief 编译左值存储(全局槽)。 + * @param f 函数编译态 + * @param target 目标名 + * @param value 表达式 + * @return true 成功;false(err 已写) + * @details 目标槽号在 link_.global_index 中查;I/O 输入禁止写 + * ("cannot write to input");存储操作码经 store_name 选择 + * (I/O 输出 STORE_Q,其余 STORE_GLOBAL)。 + * 错误:"no storage for ..." / "cannot write to input ..."。 + */ bool store_target(FuncCtx& f, const std::string& target, const Expr& value) { const auto git = link_.global_index.find(target); if (git == link_.global_index.end()) { @@ -417,6 +508,18 @@ namespace { return true; } + /** + * @brief 编译表达式到目标寄存器。 + * @param f 函数编译态 + * @param e 表达式 AST + * @param rd 目标寄存器号 + * @return true 成功;false(err 已写) + * @details 支持:字面量(LOADK,tag 来自配置类型表)、变量/字段引用、 + * 四则、比较(cmp_name)、NOT、短路 AND/OR(JF/JT)、函数调用 + * (实参 MOVE 到 r1..r7 后 CALL,结果 MOVE 回 rd)。 + * 错误:"no register or slot" / "no instance" / "unknown field" / + * "too many arguments (max 7)" / "no fn_id" / "expression not supported"。 + */ bool compile_expr(FuncCtx& f, const Expr& e, uint8_t rd) { if (e.kind == ExprKind::LitBool || e.kind == ExprKind::LitInt || e.kind == ExprKind::LitTime) { @@ -533,6 +636,11 @@ namespace { return fail("expression not supported"); } + /** + * @brief 按名查 fn_id(函数表下标)。 + * @param name 函数名 + * @return 找到返回下标;否则 -1 + */ int find_fn_id(const std::string& name) const { for (size_t i = 0; i < link_.scopes.size(); ++i) { if (link_.scopes[i].name == name) { @@ -542,6 +650,14 @@ namespace { return -1; } + /** + * @brief 回填跳转偏移。 + * @param f 函数编译态 + * @param idx 跳转指令下标 + * @param target_idx 目标指令下标 + * @details 偏移相对下一条:目标 = 当前 + 1 + off(off 为 + * int16,重写指令的低 16 位)。 + */ void patch_jump(FuncCtx& f, size_t idx, size_t target_idx) { const int16_t off = static_cast( static_cast(target_idx) - (static_cast(idx) + 1)); @@ -552,6 +668,7 @@ namespace { } // ---- 操作码名(MachineConfig 已强校验存在)---- + /// 四则运算操作码名(Add→"ADD" 等;未知回退 "ADD")。 static const char* arith_name(ExprKind k) { switch (k) { case ExprKind::Add: return "ADD"; @@ -561,6 +678,7 @@ namespace { default: return "ADD"; } } + /// 比较操作码名(Eq→"CMP_EQ" 等;未知回退 "CMP_EQ")。 static const char* cmp_name(BinOp op) { switch (op) { case BinOp::Eq: return "CMP_EQ"; @@ -572,13 +690,23 @@ namespace { default: return "CMP_EQ"; } } + /// 加载操作码选择:I/O 输入走 LOAD_I,其余 LOAD_GLOBAL。 const char* load_name(const std::string& name) const { return io_input_.count(name) ? "LOAD_I" : "LOAD_GLOBAL"; } + /// 存储操作码选择:I/O 输出走 STORE_Q,其余 STORE_GLOBAL。 const char* store_name(const std::string& name) const { return io_output_.count(name) ? "STORE_Q" : "STORE_GLOBAL"; } + /** + * @brief 布局全局数据区。 + * @return true 成功;false(err 已写) + * @details 每槽 8 字节定宽小端,槽号 × 8 定位;初值按类型元数据 + * 写(宽度 2 写 u16、8 写 u64、否则写 0/1;别名先经 type_meta 解析)。 + * 错误:"data area exceeds slot range" / "no type in machine.toml" / + * "float initializer not supported yet"。 + */ bool layout_data() { for (const Symbol& s : link_.globals) { if (s.address > 0xFFFF) { @@ -614,6 +742,12 @@ namespace { return true; } + /** + * @brief 布局 FB 实例块(全局区之后顺序追加)。 + * @return true 成功;false(err 已写) + * @details 实例 key 为 "POU名/实例名";字段槽号 = 基槽 + 字段序号。 + * 错误:"no layout for instance ..."。 + */ bool layout_fb_instances() { uint32_t cur = static_cast(data_.size() / 8); bool any = false; @@ -643,12 +777,19 @@ namespace { return true; } + /// 按 "POU名/实例名" 查实例布局;找不到返回 nullptr。 const InstFields* instance_of(const std::string& pou, const std::string& name) const { const auto it = instances_.find(pou + "/" + name); return it == instances_.end() ? nullptr : &it->second; } + /** + * @brief 查全局变量初值(源文件 globals 段)。 + * @param name 变量名 + * @param has_init 输出:是否有初值 + * @param init 输出:初值 + */ void init_of(const std::string& name, bool* has_init, int64_t* init) const { for (const SourceUnit& u : units_) { for (const VarBlock& b : u.ast.globals) { @@ -683,6 +824,13 @@ namespace { return static_cast(consts_.size() - 1); } + /** + * @brief 拼装最终映像。 + * @return true 成功;false(err 已写) + * @details 布局:头(104) + 常量表 + 函数表 + 字节码 + 数据段 + + * SHA-256 文件尾;型号标识 @72;头内各段偏移(52..68)与函数表 + * code_offset 小端写入;SHA-256 对文件尾之前全部内容计算。 + */ bool assemble_image() { const uint32_t off_const = static_cast(kHeaderSize); const uint32_t off_funcs = off_const + @@ -774,12 +922,14 @@ namespace { return true; } + /// 小端写 32 位。 static void put_le32(std::vector& b, size_t off, uint32_t v) { b[off + 0] = static_cast(v & 0xFFu); b[off + 1] = static_cast((v >> 8) & 0xFFu); b[off + 2] = static_cast((v >> 16) & 0xFFu); b[off + 3] = static_cast((v >> 24) & 0xFFu); } + /// 小端写 64 位。 static void put_le64(std::vector& b, size_t off, uint64_t v) { for (int i = 0; i < 8; ++i) { b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); @@ -787,23 +937,33 @@ namespace { } // ---- 成员 ---- - const Project& proj_; - const std::vector& units_; - const LinkResult& link_; - const MachineConfig& cfg_; - std::vector* image_; - std::string* err_; - std::vector funcs_; - std::vector consts_; - std::map io_input_; - std::map io_output_; - std::vector data_; - std::map instances_; - const InstFields* inline_fields_ = nullptr; + const Project& proj_; ///< 工程定义(cycle_limit / dt_ms / 哈希) + const std::vector& units_; ///< 全部源文件 AST + const LinkResult& link_; ///< 链接结果(POU 顺序 / 全局符号 / FB 布局) + const MachineConfig& cfg_; ///< 机器定义(opcode / tag / FB 布局) + std::vector* image_; ///< 输出映像字节 + std::string* err_; ///< 错误输出(可为 nullptr) + std::vector funcs_; ///< 已编译函数 + std::vector consts_; ///< 常量表(tag + value) + std::map io_input_; ///< I/O 输入变量名集合(小写;只影响操作码选择) + std::map io_output_; ///< I/O 输出变量名集合(小写;只影响操作码选择) + std::vector data_; ///< 全局数据区字节(每槽 8 字节定宽) + std::map instances_; ///< "POU名/实例名" → 实例布局 + const InstFields* inline_fields_ = nullptr; ///< 内联 FB 字段表(非空 = 正在内联 FB 体) }; } // namespace +/** + * @brief 编译工程为 .stb 映像字节(对外入口)。 + * @param proj 工程定义 + * @param units 全部源文件的 AST + * @param link 链接结果 + * @param cfg 机器定义(machine.toml) + * @param image 输出映像字节 + * @param err 错误输出;可为 nullptr + * @return true 成功;false(err 前缀 "codegen error") + */ bool codegen_project(const Project& proj, const std::vector& units, const LinkResult& link, const MachineConfig& cfg, std::vector* image, std::string* err) { diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index 19ed538..98e3ee9 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -37,9 +37,11 @@ namespace compiler { namespace { - // 关键字表:小写键 → Tok。 - // 与 Doc/compiler/词法.md 的冻结表一致(12.5 修订补入 then/do); - // 数值即 token 类型,只追加不删改。 + /** + * @brief 关键字表:小写键 → Tok。 + * @details 与 Doc/compiler/词法.md 的冻结表一致(12.5 修订补入 + * then/do);数值即 token 类型,只追加不删改。 + */ const struct { const char* key; Tok tok; @@ -433,15 +435,15 @@ namespace { } // ---- 状态 ---- - const std::string& src_; // 源文本(外部所有,不拷贝) - const std::string& sf_; // 源文件名(报错用) - std::vector* out_; // token 流输出 - std::string* err_; // 错误输出(可空) - size_t pos_; // 当前字符下标 - uint32_t line_; // 当前行(1 起) - uint32_t col_; // 当前列(1 起) - uint32_t tok_line_ = 1; // 本 token 起始行(push 时用) - uint32_t tok_col_ = 1; // 本 token 起始列(push 时用) + const std::string& src_; ///< 源文本(外部所有,不拷贝) + const std::string& sf_; ///< 源文件名(报错用) + std::vector* out_; ///< token 流输出 + std::string* err_; ///< 错误输出(可空) + size_t pos_; ///< 当前字符下标 + uint32_t line_; ///< 当前行(1 起) + uint32_t col_; ///< 当前列(1 起) + uint32_t tok_line_ = 1; ///< 本 token 起始行(push 时用) + uint32_t tok_col_ = 1; ///< 本 token 起始列(push 时用) }; } // namespace diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp index 4bc9969..54a0a30 100644 --- a/compiler/src/Linker.cpp +++ b/compiler/src/Linker.cpp @@ -96,7 +96,7 @@ namespace { return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time; } - // 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md;12.11 扩为 8 个) + /// 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md;12.11 扩为 8 个) const FbLayout kTonLayout{"ton", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, {"q", TypeKind::Bool}, {"et", TypeKind::Time}}}; const FbLayout kTofLayout{"tof", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time}, @@ -691,12 +691,12 @@ namespace { } // ---- 成员 ---- - const Project& proj_; // 工程定义(toml) - const std::vector& units_; // 全部源文件 AST - LinkResult* out_; // 链接结果 - std::string* err_; // 错误输出(可空) - std::filesystem::path gvl_path_; // 规范化后的 gvl 路径 - std::map> call_edges_; // 调用者 → 被调函数集 + const Project& proj_; ///< 工程定义(toml) + const std::vector& units_; ///< 全部源文件 AST + LinkResult* out_; ///< 链接结果 + std::string* err_; ///< 错误输出(可空) + std::filesystem::path gvl_path_; ///< 规范化后的 gvl 路径 + std::map> call_edges_; ///< 调用者 → 被调函数集 }; } // namespace diff --git a/compiler/src/MachineConfig.cpp b/compiler/src/MachineConfig.cpp index f3c000a..461dece 100644 --- a/compiler/src/MachineConfig.cpp +++ b/compiler/src/MachineConfig.cpp @@ -22,6 +22,10 @@ namespace compiler { +/** + * @brief 内建基元表。 + * @return 静态基元表(bit / int8..uint64 / float32 / float64,元数据在代码) + */ const std::vector& MachineConfig::prims() { static const std::vector kPrims = { {"bit", 1, false, false}, @@ -39,6 +43,11 @@ const std::vector& MachineConfig::prims() { return kPrims; } +/** + * @brief 按名称查内建基元。 + * @param name 基元名 + * @return 命中指针;未找到返回 nullptr + */ const PrimType* MachineConfig::find_prim(const std::string& name) { for (const PrimType& p : prims()) { if (p.name == name) { @@ -50,7 +59,12 @@ const PrimType* MachineConfig::find_prim(const std::string& name) { namespace { - // 稳定前缀 + /** + * @brief 写错误信息(稳定前缀 "machine error")并原样返回 msg。 + * @param err 错误输出;可为 nullptr(静默) + * @param msg 错误描述 + * @return msg(供调用方直接 return) + */ std::string fail(std::string* err, const std::string& msg) { if (err) { *err = "machine error: " + msg; @@ -58,7 +72,11 @@ namespace { return msg; } - // format → 参数数量(解析时校验) + /** + * @brief format → 期望参数数量(解析时校验)。 + * @param format 操作数形态(RR/RRR/IMM/SLOT/JMP/JC/CALL/CAL/NONE) + * @return 参数数量;未知 format 返回 -1 + */ int params_of(const std::string& format) { if (format == "RR") return 2; if (format == "RRR") return 3; @@ -72,15 +90,25 @@ namespace { return -1; } + /// @brief format 是否已知(params_of 返回非负)。 bool is_known_format(const std::string& f) { return params_of(f) >= 0; } + /// @brief tag 是否在 .stb 常量表契约内(0=BOOL、1=INT、2=TIME)。 bool is_known_tag(uint32_t tag) { return tag <= 2; // .stb 常量表契约:0=BOOL 1=INT 2=TIME } - // 必填字符串 + /** + * @brief 读必填字符串字段。 + * @param t 配置表 + * @param what 表归属描述(用于错误信息,如 "[meta]") + * @param key 字段名 + * @param out 输出值 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(字段缺失或类型不符,err 已写 "machine error: ...") + */ bool req_string(const toml::table& t, const char* what, const std::string& key, std::string* out, std::string* err) { const auto nv = t[key]; @@ -92,7 +120,15 @@ namespace { return true; } - // 必填整数 + /** + * @brief 读必填整数字段。 + * @param t 配置表 + * @param what 表归属描述(用于错误信息,如 "[meta]") + * @param key 字段名 + * @param out 输出值 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(字段缺失或类型不符,err 已写 "machine error: ...") + */ bool req_int(const toml::table& t, const char* what, const std::string& key, int64_t* out, std::string* err) { const auto nv = t[key]; @@ -104,7 +140,15 @@ namespace { return true; } - // 必填布尔 + /** + * @brief 读必填布尔字段。 + * @param t 配置表 + * @param what 表归属描述(用于错误信息,如 "[meta]") + * @param key 字段名 + * @param out 输出值 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(字段缺失或类型不符,err 已写 "machine error: ...") + */ bool req_bool(const toml::table& t, const char* what, const std::string& key, bool* out, std::string* err) { const auto nv = t[key]; @@ -118,6 +162,19 @@ namespace { } // namespace +/** + * @brief 加载 machine.toml 并做强校验。 + * @param path machine.toml 路径 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err 已写,前缀 "machine error") + * @details 校验顺序与文件头清单 1~5 一致: + * 1. TOML 语法与字段完整性(meta/type/op/fb 全属性必填) + * 2. type.base 命中内建基元;range 合法(min ≤ max) + * 3. tag 与 .stb 常量表契约一致(BOOL=0、INT=1、TIME=2,唯一) + * 4. op.opcode 唯一、0..255;class/format 合法;类别-格式互锁(INSTANCE ↔ CAL); + * params 数量与 format 一致 + * 5. fb.opcode 与 op 行(instance 类)一致;字段类型命中配置类型表;字段名唯一 + */ bool MachineConfig::load(const std::string& path, std::string* err) { ok_ = false; model_name_.clear(); @@ -352,6 +409,11 @@ bool MachineConfig::load(const std::string& path, std::string* err) { return true; } +/** + * @brief 按配置类型名查类型行。 + * @param name 配置类型名 + * @return 命中指针;未找到返回 nullptr + */ const ConfigType* MachineConfig::find_type(const std::string& name) const { for (const ConfigType& t : types_) { if (t.name == name) { @@ -361,6 +423,11 @@ const ConfigType* MachineConfig::find_type(const std::string& name) const { return nullptr; } +/** + * @brief 按助记符查指令行。 + * @param name 助记符(如 "MOVE") + * @return 命中指针;未找到返回 nullptr + */ const ConfigOp* MachineConfig::find_op(const std::string& name) const { for (const ConfigOp& o : ops_) { if (o.name == name) { @@ -370,6 +437,11 @@ const ConfigOp* MachineConfig::find_op(const std::string& name) const { return nullptr; } +/** + * @brief 按 opcode 查指令行。 + * @param opcode 操作码(0..255) + * @return 命中指针;未找到返回 nullptr + */ const ConfigOp* MachineConfig::find_op_by_code(uint32_t opcode) const { for (const ConfigOp& o : ops_) { if (o.opcode == opcode) { @@ -379,11 +451,21 @@ const ConfigOp* MachineConfig::find_op_by_code(uint32_t opcode) const { return nullptr; } +/** + * @brief 查询某 opcode 是否启用。 + * @param opcode 操作码 + * @return 指令存在且 enabled 为 true;未知 opcode 返回 false + */ bool MachineConfig::op_enabled(uint32_t opcode) const { const ConfigOp* op = find_op_by_code(opcode); return op != nullptr && op->enabled; } +/** + * @brief 按名称查内建 FB。 + * @param name FB 名(小写,如 "ton") + * @return 命中指针;未找到返回 nullptr + */ const ConfigFb* MachineConfig::find_fb(const std::string& name) const { for (const ConfigFb& f : fbs_) { if (f.name == name) { diff --git a/compiler/src/Parser.cpp b/compiler/src/Parser.cpp index 5c71730..12d326a 100644 --- a/compiler/src/Parser.cpp +++ b/compiler/src/Parser.cpp @@ -57,9 +57,12 @@ namespace compiler { namespace { - // 明确拒绝的保留名(小写命中即报错,见 Doc/compiler/语法.md)。 - // 这些词在词法层不是关键字(lex 成 IDENT),必须在语法层显式拦截, - // 避免 VAR_IN_OUT / REF / CLASS 等被静默当作用户标识符。 + /** + * @brief 语法层明确拒绝的保留名清单。 + * @details 这些词在词法层不是关键字(lex 成 IDENT),必须在语法层 + * 显式拦截,避免 VAR_IN_OUT / REF / CLASS 等被静默当作 + * 用户标识符(详见 Doc/compiler/语法.md)。 + */ const char* const kForbidden[] = { "var_in_out", "var_temp", "ref", "class", "any", "pointer", "interface", "method", @@ -989,11 +992,11 @@ namespace { // ---- 成员 ---- - std::string* err_; // 错误输出(可空) - std::vector tokens_; // 整段 token 流(构造时一次性 lex 完成) - Unit* out_; // 解析结果 - size_t pos_ = 0; // 当前 token 下标 - bool ok_ = true; // 词法阶段是否成功 + std::string* err_; ///< 错误输出(可空) + std::vector tokens_; ///< 整段 token 流(构造时一次性 lex 完成) + Unit* out_; ///< 解析结果 + size_t pos_ = 0; ///< 当前 token 下标 + bool ok_ = true; ///< 词法阶段是否成功 }; } // namespace diff --git a/compiler/src/Project.cpp b/compiler/src/Project.cpp index af2b20c..3dce331 100644 --- a/compiler/src/Project.cpp +++ b/compiler/src/Project.cpp @@ -3,6 +3,31 @@ * @brief 工程定义与 project.toml 解析(schema 校验) * @author * @date 2026-08-21 + * + * @details 设计说明(schema 规则见 Doc/初步计划.md 12.1 的 toml 字段表): + * - 解析顺序:root 键白名单 → [project] → [files] → [gvl](可选)→ [io](可选), + * 任一段失败立即返回(首个错误优先,不做恢复) + * - 错误:稳定类别前缀(parse error / unknown key / missing field / invalid value / + * bad entry)+ 行号(toml++ 源位置) + * - io 绑定不创造变量:slot 留待 12.6 链接阶段解析,此处填 0 + * + * 函数清单: + * - fail 写错误消息;err 为 nullptr 时静默 + * - at / at_pos toml 节点 / 位置 → " (line N)" 行号后缀 + * - req_string 取字符串(必填校验) + * - req_pos_int 取正整数(必填校验) + * - req_uint 取非负整数(必填校验,channel / bit 允许 0) + * - check_keys 表内键白名单(报首个未知键) + * - parse_project_section [project] 段(name/entry/cycle_limit/dt_ms 全必填) + * - parse_files_section [files] 段(st 必填、非空字符串数组) + * - parse_gvl_section [gvl] 段(可选;file 必填字符串) + * - parse_io_entry [[io.*]] 单个条目(var/channel/bit 全必填) + * - parse_io_section [io] 段(可选;input/output 数组可缺省) + * - parse_root 顶层:root 键白名单 + 按序解析四段 + * - dir_of 取路径的目录部分 + * - parse_project 对外入口:parse_file 捕获 parse_error → parse_root + * - compile_files 编译文件集合(files.st ∪ gvl.file,去重) + * - compute_project_hash 校验文件存在并计算 FNV-1a 64 工程哈希 */ #include "compiler/Project.h" @@ -23,28 +48,52 @@ namespace { // ---- 错误收集:稳定类别前缀 + 行号 ---- - // 写错误消息;err 为 nullptr 时静默忽略(调用方可不关心原因) + /** + * @brief 写错误消息。 + * @details err 为 nullptr 时静默忽略(调用方可不关心原因)。 + * @param err 错误输出(可空) + * @param msg 完整错误消息(已带类别前缀与行号) + */ void fail(std::string* err, const std::string& msg) { if (err) { *err = msg; } } - // 给 toml 节点附加行号后缀 " (line N)",用于定位非法值位置 + /** + * @brief 给 toml 节点附加行号后缀。 + * @param n toml 节点 + * @return " (line N)",用于定位非法值位置 + */ std::string at(const toml::node& n) { char buf[32]; std::snprintf(buf, sizeof buf, " (line %u)", n.source().begin.line); return buf; } - // parse_error 的位置可能是空的(如文件打不开):只在有位置时带行号 + /** + * @brief 给 parse_error 位置附加行号后缀。 + * @details parse_error 的位置可能是空的(如文件打不开):只在有位置时带行号。 + * @param pos toml 源位置 + * @return " (line N)";位置为空时返回空串 + */ std::string at_pos(const toml::source_position& pos) { char buf[32]; std::snprintf(buf, sizeof buf, " (line %u)", pos.line); return static_cast(pos) ? buf : std::string(); } - // 取字符串,必填校验 + /** + * @brief 取字符串字段(必填校验)。 + * @details 缺失报 "missing field 'key' in [sec]";类型非字符串报 + * "invalid value for 'key' in [sec] (expect string)" + 行号。 + * @param tbl 当前 toml 表 + * @param sec 段名(报错用) + * @param key 字段名 + * @param out 输出字符串值 + * @param err 错误输出(可空) + * @return true 成功;false 缺失或类型错误(err 已写) + */ bool req_string(const toml::table& tbl, const char* sec, const char* key, std::string* out, std::string* err) { if (const auto nv = tbl[key]) { @@ -60,7 +109,17 @@ namespace { return false; } - // 取正整数,必填校验 + /** + * @brief 取正整数字段(必填校验)。 + * @details 缺失报 "missing field";非整数报 "invalid value ... (expect integer)"; + * v <= 0 报 "(expect > 0)",均带行号。 + * @param tbl 当前 toml 表 + * @param sec 段名(报错用) + * @param key 字段名 + * @param out 输出整数值 + * @param err 错误输出(可空) + * @return true 成功;false 缺失或非法(err 已写) + */ bool req_pos_int(const toml::table& tbl, const char* sec, const char* key, uint32_t* out, std::string* err) { if (const auto nv = tbl[key]) { @@ -82,7 +141,16 @@ namespace { return false; } - // 取非负整数,必填校验(channel / bit 允许 0) + /** + * @brief 取非负整数字段(必填校验)。 + * @details 与 req_pos_int 相同校验,但允许 0(channel / bit 可为 0)。 + * @param tbl 当前 toml 表 + * @param sec 段名(报错用) + * @param key 字段名 + * @param out 输出整数值 + * @param err 错误输出(可空) + * @return true 成功;false 缺失或非法(err 已写) + */ bool req_uint(const toml::table& tbl, const char* sec, const char* key, uint32_t* out, std::string* err) { if (const auto nv = tbl[key]) { @@ -104,7 +172,16 @@ namespace { return false; } - // 表内键白名单;返回 false 并报首个未知键 + /** + * @brief 表内键白名单校验。 + * @details 发现首个未知键即失败,报 "unknown key 'key' in [sec]" + 行号。 + * @param tbl 当前 toml 表 + * @param sec 段名(报错用) + * @param allowed 允许的键名数组 + * @param n_allowed 键名数量 + * @param err 错误输出(可空) + * @return true 全部合法;false 有未知键(err 已写) + */ bool check_keys(const toml::table& tbl, const char* sec, const char* const* allowed, size_t n_allowed, std::string* err) { @@ -127,8 +204,14 @@ namespace { // ---- [project] ---- - // [project] 段:name/entry/cycle_limit/dt_ms 全必填; - // 第一版 entry 只接受 "program MAIN" + /** + * @brief 解析 [project] 段。 + * @details name/entry/cycle_limit/dt_ms 全必填;第一版 entry 只接受 "program MAIN"。 + * @param root 根表 + * @param out 输出 Project(填充 name/entry/cycle_limit/dt_ms) + * @param err 错误输出(可空) + * @return true 成功;false(err 已写) + */ bool parse_project_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"name", "entry", "cycle_limit", "dt_ms"}; @@ -166,7 +249,14 @@ namespace { // ---- [files] ---- - // [files] 段:st 必填、非空数组,元素必须全为字符串,按声明顺序收集 + /** + * @brief 解析 [files] 段。 + * @details st 必填、非空数组,元素必须全为字符串,按声明顺序收集。 + * @param root 根表 + * @param out 输出 Project(填充 files_st) + * @param err 错误输出(可空) + * @return true 成功;false(err 已写) + */ bool parse_files_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"st"}; @@ -208,7 +298,14 @@ namespace { // ---- [gvl](可选)---- - // [gvl] 段:可选;存在时 file 必填字符串,缺失整段不报错 + /** + * @brief 解析 [gvl] 段(可选)。 + * @details 存在时 file 必填字符串;整段缺失不报错。 + * @param root 根表 + * @param out 输出 Project(填充 gvl_file) + * @param err 错误输出(可空) + * @return true 成功;false(err 已写) + */ bool parse_gvl_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"file"}; @@ -228,7 +325,16 @@ namespace { // ---- [[io.input]] / [[io.output]](可选)---- - // 单个 I/O 条目:var/channel/bit 全必填、全非负整数;slot 留待 12.6 链接阶段 + /** + * @brief 解析单个 I/O 条目([[io.input]] / [[io.output]] 数组元素)。 + * @details var/channel/bit 全必填,channel/bit 为非负整数;类型非表报 + * "bad entry in [[io.xxx]] (expect table)";slot 留待 12.6 链接阶段解析。 + * @param el 条目节点 + * @param is_input true = [[io.input]],false = [[io.output]] + * @param out 输出 Project(追加 io 条目) + * @param err 错误输出(可空) + * @return true 成功;false(err 已写) + */ bool parse_io_entry(const toml::node& el, bool is_input, Project* out, std::string* err) { static const char* const allowed[] = {"var", "channel", "bit"}; @@ -257,7 +363,14 @@ namespace { return true; } - // [io] 段:可选;input/output 子表为数组时逐条解析,两数组都可缺省 + /** + * @brief 解析 [io] 段(可选)。 + * @details input/output 子表为数组时逐条解析,两数组都可缺省;整段缺失不报错。 + * @param root 根表 + * @param out 输出 Project(填充 io) + * @param err 错误输出(可空) + * @return true 成功;false(err 已写) + */ bool parse_io_section(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"input", "output"}; @@ -292,8 +405,15 @@ namespace { // ---- 顶层 ---- - // 顶层:root 键白名单 + 按 project → files → gvl → io 顺序解析, - // 任一段失败立即返回 false(首个错误优先,不做恢复) + /** + * @brief 顶层解析入口。 + * @details root 键白名单 + 按 project → files → gvl → io 顺序解析, + * 任一段失败立即返回 false(首个错误优先,不做恢复)。 + * @param root 根表 + * @param out 输出 Project + * @param err 错误输出(可空) + * @return true 成功;false(err 已写) + */ bool parse_root(const toml::table& root, Project* out, std::string* err) { static const char* const allowed[] = {"project", "files", "gvl", "io"}; if (!check_keys(root, "root", allowed, 4, err)) { @@ -311,7 +431,12 @@ namespace { return parse_io_section(root, out, err); } - // 取路径的目录部分:最后一个分隔符之前;无分隔符返回 "."(相对当前目录) + /** + * @brief 取路径的目录部分。 + * @details 最后一个分隔符之前;无分隔符返回 "."(相对当前目录)。 + * @param path 文件路径 + * @return 目录部分 + */ std::string dir_of(const std::string& path) { const size_t slash = path.find_last_of("/\\"); return (slash == std::string::npos) ? "." : path.substr(0, slash); @@ -319,6 +444,16 @@ namespace { } // namespace +/** + * @brief 解析并校验 project.toml(对外入口)。 + * @details 先定 base_dir(toml 所在目录),再 parse_file;toml++ 默认 TOML_EXCEPTIONS=1, + * parse_file 失败直接抛 parse_error,捕获后报 "parse error: <描述>" + 行号 + * (位置空时不带),随后交由 parse_root 按段解析。 + * @param toml_path project.toml 路径 + * @param out 输出 Project + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 已写) + */ bool parse_project(const std::string& toml_path, Project* out, std::string* err) { out->base_dir = dir_of(toml_path); @@ -336,6 +471,12 @@ bool parse_project(const std::string& toml_path, Project* out, std::string* err) // ---- 文件集合与工程哈希 ---- +/** + * @brief 编译文件集合:files.st ∪ gvl.file,去重(gvl 已在 files.st 则跳过)。 + * @details 保持 files.st 顺序,gvl 追加在后;相对路径以 base_dir 为基准解析。 + * @param p 工程定义 + * @return 编译文件路径集合 + */ std::vector compile_files(const Project& p) { std::vector out = p.files_st; if (!p.gvl_file.empty() && @@ -345,6 +486,15 @@ std::vector compile_files(const Project& p) { return out; } +/** + * @brief 校验全部文件存在并计算工程哈希。 + * @details 集合按路径排序(规格:路径只当排序键),对内容做 FNV-1a 64 增量; + * 空集合 = basis(kFnvBasis);缺文件报错前缀 "file missing"。 + * @param p 工程定义 + * @param hash 输出工程哈希 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 文件缺失(err 已写) + */ bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err) { std::vector files = compile_files(p); diff --git a/compiler/src/Stb.cpp b/compiler/src/Stb.cpp index 156fc2f..75a85e2 100644 --- a/compiler/src/Stb.cpp +++ b/compiler/src/Stb.cpp @@ -3,6 +3,10 @@ * @brief 编译器自带的 .stb 映像规范(写侧)+ 只读视图 + FNV-1a + sidecar * @author * @date 2026-08-21 + * + * @details 与 vm 侧各一份实现;格式契约见 Doc/isa/指令与映像.md。 + * 12.13 修订已落地:头 104 = 原 72 + 型号标识[32] @72,文件尾 SHA-256[32]; + * 工程哈希为 FNV-1a 64。 */ #include "compiler/Stb.h" @@ -15,6 +19,14 @@ namespace compiler { +/** + * @brief FNV-1a 64 增量哈希更新(工程哈希,非密码学)。 + * @param h 当前哈希(首轮传 kFnvBasis) + * @param data 输入字节 + * @param len 字节数 + * @return 更新后的哈希值 + * @details 每字节:h ^= byte; h *= kFnvPrime。 + */ uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len) { for (size_t i = 0; i < len; ++i) { h ^= data[i]; @@ -23,12 +35,19 @@ uint64_t fnv1a64_update(uint64_t h, const uint8_t* data, size_t len) { return h; } +/** + * @brief FNV-1a 64 一次性哈希(工程哈希)。 + * @param data 输入字节 + * @param len 字节数 + * @return 哈希值(从 kFnvBasis 起) + */ uint64_t fnv1a64(const uint8_t* data, size_t len) { return fnv1a64_update(kFnvBasis, data, len); } namespace { + /// 小端读 32 位。 uint32_t get_le32(const uint8_t* p) { return static_cast(p[0]) | (static_cast(p[1]) << 8) @@ -36,6 +55,7 @@ namespace { | (static_cast(p[3]) << 24); } + /// 小端读 64 位。 uint64_t get_le64(const uint8_t* p) { uint64_t v = 0; for (int i = 0; i < 8; ++i) { @@ -46,6 +66,7 @@ namespace { // ---- SHA-256(FIPS 180-4)---- + /// SHA-256 轮常量 K[0..63]。 const uint32_t kShaK[64] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, @@ -60,15 +81,26 @@ namespace { 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, }; + /// 循环右移。 inline uint32_t rotr(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } + /** + * @brief SHA-256 增量状态机。 + * @details 标准 FIPS 180-4 实现:update() 吸收任意长度字节流,final() 输出 + * 32 字节大端摘要。按 64 字节块 process()。 + */ struct Sha256 { uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, - 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; - uint64_t total = 0; - uint8_t block[64]; - size_t block_len = 0; + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; ///< 初始哈希值 + uint64_t total = 0; ///< 已吸收字节数(final 时编码进长度域) + uint8_t block[64]; ///< 当前块缓冲 + size_t block_len = 0; ///< 块缓冲已用字节数 + /** + * @brief 吸收数据。 + * @param data 输入字节 + * @param len 字节数 + */ void update(const uint8_t* data, size_t len) { total += len; while (len > 0) { @@ -91,6 +123,7 @@ namespace { } } + /// 压缩一个满块(64 字节:w[0..63] 展开 + 64 轮)。 void process() { uint32_t w[64]; for (int i = 0; i < 16; ++i) { @@ -123,6 +156,10 @@ namespace { h[4] += e; h[5] += f; h[6] += g; h[7] += hh; } + /** + * @brief 结束并输出摘要。 + * @param out 32 字节输出缓冲(大端) + */ void final(uint8_t out[32]) { const uint64_t bitlen = total * 8; const uint8_t pad = 0x80; @@ -146,6 +183,7 @@ namespace { } } + /// 大端读 32 位。 static uint32_t get_be32(const uint8_t* p) { return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) | (static_cast(p[2]) << 8) | static_cast(p[3]); @@ -154,12 +192,25 @@ namespace { } // namespace +/** + * @brief 一次性 SHA-256(文件完整性校验)。 + * @param data 输入字节 + * @param len 字节数 + * @param out 32 字节摘要输出(大端) + */ void sha256(const uint8_t* data, size_t len, uint8_t out[kSha256Size]) { Sha256 s; s.update(data, len); s.final(out); } +/** + * @brief 按型号标识约定填充 32 字节:name + version(如 "STATOR1")。 + * @param name 型号名 + * @param version 版本号 + * @param out 32 字节输出缓冲 + * @details 不足补 '\0',超长截断。 + */ void fill_model_id(const std::string& name, uint32_t version, char out[kModelIdSize]) { const std::string id = name + std::to_string(version); for (size_t i = 0; i < kModelIdSize; ++i) { @@ -167,6 +218,19 @@ void fill_model_id(const std::string& name, uint32_t version, char out[kModelIdS } } +/** + * @brief 从原始字节构造只读视图(不拷贝,调用方保证生命周期)。 + * @param buf 映像缓冲;可为 nullptr + * @param len 缓冲字节数 + * @return 校验结果;ok() 判成功,error() 取失败原因 + * @details 校验顺序:长度 ≥ 头 → 魔数 → 版本 → 段偏移范围/单调 → + * 常量表/函数表大小 → 代码段 4 字节对齐 → 入口 fn_id 范围; + * 错误消息:"null buffer" / "image too short" / "bad magic" / + * "bad version" / "missing sha256 tail" / "segment offset out of range" / + * "segment offsets not monotonic" / "const table size mismatch" / + * "function table size mismatch" / "code segment not 4-byte aligned" / + * "entry fn_id out of range"。 + */ StbView StbView::from(const uint8_t* buf, size_t len) { StbView v; v.buf_ = buf; @@ -236,10 +300,12 @@ StbView StbView::from(const uint8_t* buf, size_t len) { return v; } +/// 从 vector 构造(转发 from(buf.data(), buf.size()))。 StbView StbView::from(const std::vector& buf) { return from(buf.data(), buf.size()); } +/// 读常量表一行;越界或未 ok() 时返回全 0。 ConstEntry StbView::const_entry(size_t i) const { ConstEntry e; if (ok_ && i < n_consts_) { @@ -250,11 +316,13 @@ ConstEntry StbView::const_entry(size_t i) const { return e; } +/// 常量表段起点(头 @52;由 from() 已校验的段起点)。 uint32_t StbView::offs_of_const() const { // offset_const = offs[0],由 from() 已校验的段起点 return static_cast(get_le32(buf_ + 52)); } +/// 读函数表一行;越界或未 ok() 时返回全 0。 StbView::FuncRow StbView::func_row(size_t i) const { FuncRow r; if (ok_ && i < n_funcs_) { @@ -266,22 +334,27 @@ StbView::FuncRow StbView::func_row(size_t i) const { return r; } +/// 字节码段起点(未 ok() 时返回 nullptr)。 const uint8_t* StbView::code_bytes() const { return ok_ ? buf_ + offset_code_ : nullptr; } +/// 字节码段字节数(未 ok() 时返回 0)。 size_t StbView::code_len() const { return ok_ ? offset_fb_ - offset_code_ : 0; } +/// 数据段起点(未 ok() 时返回 nullptr)。 const uint8_t* StbView::data_bytes() const { return ok_ ? buf_ + offset_data_ : nullptr; } +/// 数据段字节数(不含文件尾 SHA-256;未 ok() 时返回 0)。 size_t StbView::data_len() const { return ok_ ? (len_ - kSha256Size) - offset_data_ : 0; } +/// 型号标识字符串(头 72..103,截断到首个 '\0';未 ok() 时返回空串)。 std::string StbView::model_id() const { if (!ok_) { return ""; @@ -294,12 +367,14 @@ std::string StbView::model_id() const { return s; } +/// 型号标识是否匹配 name + version(未 ok() 时返回 false)。 bool StbView::model_matches(const std::string& name, uint32_t version) const { char want[kModelIdSize]; fill_model_id(name, version, want); return std::memcmp(buf_ + 72, want, kModelIdSize) == 0; } +/// 文件尾 32 字节 SHA-256 校验(对文件尾之前全部内容重算;未 ok() 时返回 false)。 bool StbView::sha_ok() const { if (!ok_) { return false; @@ -310,6 +385,13 @@ bool StbView::sha_ok() const { return std::memcmp(buf_ + content_len, digest, kSha256Size) == 0; } +/** + * @brief 读整个文件为字节(纯字节;校验交给 StbView)。 + * @param path 文件路径 + * @param out 输出字节 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err "cannot open for read: ") + */ bool read_stb_file(const char* path, std::vector* out, std::string* err) { std::ifstream in(path, std::ios::binary); if (!in) { @@ -322,6 +404,13 @@ bool read_stb_file(const char* path, std::vector* out, std::string* err return true; } +/** + * @brief 写字节为文件(纯字节)。 + * @param path 文件路径 + * @param img 映像字节 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err "cannot open for write: " / "write failed: ") + */ bool write_stb_file(const char* path, const std::vector& img, std::string* err) { std::FILE* f = std::fopen(path, "wb"); if (f == nullptr) { @@ -338,6 +427,13 @@ bool write_stb_file(const char* path, const std::vector& img, std::stri return ok; } +/** + * @brief 生成 sidecar 文本(TOML)。 + * @param bindings I/O 绑定列表 + * @return TOML 文本(每个绑定一节 [[io.input]] / [[io.output]],含 + * var / slot / channel / bit 四字段) + * @details I/O 绑定 var → 槽号 → channel/bit(执行器采样用)。 + */ std::string make_sidecar(const std::vector& bindings) { std::string out; for (const IoBinding& b : bindings) { @@ -357,6 +453,13 @@ std::string make_sidecar(const std::vector& bindings) { return out; } +/** + * @brief 生成并写 sidecar 文件。 + * @param path 文件路径 + * @param bindings I/O 绑定列表 + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false(err "cannot open for write: " / "write failed: ") + */ bool write_sidecar_file(const char* path, const std::vector& bindings, std::string* err) { std::FILE* f = std::fopen(path, "wb"); diff --git a/compiler/src/TypeInfo.cpp b/compiler/src/TypeInfo.cpp index 3c43e5c..64f713c 100644 --- a/compiler/src/TypeInfo.cpp +++ b/compiler/src/TypeInfo.cpp @@ -3,6 +3,9 @@ * @brief 语言类型元数据(machine.toml 别名 + 内建基元解析) * @author * @date 2026-08-21 + * + * @details type_meta 实现:先按名称(大小写不敏感)命中配置类型行, + * 再由 base 解析出内建基元,合成 TypeMeta。 */ #include "compiler/TypeInfo.h" @@ -13,6 +16,11 @@ namespace compiler { namespace { + /** + * @brief 转小写。 + * @param s 输入串 + * @return 全小写副本(逐字符 tolower,逐字节安全) + */ std::string lower(const std::string& s) { std::string out = s; for (char& ch : out) { @@ -23,6 +31,14 @@ namespace { } // namespace +/** + * @brief 按语言类型名查元数据。 + * @param cfg 机器配置(类型表来源) + * @param name 语言类型名 + * @return 对应 TypeMeta;未找到返回空 TypeMeta(operator bool 为 false) + * @details 比较前两侧都转小写(配置名大写 BOOL/INT/TIME,Linker type_name 小写); + * 命中配置行后再用 base 解析内建基元,两者都命中才算有效。 + */ TypeMeta type_meta(const MachineConfig& cfg, const std::string& name) { TypeMeta m; const std::string key = lower(name); diff --git a/compiler/src/Typecheck.cpp b/compiler/src/Typecheck.cpp index bcee19f..41cba80 100644 --- a/compiler/src/Typecheck.cpp +++ b/compiler/src/Typecheck.cpp @@ -468,10 +468,10 @@ namespace { } // ---- 成员 ---- - const Project& proj_; // 工程定义(本阶段未用) - const std::vector& units_; // 全部源文件 AST - const LinkResult& link_; // 链接结果(只读) - std::string* err_; // 错误输出(可空) + const Project& proj_; ///< 工程定义(本阶段未用) + const std::vector& units_; ///< 全部源文件 AST + const LinkResult& link_; ///< 链接结果(只读) + std::string* err_; ///< 错误输出(可空) }; } // namespace diff --git a/compiler/src/main.cpp b/compiler/src/main.cpp index 05ed636..c3575de 100644 --- a/compiler/src/main.cpp +++ b/compiler/src/main.cpp @@ -3,6 +3,11 @@ * @brief STCompiler 可执行入口 * @author * @date 2026-08-21 + * + * @details 用法:STCompiler [-o out.stb] --machine ; + * --disasm 模式反汇编已编译映像。编译/反汇编必填 --machine(缺失报 + * "error: missing --machine ");打印模式(无 -o)不需要 + * 机器定义。错误统一打到 stderr,前缀 "error: "。 */ #include @@ -21,6 +26,7 @@ #include "compiler/Typecheck.h" namespace { + /// 打印命令行用法(三种模式 + -o / --machine / --disasm / --help)。 void usage() { std::printf("usage: STCompiler [-o .stb] --machine \n" " STCompiler .stb --disasm --machine \n" @@ -31,8 +37,16 @@ namespace { " --help 打印本帮助\n"); } - // 反汇编一个已编译映像(函数表逐条 + 常量表 + 数据段摘要)。 - // 指令偏移为文件绝对字节偏移;反汇编按 machine.toml 的 format 输出。 + /** + * @brief 反汇编一个已编译映像。 + * @param path .stb 文件路径 + * @param cfg 机器定义(machine.toml;反汇编按 format 输出) + * @return 0 成功;1 失败(错误打到 stderr,前缀 "error: ") + * @details 输出:映像摘要(大小 / 函数数 / 全局数 / 入口 fn_id / + * dt_ms / cycle_limit / hash / model / sha)、常量表、函数表逐条 + * 反汇编、数据段摘要。指令偏移为文件绝对字节偏移。 + * 错误:"cannot open for read" / 解析失败消息(见 StbView::from)。 + */ int dump_image(const char* path, const compiler::MachineConfig& cfg) { std::vector bytes; std::string err; @@ -92,6 +106,16 @@ namespace { } } +/** + * @brief STCompiler 程序入口。 + * @param argc 参数个数 + * @param argv 参数列表 + * @return 0 成功;1 失败(错误 stderr 前缀 "error: ") + * @details 参数:argv[1] 为 project.toml 或 .stb(--disasm 时); + * -o 指定输出(打印模式判定依据);--machine 编译/反汇编必填。 + * 编译管线:读 .st → 链接 → 类型检查 → 寄存器码 → 写映像 → + * 写后自检(型号标识 + SHA-256)→ sidecar(I/O 绑定 var → 槽号)。 + */ int main(int argc, char** argv) { if (argc < 2) { std::printf("STCompiler 0.1\n"); From afb5d5b94a7598ddb54e8586cbd56b9c2efb13ba Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 23:34:07 +0800 Subject: [PATCH 67/69] =?UTF-8?q?executor=20=E6=A8=A1=E5=9D=97=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=AE=8C=E5=96=84=EF=BC=9ADoxygen=20=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=EF=BC=88main.cpp=20=E5=85=A5=E5=8F=A3/usage/Binding/s?= =?UTF-8?q?idecar=20=E8=A7=A3=E6=9E=90/=E9=87=87=E6=A0=B7/=E5=8D=95?= =?UTF-8?q?=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- executor/src/main.cpp | 72 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/executor/src/main.cpp b/executor/src/main.cpp index a1ca3a5..22157cd 100644 --- a/executor/src/main.cpp +++ b/executor/src/main.cpp @@ -5,7 +5,7 @@ * @date 2026-08-21 * * @details 12.10:加载 .stb + sidecar,按扫描周期执行。 - * - `BytecodeExecutor .stb [--cycles N] [--replay ]` + * - `BytecodeExecutor .stb [--cycles N] [--replay ] [--step]` * - 只链 vm + isa,不链 compiler;dt_ms / cycle_limit 从映像头取 * - I 采样 / Q 写回按 sidecar(var → 槽号 → channel/bit),不创造变量 * - --replay:读录制文本(每行空格分隔的 0/1,按 io.input 绑定顺序), @@ -26,6 +26,7 @@ namespace { + /// 打印命令行用法。 void usage() { std::printf("usage: BytecodeExecutor .stb [--cycles N] [--replay ] [--step]\n" " 加载 .stb + .runtime.toml,按扫描周期执行\n" @@ -35,16 +36,28 @@ namespace { " --help 打印本帮助\n"); } - // sidecar 绑定(TOML 子集,格式冻结于 Doc/isa/指令与映像.md) + /** + * @brief sidecar 中的一条 I/O 绑定。 + * @details TOML 子集 `var`/`slot`/`channel`/`bit`,格式冻结于 + * Doc/isa/指令与映像.md;不创造变量,只把外部通道映射到数据区槽。 + */ struct Binding { - bool is_input = true; - std::string var; - uint32_t slot = 0; - uint32_t channel = 0; - uint32_t bit = 0; + bool is_input = true; ///< true = [[io.input]],false = [[io.output]] + std::string var; ///< 绑定的全局变量名 + uint32_t slot = 0; ///< 数据区槽号(槽 × 8 = 字节偏移) + uint32_t channel = 0; ///< 通道号(展示用) + uint32_t bit = 0; ///< 位号(展示用) }; - // 解析 .runtime.toml(逐行手写解析,仅冻结子集) + /** + * @brief 解析 .runtime.toml(逐行手写解析,仅冻结子集)。 + * @param path sidecar 路径 + * @param out 输出绑定列表(按文件中 [[io.input]] / [[io.output]] 出现顺序) + * @param err 错误输出 + * @return true 成功;false(文件打不开,err 已写) + * @details 识别行:`[[io.input]]` / `[[io.output]]` 分段,`key = "value"` 或 + * `key = 数字` 字段(var/slot/channel/bit);空行与 `#` 注释跳过。 + */ bool parse_sidecar(const std::string& path, std::vector* out, std::string* err) { std::ifstream in(path); @@ -109,16 +122,36 @@ namespace { return true; } - // 读一个槽的 BOOL 值(低字节) + /** + * @brief 读一个槽的 BOOL 值。 + * @param m 机器实例 + * @param slot 槽号 + * @return 低字节非零 → 1,否则 0 + */ int slot_bool(vm::Machine& m, uint32_t slot) { return m.data()[static_cast(slot) * 8] ? 1 : 0; } + /** + * @brief 写一个槽的 BOOL 值。 + * @param m 机器实例 + * @param slot 槽号 + * @param v 0/1(非零归一化为 1) + */ void put_bool(vm::Machine& m, uint32_t slot, int v) { m.data()[static_cast(slot) * 8] = v ? 1 : 0; } - // 采样:回放行按 io.input 顺序填,无回放则全 0;文件读尽后保持最后一行 + /** + * @brief 本周期 I 采样:喂入数据区。 + * @param m 机器实例 + * @param bindings sidecar 绑定列表 + * @param replay_in 回放文件流(未打开则无回放) + * @param replay_line 当前回放行(0/1 按 io.input 绑定顺序) + * @details 有回放:读一行,按 io.input 顺序填槽,缺位补 0; + * 文件读尽后保持最后一行(replay_line 不清空)。 + * 无回放:所有 input 槽填 0。 + */ void sample_inputs(vm::Machine& m, const std::vector& bindings, std::ifstream& replay_in, std::vector& replay_line) { if (replay_in.is_open()) { @@ -155,7 +188,14 @@ namespace { } } - // 按 sidecar 绑定顺序打印 I/Q + /** + * @brief 按 sidecar 绑定顺序生成 I / Q 文本。 + * @param m 机器实例 + * @param bindings sidecar 绑定列表 + * @param is 输出:input 槽值(空格分隔) + * @param qs 输出:output 槽值(空格分隔) + * @return is(与参数 3 同对象) + */ std::string iq_string(vm::Machine& m, const std::vector& bindings, std::string* is, std::string* qs) { for (const Binding& b : bindings) { @@ -172,6 +212,16 @@ namespace { } // namespace +/** + * @brief 程序入口。 + * @param argc 参数个数 + * @param argv 参数表 + * @return 0 成功;1 失败 + * @details 流程:解析参数(--cycles/--replay/--step)→ 读 .stb → Image 解析 + * → Machine::create(型号/SHA 校验)→ 解析 sidecar → 按周期循环: + * 采样 I → run_cycle → 打印 I/Q。--step 模式跑 1 个周期并逐指令 + * 打印 pc/fn/反汇编/非零寄存器。所有错误打印 `error: ...` 到 stderr。 + */ int main(int argc, char** argv) { if (argc < 2) { std::printf("BytecodeExecutor 0.1\n"); From 351d6ef40cb9744d64cef472d7616a2aca740697 Mon Sep 17 00:00:00 2001 From: chentianya Date: Sat, 22 Aug 2026 10:45:26 +0800 Subject: [PATCH 68/69] =?UTF-8?q?executor:=20main=20=E5=86=85=E9=83=A8?= =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E5=9D=97=E6=B3=A8=E9=87=8A=E8=A1=A5=E5=85=85?= =?UTF-8?q?=EF=BC=88=E5=8F=82=E6=95=B0=E8=A7=A3=E6=9E=90/=E8=AF=BB?= =?UTF-8?q?=E6=98=A0=E5=83=8F/=E4=BF=A1=E4=BB=BB=E9=93=BE/sidecar=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84/=E5=9B=9E=E6=94=BE=E6=89=93=E5=BC=80?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- executor/src/main.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/executor/src/main.cpp b/executor/src/main.cpp index 22157cd..938969f 100644 --- a/executor/src/main.cpp +++ b/executor/src/main.cpp @@ -234,6 +234,7 @@ int main(int argc, char** argv) { } const std::string stb_path = argv[1]; + // 参数:--cycles N / --replay / --step(缺省 cycles=10、无回放、非单步) uint32_t cycles = 10; bool step_mode = false; std::string replay; @@ -247,6 +248,7 @@ int main(int argc, char** argv) { } } + // 读 .stb 映像(二进制)到字节缓冲 std::string err; std::vector bytes; { @@ -257,6 +259,7 @@ int main(int argc, char** argv) { } bytes.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); } + // 结构校验(Image::from)+ 型号/SHA-256 校验(Machine::create) const vm::Image img = vm::Image::from(bytes); if (!img.ok()) { std::fprintf(stderr, "error: %s\n", img.error().c_str()); @@ -268,7 +271,7 @@ int main(int argc, char** argv) { return 1; } - // sidecar:.runtime.toml + // sidecar 路径推导:.stb → .runtime.toml std::string sidecar_path = stb_path; const size_t dot = sidecar_path.find_last_of('.'); if (dot != std::string::npos) { @@ -281,7 +284,7 @@ int main(int argc, char** argv) { return 1; } - // 回放缓冲 + // 回放缓冲(当前行 0/1 序列)+ 打开回放文件;打不开 → 报错退出 std::vector replay_line; std::ifstream replay_in; if (!replay.empty()) { From d51dce4ae38b8990c5dd75f24e5b9fb3d0a11f5a Mon Sep 17 00:00:00 2001 From: chentianya Date: Sat, 22 Aug 2026 13:13:12 +0800 Subject: [PATCH 69/69] =?UTF-8?q?CMakePresets:=20=E5=90=AF=E7=94=A8=20comp?= =?UTF-8?q?ile=5Fcommands.json=20=E5=AF=BC=E5=87=BA=EF=BC=88IDE/=E9=9D=99?= =?UTF-8?q?=E6=80=81=E5=88=86=E6=9E=90=E7=94=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakePresets.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index e150da9..c584cae 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,7 +13,8 @@ "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { "CMAKE_C_COMPILER": "gcc", - "CMAKE_CXX_COMPILER": "g++" + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } }, {