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/.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/.gitmodules b/.gitmodules new file mode 100644 index 0000000..154ee6c --- /dev/null +++ b/.gitmodules @@ -0,0 +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/CMakeLists.txt b/CMakeLists.txt index e69de29..ff69001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(Stator + 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(executor) + +# 自动化用例 +enable_testing() +add_subdirectory(tests) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..c584cae --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,49 @@ +{ + "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++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "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" + } + ] +} diff --git a/Doc/README.md b/Doc/README.md deleted file mode 100644 index 3ce586c..0000000 --- a/Doc/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Doc - -设计文档,不是编译目标。 - -| 文件 | 内容 | -|---|---| -| [`初步计划.md`](初步计划.md) | 定案:子集 ST、toml 工程、寄存器 VM、执行模型、ST 子集、实现顺序 | - -仓库根目录 [`README.md`](../README.md) 只说明目录和模块边界;语言规则、映像与扫描周期的细节以本目录为准。 diff --git a/Doc/compiler/STCompiler使用说明.md b/Doc/compiler/STCompiler使用说明.md new file mode 100644 index 0000000..4a1c268 --- /dev/null +++ b/Doc/compiler/STCompiler使用说明.md @@ -0,0 +1,158 @@ +# 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] --machine +STCompiler .stb --disasm --machine +``` + +| 参数 | 作用 | +|---|---| +| `` | 工程文件(必填) | +| `-o .stb` | 编译并写出映像文件(+ sidecar);**缺省时只打印文件集合与工程哈希**(不需 `--machine`) | +| `--machine ` | 机器定义(指令 opcode / 类型 / FB 布局,见 `Doc/isa/指令配置.md`);**编译与 `--disasm` 必填**,加载/校验失败报错退出 | +| `.stb --disasm` | **反汇编一个已编译的映像**(不重新编译,见下) | +| `--help` | 打印帮助 | + +退出码:`0` 成功;`1` 失败(错误信息打到 stderr)。 + +## 反汇编(--disasm) + +对已编译的 `.stb` 逐段查看:段摘要(含型号与 SHA 校验)、函数表逐条指令(文件绝对偏移)、常量表、数据段 hex。 + +```bash +$ 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 + 0x0080 RET + fn 1: nregs=13, offset=4, len=17 + 0x0084 LOAD_I r8, 1 + 0x0088 STORE_GLOBAL r8, 4 + ... +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 +$ STCompiler examples/line1/project.toml +project: line1 +files: + globals.st + motor.st + main.st +hash: 0xc3fe4ae74ad45900 + +$ STCompiler examples/line1/project.toml -o line1.stb --machine compiler/machine.toml +compiled: line1.stb (288 bytes, 2 functions, 4 globals) +``` + +产物:`line1.stb`(288 字节:头 104 含型号标识 + 各段 + SHA-256 文件尾)+ `line1.runtime.toml`(sidecar)。指令见 `Doc/isa/指令与映像.md`,结构拆解见 `Doc/isa/stb文件格式.md`。 + +## 工程文件(project.toml) + +只做三件事:**列出源文件、指定唯一 GVL、可选地把已声明全局接到硬件**。不声明变量。 + +| 字段 | 必填 | 说明 | +|---|---|---| +| `project.name` | 是 | 工程名 | +| `project.entry` | 是 | 第一版必须 `"program MAIN"` | +| `project.cycle_limit` | 是 | 每周期指令上限(>0) | +| `project.dt_ms` | 是 | 本周期 Δt 毫秒(>0),给内置 FB 定时器 | +| `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` / `TP` / `CTU` / `CTD` / `CTUD` / `R_TRIG` / `F_TRIG`(关键字,不可作变量名)。 + +**语句**:赋值 `:=`、`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(`compiler_toml` / `lexer_tokens` / `parser_syntax` / `linker_links` / `typecheck_types` / `codegen_slice1` / `machine_config` / `vm_cycles` / `cases_all` / `cli_*`) + +## 当前状态 + +- ✅ 可编译:14 个正例用例全部产出 `.stb`(含型号标识 + SHA-256);6 个负例按期望拒绝 +- ✅ 执行:`BytecodeExecutor` 加载 `.stb` 跑扫描周期(型号/SHA 校验、回放、单步) +- ✅ 配置驱动:指令 opcode / 类型元数据 / FB 布局来自 `compiler/machine.toml`(`--machine`) diff --git a/Doc/compiler/寄存器码.md b/Doc/compiler/寄存器码.md new file mode 100644 index 0000000..687f402 --- /dev/null +++ b/Doc/compiler/寄存器码.md @@ -0,0 +1,135 @@ +# 寄存器码(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 已定槽号 | + +- **数据区** = 全局块(声明序,**每槽 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 按此实现) + +| 项 | 约定 | +|---|---| +| 结果寄存器 | **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) +- 表达式临时从 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/compiler/符号表与链接.md b/Doc/compiler/符号表与链接.md new file mode 100644 index 0000000..90eceeb --- /dev/null +++ b/Doc/compiler/符号表与链接.md @@ -0,0 +1,116 @@ +# 符号表与链接(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 实例布局:按类型算字段清单(输入/输出/内部/内建 8 个 FB 固定字段(12.11 扩充)) + +**不做(第一版)** + +- 寄存器分配(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 规格;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 | + +- 实例布局 = 类型字段表的一份拷贝;字段偏移 = 字段序号 × 8(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/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/compiler/编译管线.md b/Doc/compiler/编译管线.md new file mode 100644 index 0000000..4deeda8 --- /dev/null +++ b/Doc/compiler/编译管线.md @@ -0,0 +1,28 @@ +# 编译管线 + +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)。`.stb` 格式契约见 [`指令与映像.md`](../isa/指令与映像.md)(compiler 自带写实现 + 型号标识/SHA-256)。 + +## 管线 + +```text +加载 machine.toml(--machine,强校验) + → 读 toml、收齐 .st + → 词法 / 递归下降语法 + → 收集导出符号 + → 解析 VAR_EXTERNAL、链接、定地址 + → 类型检查 + → 按 PROGRAM / FUNCTION / FB 编寄存器码(opcode/类型/FB 布局查配置) + → 回填跳转、写映像头与工程哈希 + → 写 .stb(头 104 含型号标识 + 段 + SHA-256 文件尾)与 .runtime.toml(sidecar:io 绑定) +``` + +## 边界 + +- toml **不声明变量**;`VAR_GLOBAL` 第一版只允许出现在 `gvl.file` +- `[[io.*]]` 的 `var` 必须已在 GVL 中;同名全局链接失败 +- 第三方 toml 解析(toml++)放 `third_party/`,本库私有链接 +- 不写扫描周期,不链 `vm`、不链 `isa` diff --git a/Doc/compiler/词法.md b/Doc/compiler/词法.md new file mode 100644 index 0000000..f0dfef8 --- /dev/null +++ b/Doc/compiler/词法.md @@ -0,0 +1,131 @@ +# 词法(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 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)出明确错误。 + +## 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/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/executor/执行器入口.md b/Doc/executor/执行器入口.md new file mode 100644 index 0000000..bad9343 --- /dev/null +++ b/Doc/executor/执行器入口.md @@ -0,0 +1,41 @@ +# 执行器入口 + +BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`(`EXECUTABLE`),链接 `vm` 和 `isa`,**不链 `compiler`**。源码目录 `executor/`。 + +第一版两个可执行:`STCompiler` 编译出 `.stb` 与 `.runtime.toml`;`BytecodeExecutor` 加载映像按扫描周期跑。总顺序见 [`初步计划.md`](../初步计划.md) 第 12.10 节。 + +## 用法 + +```text +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 + +## 职责 + +- 加载 `.stb` 映像与 `.runtime.toml` sidecar(I/O 绑定 var → 槽号 → channel/bit) +- 读取时**校验型号标识与 SHA-256**:型号不匹配 / 文件被篡改 → 直接报错拒绝(执行器只接受与自身内建型号一致的映像) +- 按 sidecar 采样 I、写回 Q(不创造变量) +- `dt_ms` / `cycle_limit` 从映像头取,推进内置 FB(TON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG) +- 单步、看寄存器和映像、按录制的 `I` 回放 + +## 边界 + +- 指令与映像编码见 [`指令与映像.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/stb文件格式.md b/Doc/isa/stb文件格式.md new file mode 100644 index 0000000..3f59a55 --- /dev/null +++ b/Doc/isa/stb文件格式.md @@ -0,0 +1,127 @@ +# .stb 映像文件结构 + +编译产物 `.stb` 的**面向使用者**的结构说明。规范以 [`指令与映像.md`](指令与映像.md) 为准,本文用 `line1.stb`(288 字节,12.13 修订后)做真实拆解。 + +## 概览 + +- 魔数 `STSC`,版本 `1`,**全部整数小端** +- 文件 = 头 + 五个段,段序:**常量表 → 函数表 → 字节码 → FB 布局 → 数据** +- 段偏移记在头里;`offset_const ≤ offset_funcs ≤ offset_code ≤ offset_fb ≤ offset_data ≤ 文件长度` + +``` +┌──────────────────────────────────────────────────┐ +│ 头(104 字节,偏移 0,含型号标识[32]) │ +├──────────────────────────────────────────────────┤ +│ 常量表段 offset_const .. offset_funcs │ +├──────────────────────────────────────────────────┤ +│ 函数表段 offset_funcs .. offset_code │ +├──────────────────────────────────────────────────┤ +│ 字节码段 offset_code .. offset_fb │ +├──────────────────────────────────────────────────┤ +│ FB 布局段 offset_fb .. offset_data(v1 为空)│ +├──────────────────────────────────────────────────┤ +│ 数据段 offset_data .. offset_data+len │ +├──────────────────────────────────────────────────┤ +│ SHA-256 文件尾(32 字节,对文件尾之前全部内容) │ +└──────────────────────────────────────────────────┘ +``` + +## 头(104 字节) + +| 偏移 | 宽 | 字段 | 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` | 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` 起) + +一行 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 条(`0x84` 起;`0x80` 是 `fn 0` 的占位 `RET`,slot 为 8 字节定宽槽号): + +| 偏移 | 字节 | 反汇编 | 含义 | +|---|---|---|---| +| 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` 起) + +v1 为空(用户 FB 内联展开,字段偏移已算进数据段;内建 8 个 FB 布局冻结在编译器中(12.11 扩充))。行格式约定:`[field_count:u32][field_count × (tag:u32, offset:u32)]`,offset 相对实例基址。 + +## 数据段(`offset_data` 起) + +**每槽 8 字节定宽**(12.9 方案 a):`BOOL` 用低 1 字节、`INT` 用低 2 字节(小端)、`TIME` 全 8 字节。指令 `slot` 操作数即**槽号**,数据偏移 = 槽号 × 8。 + +槽序:**全局块**(`n_globals` 个,声明序)→ **FB 实例块**(POU 收集序、实例声明序,每字段一槽)。 + +line1 数据段(56 字节 = 7 槽 × 8B,`0xC8` 起): + +| 槽号 | 偏移 | 内容 | +|---|---|---| +| 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`:`codegen_slice1`(写侧 model/sha 自检)、`vm_cycles`(读侧型号/SHA 拒绝)、`machine_config` +- `STCompiler x.stb --disasm --machine machine.toml`:显示 `model=` 与 `sha=ok|BAD` +- 手工检查:`xxd line1.stb` 对照上表逐段核对 + +## 与执行器的关系 + +`BytecodeExecutor` 只读本格式:头(`cycle_limit`/`dt_ms`/型号标识)、函数表(帧 `nregs` + 代码定位)、数据段(全局 + FB 实例状态)。读取时**型号匹配 + SHA-256 校验**,任一不符 → 报错拒绝。I/O 绑定**不在**映像里,在 sidecar `.runtime.toml`(var → 槽号 → channel/bit)。 diff --git a/Doc/isa/指令与映像.md b/Doc/isa/指令与映像.md new file mode 100644 index 0000000..354fee2 --- /dev/null +++ b/Doc/isa/指令与映像.md @@ -0,0 +1,259 @@ +# 指令与映像 + +isa 模块规范:指令集 + 映像 + 定宽类型。不是编译器,不把 `.st` 编成映像。 + +`compiler` 写 `.stb`、`vm` 读 `.stb`,两边**各自实现**读写;`isa` 归执行器侧只管指令定义。指令 opcode / 类型 / FB 布局的登记见 [`指令配置.md`](指令配置.md)(`compiler/machine.toml`,与代码强校验)。 + +全工程定案见 [`初步计划.md`](../初步计划.md)。**类型、指令编码、映像布局以本文为准**,不要另写一份互相打架的表。头文件实现本文,不另当规范。 + +```text +STCompiler → compiler (编码查 machine.toml;自带 .stb 写) +BytecodeExecutor → vm → isa (isa 只管指令;vm 自带 .stb 读) +``` + +CMake 目标:`isa`(`STATIC`),无依赖;`vm` PUBLIC 链 `isa`。公开头:`isa/include/isa/`。 + +--- + +## 做 / 不做 + +**做** + +- 操作码与 32 位指令布局 +- 映像头与段(函数字节码、全局 / FB 槽、工程哈希) +- `BOOL` / `INT` / `TIME` 宽度与溢出规则 +- 编解码、工程哈希、一条指令的文本转储(反汇编,给 STCompiler `--disasm` 与测试打印) + +**不做** + +- toml 字段、关键字表、符号表、AST +- 扫描周期循环、I/O 采样 +- 短路语义(`AND`/`OR` 指令只组合已算好的值;短路由 `compiler` 编成跳转) + +指令表可作为内部 IR 的文本形态,不必当主语言。 + +--- + +## 特性 + +| 项 | 第一版 | +|---|---| +| `BOOL` | `uint8_t`,只允许 0/1 | +| `INT` | `int16_t`(IEC `INT`) | +| `TIME` | `int64_t` 毫秒(有符号);与 `INT` 不混用 | +| 整数溢出 | **饱和**(夹到 `INT` 最小/最大) | +| 字节序 | 小端 | +| 指令宽度 | 32 bit,一种主布局 | +| 寄存器 | 下标 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 采样 | +| 不做 | `REAL`、隐式宽化 | + +--- + +## 类型 + +C++20 定宽: + +- `BOOL`:`uint8_t`,0/1 +- `INT`:`int16_t`;加减乘除溢出饱和 +- `TIME`:`int64_t` 毫秒,有符号 + +`compiler` 与 `vm` 都按同一规则,不再另选溢出策略。 + +`TIME` 选 `int64_t` 的理由(冻结,不再反复问): + +- 有符号:`TOF` 剩余时间、两个时间点相减会出负值,免去无符号边界处理 +- 64 位:为将来 `DATE_AND_TIME`(毫秒时间戳)与 `TON` / `TOF` / `TP` 的 64 位定时器预留;毫秒量程约 2.9 亿年,扫描周期场景永不溢出 +- 代价:槽位 8 字节对齐。**槽位规则**(12.9 方案 a):每槽 8 字节定宽,`BOOL` 用低 1 字节、`INT` 用低 2 字节(小端)、`TIME` 全 8 字节;槽号 × 8 = 数据偏移,无 padding。 + +--- + +## 指令 + +定宽 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(指令条数),**相对下一条指令**:目标下标 = 当前下标 + 1 + off +- `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_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 实例固定布局,字段偏移编译期算死。 + +### 反汇编格式(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` 无符号十进制。 + +--- + +## 映像 + +魔数 `STSC`,版本 `1`。全部整数小端。文件 = 头 + 五个段 + **文件尾校验**,段序:常量表 → 函数表 → 字节码 → FB 布局 → 数据。偏移记在头里,段必须连续、偏移单调不减,末段终点不超过文件长度(文件尾在数据段之后)。 + +> 12.13 修订:头新增 **型号标识[32]**(原头 72 字节 → 104 字节,段偏移基准同步后移); +> 文件尾新增 **SHA-256[32]**(对文件尾之前全部内容计算,compiler 写侧 / vm 读侧各自实现)。 +> 执行器读取时:型号标识与自身支持型号不匹配 → 报错;SHA-256 不匹配 → 报错(损坏/篡改)。 + +### 头(104 字节) + +| 偏移 | 宽 | 字段 | +|---|---|---| +| 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` | +| 72 | 32 | **型号标识**(定长 ASCII,含版本,如 `"STATOR" + "1"` 拼 32 字节补 `'\0'`) | + +### 文件尾(SHA-256) + +数据段之后追加 **32 字节 SHA-256**:对文件尾之前全部内容计算。执行器必须校验通过才运行;compiler 写入。 + +### 常量表一行(12 字节) + +`[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`,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`。 + +`[[io.input]]` / `[[io.output]]` 不进映像。 + +--- + +## 映像文件(`.stb`)与 sidecar + +编译产物是单文件 `.stb`:映像头 + 各段按本文布局连续排,魔数 `STSC`、版本 `1`、小端。`STCompiler` 写,`BytecodeExecutor` 读。 + +- `dt_ms` / `cycle_limit` 在映像头,执行器只认映像,不重复配置。 +- 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` 为准)。 + +--- + +## 目录与 CMake + +```text +isa/ + CMakeLists.txt + README.md # 代码目录入口,指向本文 + include/isa/ + Types.h # 宽度、饱和、槽值表示 + Op.h # 操作码枚举 + 助记符 + Instr.h # 32-bit 打包/拆字段(inline) + Image.h # 头、段、函数表/常量表/FB 布局 + Encode.h # 编解码、哈希、反汇编一行 + src/ + Encode.cpp + Image.cpp +``` + +```cmake +add_library(isa STATIC src/Encode.cpp) +target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +``` + +`vm`:`target_link_libraries(vm PUBLIC isa)`;`compiler` 不链 `isa`。 + +--- + +## 实现顺序(对齐初步计划 12.0~12.1) + +1. `Types.h`:三类型 + `sat_add` 等饱和函数(inline 即可) +2. `Op.h` + `Instr.h`:枚举与 32-bit 编解码 +3. `Encode.cpp`:`encode` / `decode` / `disasm` 一条指令 +4. `Image.h` + `Image.cpp`:头布局、FNV-1a、读写缓冲(先能写空映像 + 一条 `RET`) +5. 测试随后在 `tests/` 加编解码往返;本阶段以头文件和库能链为准 + +不在本模块做:VM `switch`、toml、关键字、符号表。 diff --git a/Doc/isa/指令配置.md b/Doc/isa/指令配置.md new file mode 100644 index 0000000..88baef7 --- /dev/null +++ b/Doc/isa/指令配置.md @@ -0,0 +1,176 @@ +# 机器定义文件(machine.toml) + +指令集与类型的**唯一事实来源**,**只给编译器**(STCompiler/compiler)使用。执行器(vm)不读本文件——它是下位机程序,内建全部指令。 + +## 架构(最终定案) + +```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(操作数形态) → 参数名(操作数语义) + PLAIN/INSTANCE RR/RRR/IMM/SLOT/ rd/rs/const_id/ + JMP/JC/CALL/CAL/NONE slot/off/fn_id/instance +``` + +| 大类 | 操作对象 | 现有指令 | +|---|---|---| +| **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 能表达。 + +## 配置文件格式(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 +型号标识与自身支持型号不匹配 → 直接报错 +SHA-256 不匹配 → 报错(文件损坏/篡改) +``` + +## 强校验清单(加载/读取时,任一不符 → 报错退出) + +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 +加普通指令: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 被旧/新执行器按型号校验决定是否接受 +``` + +## 执行计划(分支 dev_ops_config) + +``` +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. 文档 + 提交 +``` + +## 实现前确认项(4 项,未定案) + +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`) diff --git a/Doc/vm/扫描周期.md b/Doc/vm/扫描周期.md new file mode 100644 index 0000000..33c0bd1 --- /dev/null +++ b/Doc/vm/扫描周期.md @@ -0,0 +1,122 @@ +# 扫描周期 + +vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [`指令与映像.md`](../isa/指令与映像.md)。 + +运行时是 **一台 VM、一个入口、一份映像**。不链接 `compiler`。扫描周期与确定性见 [`初步计划.md`](../初步计划.md) 第 2、8 节。 + +## 扫描周期 + +```text +1. 按 sidecar 的 io 绑定,采样 → I / 输入全局 +2. 从 PROGRAM MAIN 的 pc=0 执行到 RET +3. 写回 Q / 输出全局 +4. 用本周期 Δt 推进内置 FB(不用 wall clock) +``` + +## 边界 + +- 只认 `.stb` 映像(`vm` 自带读实现);读取时**型号标识匹配 + SHA-256 校验**,不符/篡改 → 拒绝 +- 类型与溢出按 [`指令与映像.md`](../isa/指令与映像.md)(`INT` 饱和);指令定义归 `isa` +- 局部 / `VAR` = 固定寄存器;`nregs` 在函数头 +- `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 上。 + +--- + +## 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`(指令定义)+ 自带 `vm::Image`(.stb 读) + +### 步骤 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:定时器与边沿(已落地) + +- 执行时用映像 `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`(已落地) + +- 手工拼小映像(`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:验证 + 提交(已落地,ctest 14/14) + +- `cmake --build` + `ctest`(新增 `vm_cycles` 后 10/10 全绿);20 用例扫描:14 正例执行通过 diff --git a/Doc/vm/指令执行.md b/Doc/vm/指令执行.md new file mode 100644 index 0000000..f6ade00 --- /dev/null +++ b/Doc/vm/指令执行.md @@ -0,0 +1,167 @@ +# 指令执行流程(12.9) + +`vm` 模块的指令执行语义:取指、译码、执行、故障。规范以 [`指令与映像.md`](../isa/指令与映像.md) 为准,本文描述**执行器怎么跑一条指令**。执行模型见 [`初步计划.md`](../初步计划.md) 第 2 节。 + +## 执行模型 + +```text +加载(Machine::create):解析 .stb(vm 自读实现)→ 型号匹配(内建 STATOR1)→ SHA-256 校验 + 任一不符 → 直接拒绝(不创建机器) + +每个扫描周期(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/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG 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_TP / CAL_CTU / CAL_CTD / CAL_CTUD / CAL_R_TRIG / CAL_F_TRIG) + +`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 为真: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 + +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 真(下降沿) +``` + +## 故障 + +| 故障 | 触发 | 周期行为 | +|---|---|---| +| `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 93494d5..4e9e5ae 100644 --- a/Doc/初步计划.md +++ b/Doc/初步计划.md @@ -1,4 +1,4 @@ -# 子集 ST + toml 工程 + 寄存器 VM +# StScan:子集 ST + toml 工程 + 寄存器 VM 独立方案说明。调研全文见 `PLC-DSL.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、一个入口、一份映像**。多文件只影响源码怎么切、怎么链接。 @@ -202,6 +202,8 @@ motor.st / main.st ## 7. 寄存器机与指令 +编码宽度、操作码取值、映像头与定宽类型以 [`isa/指令与映像.md`](isa/指令与映像.md) 为准。本节只列助记符。 + - 局部 / `VAR` = 固定寄存器;表达式临时值往后编号;`nregs` 写进函数头。 - `I/Q/M` 走映像指令,不是通用寄存器。 - 每个 FB 实例一块固定布局,字段偏移编译期算死。 @@ -217,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 ``` @@ -269,14 +271,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;STCompiler 入口在 src/main.cpp +vm/ # 扫描周期、寄存器、映像、FB(库) +executor/ # BytecodeExecutor:加载 .stb + sidecar,喂 I、收 Q、Δt、回放 +examples/line1/ # 第 11 节最小样例(给人看) +tests/ # 冻规则用的正例 / 负例 ``` --- @@ -322,8 +325,264 @@ 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`。 + +--- + +### 12.0 骨架(约 1 天) + +CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCompiler`、`BytecodeExecutor` 先打印版本即可。 + +- 落下 `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 | 给内置 FB 定时器,不是 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` +`THEN` `DO` (12.5 修订补入:`IF…THEN` / `WHILE…DO` 语法需要) +`AND` `OR` `NOT` +`TRUE` `FALSE` +`TON` `TOF` `TP` `CTU` `CTD` `CTUD` `R_TRIG` `F_TRIG` (12.11 修订:内置 FB 扩为 8 个) + +其余一律当标识符或直接拒绝(`VAR_IN_OUT`、`REF`、`CLASS` 等出明确错误)。 + +**符号表一行** + +`name` / `kind` / `type` / `address` / `source_file` +`kind` 至少:`global` / `external` / `local` / `input` / `output` / `pou` / `fb_instance` / `const`。 + +**指令(进 `isa`,约 20 条)** + +布局、操作码、映像、哈希、饱和以 [`isa/指令与映像.md`](isa/指令与映像.md) 为准。第 7 节只列助记符。 + +完成:`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` `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 里保留短路语义)、比较、加减乘除 + +拒绝时给明确错误,不要 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 推进内置 FB +``` + +- 译码 + 大 `switch`;寄存器文件按函数头 `nregs` 分配。 +- 每周期计指令数,超过 `cycle_limit` 报故障(用例 6)。 +- 调用栈有界;`CALL`/`RET` 恢复 `pc` 与帧。 +- 无 GC、无堆、无线程。同一映像 + 同一组 `I` + 同一 `Δt` → `Q`/`M`/FB 一致。 +- 内置 FB:`CAL_TON`/`CAL_TOF`/`CAL_CTU` 只用映像里的 Δt,不读系统时钟。 + +完成:对手工拼的小映像能跑赋值和跳转;再接编译器产出的映像。 + +--- + +### 12.10 双可执行:STCompiler 与 BytecodeExecutor(约 2~3 天) + +编译与执行拆成两个可执行,映像 `.stb` 是中间产物: + +```text +STCompiler -o .stb --machine +BytecodeExecutor .stb [--cycles N] +``` + +- `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 --machine compiler/machine.toml` 产出两个文件;`BytecodeExecutor line1.stb` 能跑若干周期。 + +> 12.13 修订:指令 opcode / 类型 / FB 布局的登记移入 `compiler/machine.toml`(见 [`指令配置.md`](isa/指令配置.md)), +> `STCompiler` 编译路径必填 `--machine`。 + +--- + +### 12.11 闭环与冻测试(约 1 周) + +- 点亮第 12.2 节 20 个用例;负例检查报错类别,不查英文原文。 +- `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 + 全局**,扫描周期可重复。 + +--- + +### 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、两个可执行跑起来 | +| 6 | 定时器/计数器、io、Δt、`line1` 语义 | +| 7~8 | 20 用例、回放、单步、补漏 | + +周 4 结束时就应能跑一个「读 I、写 Q」的假 MAIN。不要等 FB 和 TON 齐了才第一次执行。 + +--- + +### 12.13 依赖方向(全程遵守) + +```text +STCompiler → compiler (编码查 machine.toml;自带 .stb 写) +BytecodeExecutor → vm → isa (isa 只管指令;vm 自带 .stb 读) +``` + +- 指令或映像布局有变:先改 `isa` 和编解码测试,再改两端。 +- 新 ST 特性:先加 12.2 的用例行,再改关键字 / AST / 检查 / 码 / VM。 +- 不要为了一个 pass 新建 CMake 库。 diff --git a/Doc/索引.md b/Doc/索引.md new file mode 100644 index 0000000..5b30e71 --- /dev/null +++ b/Doc/索引.md @@ -0,0 +1,41 @@ +# 文档索引 + +设计文档,不是编译目标。全工程定案在根下;各 CMake 模块一个文件夹,文件名标明用途。 + +```text +doc/ + 索引.md + 初步计划.md + isa/指令与映像.md + isa/stb文件格式.md + isa/指令配置.md + compiler/编译管线.md + compiler/STCompiler使用说明.md + compiler/词法.md + compiler/语法.md + compiler/符号表与链接.md + compiler/类型检查.md + compiler/寄存器码.md + vm/扫描周期.md + vm/指令执行.md + executor/执行器入口.md +``` + +| 路径 | 内容 | +|---|---| +| [`初步计划.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 字面量 | +| [`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 边界 | +| [`vm/指令执行.md`](vm/指令执行.md) | 12.9 指令执行流程:帧/调用栈、指令表、定时器、故障 | +| [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | + +代码目录里的 README 只作入口;类型、指令、映像以 `isa/指令与映像.md` 为准。 diff --git a/README.md b/README.md index ce3f380..4f7c4d5 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,30 @@ -# 子集 ST + 寄存器 VM +# StScan -IEC 风格的子集 ST:用 `project.toml` 组织多个 `.st`,编译成单一映像,由寄存器虚拟机按扫描周期执行。 +子集 ST + 扫描周期虚拟机。IEC 风格的子集 ST:用 `project.toml` 组织多个 `.st`,编译成单一映像,由寄存器虚拟机按扫描周期执行。 -方案见 [`Doc/初步计划.md`](Doc/初步计划.md)。 +方案见 [`Doc/初步计划.md`](Doc/初步计划.md),模块文档见 [`Doc/索引.md`](Doc/索引.md)。 ```text -多个 .st - ↑ project.toml 列出 +多个 .st + machine.toml(指令/类型/FB 布局) + ↑ project.toml 列出源文件;machine.toml 唯一事实来源 ↓ -compiler(词法 / 语法 → 符号表 → 类型检查 → 寄存器码 → 链接) +STCompiler(compiler 库:词法 / 语法 → 符号表 → 类型检查 → 寄存器码 → 链接;编码查 machine.toml) ↓ -单一映像(函数字节码 + 全局 / FB 数据槽 + 工程哈希) +.stb 映像(头 104 含型号标识 + 段 + SHA-256 文件尾)+ .runtime.toml sidecar ↓ -vm 按扫描周期执行;host 喂 I、收 Q、给 Δt、回放 +BytecodeExecutor(vm 库按扫描周期执行;型号/SHA 校验;喂 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,32 +40,33 @@ vm 按扫描周期执行;host 喂 I、收 Q、给 Δt、回放 ## 模块与 CMake 目标 -四个链接目标,与目录一一对应。`compiler` 与 `vm` 不互相链接,合同放在 `isa`。 +三个静态库与两个可执行,与目录一一对应。`compiler` 与 `vm` 不互相链接。指令/类型/FB 布局的**唯一事实来源是 `compiler/machine.toml`**(编译器运行时加载);`isa` 归执行器侧(`vm` 依赖),只管指令定义。依赖方向:`STCompiler → compiler`、`BytecodeExecutor → vm → isa`。 | 目录 | 目标 | 类型 | 依赖 | 职责 | |---|---|---|---|---| -| `isa/` | `isa` | `STATIC` 或 `INTERFACE` | 无 | 约 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 推进定时器 / 计数器、回放 | +| `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**(不匹配/篡改 → 拒绝)。 -### `host` +### `executor`(BytecodeExecutor) -第一版做成一条命令:编译 + 跑周期。不必再拆独立的编译器可执行文件。 +`STCompiler` 编译出 `.stb`(含型号标识 + SHA-256)+ `.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/cmake/README.md b/cmake/README.md index 4606336..b68d32a 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` | C17、C++20;`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..f34e2ab --- /dev/null +++ b/cmake/Standards.cmake @@ -0,0 +1,8 @@ +# C / C++ 语言标准。在 project() 之后 include,对后续目标生效。 +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt new file mode 100644 index 0000000..08045cf --- /dev/null +++ b/compiler/CMakeLists.txt @@ -0,0 +1,30 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(Compiler + VERSION 0.1 + DESCRIPTION "ST / toml 编译器") + +# 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 +# 指令 opcode / 类型 / FB 布局来自 machine.toml(MachineConfig),不依赖 isa +add_library(compiler STATIC + ./src/Lexer.cpp + ./src/Project.cpp + ./src/Parser.cpp + ./src/Linker.cpp + ./src/Typecheck.cpp + ./src/Codegen.cpp + ./src/MachineConfig.cpp + ./src/Stb.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) + +# 可执行入口:STCompiler -o .stb +add_executable(STCompiler + ./src/main.cpp) + +target_link_libraries(STCompiler PRIVATE compiler) diff --git a/compiler/README.md b/compiler/README.md index 239d95a..0b567c3 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -1,28 +1,7 @@ # compiler -ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),依赖 `isa`。 +ST / toml 编译器。CMake 目标:`compiler`(`STATIC`),**不依赖 `isa`**——指令 opcode / 类型元数据 / FB 布局来自 `compiler/machine.toml`(见 [`指令配置.md`](../doc/isa/指令配置.md));自带 `.stb` 写实现。可执行入口 `STCompiler`。 -读齐工程后走直通 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/`,本库私有链接。 +- **使用说明**(构建 / 用法 / 工程文件 / ST 子集 / 错误类别):[`doc/compiler/STCompiler使用说明.md`](../doc/compiler/STCompiler使用说明.md) +- 边界与管线:[`doc/compiler/编译管线.md`](../doc/compiler/编译管线.md) +- 语言规则:[`doc/初步计划.md`](../doc/初步计划.md) diff --git a/compiler/include/compiler/Codec.h b/compiler/include/compiler/Codec.h new file mode 100644 index 0000000..3a38dfc --- /dev/null +++ b/compiler/include/compiler/Codec.h @@ -0,0 +1,59 @@ +/** + * @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; + + /** + * @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); + /// @brief a|b 拼 16 位无符号数(const_id / slot / fn_id)。 + uint16_t imm16_of(Instr w); + /// @brief a|b 为有符号相对偏移(单位:指令条数,相对下一条指令)。 + int16_t off16_of(Instr w); + + /** + * @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 new file mode 100644 index 0000000..1a52a78 --- /dev/null +++ b/compiler/include/compiler/Codegen.h @@ -0,0 +1,38 @@ +/** + * @file Codegen.h + * @brief 寄存器码生成(12.8,切片 1:MOVE / LOADK / RET) + * @author + * @date 2026-08-21 + * + * @details 代码生成:输入 Project + Unit(AST)+ LinkResult,输出 .stb 映像字节。 + * 寄存器分配:r0 结果、r1..r7 参数、r8+ 变量/临时;跳转偏移相对下一条。 + */ + +#pragma once + +#include +#include +#include + +#include "compiler/Linker.h" +#include "compiler/MachineConfig.h" +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + /** + * @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 new file mode 100644 index 0000000..371e085 --- /dev/null +++ b/compiler/include/compiler/Lexer.h @@ -0,0 +1,120 @@ +/** + * @file Lexer.h + * @brief ST 子集词法分析 + * @details 本文件是词法层对外接口:定义 token 类型(Tok)、token 载体 + * (Token)与两个入口(lex / lex_file)。扫描器实现见 Lexer.cpp, + * 关键字与 token 集见 Doc/compiler/词法.md。 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include + +namespace compiler { + + /** + * @brief token 类型枚举。 + * @details 关键字与 token 集见 Doc/compiler/词法.md;关键字共 25 个, + * 与 12.1 冻结表一致(12.5 修订补入 THEN/DO)。顺序冻结后 + * 不得插值改动,只能追加到末尾。 + */ + enum class Tok { + // 关键字(25 个,与 12.1 冻结表一致) + 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 = 毫秒) + // 符号 + 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; ///< token 类型 + std::string text; ///< 标识符/关键字的小写文本;字面量原文 + int64_t int_value; ///< INT_LIT / TIME_LIT(毫秒)的字面量值 + std::string source_file; ///< 来源文件名 + uint32_t line; ///< 起始行(从 1 起) + uint32_t col; ///< 起始列(从 1 起) + }; + + /** + * @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); + + /** + * @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 new file mode 100644 index 0000000..65338f1 --- /dev/null +++ b/compiler/include/compiler/Linker.h @@ -0,0 +1,129 @@ +/** + * @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 + */ + +#pragma once + +#include +#include +#include +#include + +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + /** + * @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; ///< 声明列号 + }; + + /** + * @brief FB 类型字段(实例布局用;v1 字段类型仅 BOOL/INT/TIME)。 + */ + struct FbField { + std::string name; ///< 字段名 + TypeKind type = TypeKind::Bool; ///< 字段类型(仅标量) + }; + + /** + * @brief 一个 FB 类型的字段布局(供实例分配与字段引用解析)。 + */ + struct FbLayout { + std::string type_name; ///< 类型名(小写) + std::vector fields; ///< 段序:input → output → 内部 var + }; + + /** + * @brief 一个源文件的 AST(供链接输入)。 + */ + struct SourceUnit { + std::string path; ///< 源文件路径 + Unit ast; ///< 解析出的 AST + }; + + /** + * @brief 链接结果:全局槽表 + FB 类型布局 + 各 POU 作用域 + 函数表顺序。 + */ + struct LinkResult { + std::vector globals; ///< 全局槽表:下标即槽号(声明顺序) + std::map global_index; ///< 名 → 槽号 + + std::map fb_types; ///< 用户 FB 类型布局(按类型名) + + /** + * @brief 每 POU 的局部符号与 FB 实例。 + */ + struct PouScope { + 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; ///< 全部 POU 作用域 + + std::vector fn_order; ///< 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序) + }; + + /** + * @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); + + /** + * @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); + + /** + * @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 new file mode 100644 index 0000000..5bf8fce --- /dev/null +++ b/compiler/include/compiler/MachineConfig.h @@ -0,0 +1,164 @@ +/** + * @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 { + + /** + * @brief 内建基元(编译器内建,C 语义)。 + * @details 元数据写在代码而非 machine.toml;配置类型的 base 必须命中本表。 + */ + struct PrimType { + const char* name; ///< 基元名(bit / int8..uint64 / float32 / float64) + uint32_t width; ///< 宽度(字节) + bool is_signed; ///< 是否带符号 + bool is_float; ///< 是否浮点 + }; + + /** + * @brief 配置类型 = 基元别名(+ 值域约束 + .stb 契约 tag)。 + * @details 对应 machine.toml [[type]] 行:base 必为内建基元名;range 可选且 + * min ≤ max;tag 与 .stb 常量表契约一致(0=BOOL、1=INT、2=TIME,唯一)。 + */ + struct ConfigType { + 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; ///< 助记符(与 .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; ///< 字段名(如 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; ///< 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: + /** + * @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_; ///< 型号名(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 new file mode 100644 index 0000000..24b7d8c --- /dev/null +++ b/compiler/include/compiler/Parser.h @@ -0,0 +1,214 @@ +/** + * @file Parser.h + * @brief ST 子集递归下降语法分析(AST 定义 + 入口) + * @details 本文件定义语法层全部公开类型(AST 节点与相关枚举)与两个 + * 解析入口(parse_pous / parse_pous_file)。解析器实现见 + * Parser.cpp,文法详见 Doc/compiler/语法.md。 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include +#include + +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 类型名(小写) + }; + + // ---- 变量段 ---- + + /** + * @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; ///< 声明起始列 + }; + + /** + * @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; ///< 段内声明(按出现顺序) + }; + + // ---- 表达式 ---- + + /** + * @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, ///< 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; ///< 二目运算(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 }; + + /** @brief 语句节点(前置声明;Stmt 与 IfBranch 相互引用)。 */ + struct Stmt; + + /** + * @brief FB 调用实参:命名参数(名 := 表达式)。 + */ + struct FbArg { + std::string name; ///< 形参名(小写) + std::unique_ptr value; ///< 实参表达式 + }; + + /** + * @brief IF 语句的一个 ELSIF 分支:条件 + 分支体。 + */ + struct IfBranch { + std::unique_ptr cond; ///< 分支条件 + std::vector body; ///< 分支体(条件成立时执行) + }; + + /** + * @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; ///< 语句种类 + // 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; ///< ELSIF 分支列表 + std::vector else_body; ///< ELSE 分支体 + // FbCall + 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; ///< POU 种类 + std::string name; ///< POU 名(小写) + TypeRef result_type; ///< 返回类型(仅 FUNCTION 才有) + std::vector blocks; ///< 变量段(按声明顺序) + std::vector body; ///< 语句体 + std::string source_file; ///< 来源文件名 + }; + + /** + * @brief 一个 .st 文件的解析结果。 + * @details GVL 文件形态(无 POU)时只有 globals 非空。 + */ + struct Unit { + std::vector globals; ///< 顶层 VAR_GLOBAL 段(仅 GVL 文件) + std::vector pous; ///< POU 列表(按出现顺序) + }; + + /** + * @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 new file mode 100644 index 0000000..fe6882e --- /dev/null +++ b/compiler/include/compiler/Project.h @@ -0,0 +1,80 @@ +/** + * @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 + */ + +#pragma once + +#include +#include +#include + +namespace compiler { + + /** + * @brief I/O 绑定(project.toml [[io.*]];不创造变量,slot 由链接结果解析)。 + */ + struct IoBinding { + 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]] + }; + + /** + * @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 所在目录(解析相对路径用) + }; + + /** + * @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); + + /** + * @brief 编译文件集合:files.st ∪ gvl.file,去重(gvl 已在 files.st 则跳过)。 + * @details 保持 files.st 顺序,gvl 追加在后;相对路径以 base_dir 为基准解析。 + * @param p 工程定义 + * @return 编译文件路径集合 + */ + std::vector compile_files(const Project& p); + + /** + * @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 new file mode 100644 index 0000000..0143493 --- /dev/null +++ b/compiler/include/compiler/Stb.h @@ -0,0 +1,206 @@ +/** + * @file Stb.h + * @brief 编译器自带的 .stb 映像规范(写侧)+ 只读视图 + FNV-1a + * @author + * @date 2026-08-21 + * + * @details 执行器(vm)各有自己的读写实现;格式契约见 Doc/isa/指令与映像.md。 + * 12.13 修订:头 104 = 原 72 + 型号标识[32] @72,文件尾 SHA-256[32]。 + * 常量表 tag 契约:0=BOOL、1=INT、2=TIME;工程哈希用 FNV-1a 64。 + */ + +#pragma once + +#include +#include +#include +#include + +#include "compiler/Project.h" + +namespace compiler { + + // ---- .stb 格式常量(契约;12.13 修订:头 104 = 原 72 + 型号标识[32],文件尾 SHA-256[32])---- + /// 映像魔数:"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; + + /** + * @brief 常量表一行。 + * @details tag 契约:0=BOOL、1=INT、2=TIME。 + */ + struct ConstEntry { + uint32_t tag = 0; ///< 类型 tag(0=BOOL、1=INT、2=TIME) + uint64_t value = 0; ///< 常量值 + }; + + /** + * @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); + + /** + * @brief 一次性 SHA-256(文件完整性校验)。 + * @param data 输入字节 + * @param len 字节数 + * @param out 32 字节摘要输出(大端) + */ + void sha256(const uint8_t* data, size_t len, uint8_t out[kSha256Size]); + + /** + * @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]); + + /** + * @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; ///< 字节码段内偏移(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; + + /// 型号标识字符串(头 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; ///< 入口函数 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; ///< 数据段偏移 + }; + + /** + * @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); + + /** + * @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 new file mode 100644 index 0000000..f71d912 --- /dev/null +++ b/compiler/include/compiler/TypeInfo.h @@ -0,0 +1,62 @@ +/** + * @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 { + + /** + * @brief 语言类型元数据。 + * @details 由配置类型行 + 内建基元合成;config 或 prim 未命中时 + * 对应访问器取安全默认值,operator bool 为 false。 + */ + struct TypeMeta { + 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; } + }; + + /** + * @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 new file mode 100644 index 0000000..c8dfca4 --- /dev/null +++ b/compiler/include/compiler/Typecheck.h @@ -0,0 +1,43 @@ +/** + * @file Typecheck.h + * @brief 类型检查 + * @details 本文件定义类型检查的求值类型与对外入口(实现见 Typecheck.cpp): + * - TType:表达式求值类型(v1 三型,无隐式宽化) + * - check_project:对工程做类型检查(在链接成功后调用),失败 err 前缀 "type error" + * 规则详见 Doc/compiler/类型检查.md。 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + /** + * @brief 表达式求值类型(v1 三型,无隐式宽化)。 + * @details 各成员含义: + * - Bool:布尔量(逻辑运算 / 条件表达式) + * - Int:整数(算术运算) + * - Time:时间量(仅比较,无算术);INT 与 TIME 不混用 + */ + enum class TType { Bool, Int, Time }; + + /** + * @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/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"]] diff --git a/compiler/src/Codec.cpp b/compiler/src/Codec.cpp new file mode 100644 index 0000000..96f7c65 --- /dev/null +++ b/compiler/src/Codec.cpp @@ -0,0 +1,99 @@ +/** + * @file Codec.cpp + * @brief 指令字编解码 + 配置驱动反汇编(编译器侧) + * @author + * @date 2026-08-21 + * + * @details pack / op_of / imm16_of / off16_of 与执行器 isa 字节布局一致; + * disasm 由 MachineConfig 驱动(opcode 名 / format / 参数名来自配置)。 + */ + +#include "compiler/Codec.h" + +#include + +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) + | (static_cast(a) << 16) + | (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; + } + 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 new file mode 100644 index 0000000..721a6d2 --- /dev/null +++ b/compiler/src/Codegen.cpp @@ -0,0 +1,974 @@ +/** + * @file Codegen.cpp + * @brief 寄存器码生成(配置驱动;compiler 不依赖 isa) + * @author + * @date 2026-08-21 + * + * @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 = 槽号 + * + * 函数清单: + * - 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(帧约定分配;FB 占位) + * - begin_stmt / alloc_temp 临时寄存器管理 + * - compile_stmt 语句编译(赋值 / IF / WHILE / FB 调用) + * - 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 对外入口 + */ + +#include "compiler/Codegen.h" + +#include +#include +#include +#include +#include + +#include "compiler/Codec.h" +#include "compiler/MachineConfig.h" +#include "compiler/Stb.h" +#include "compiler/TypeInfo.h" + +namespace compiler { +namespace { + + /** + * @brief 代码生成器(寄存器码)。 + * @details 流程:布局数据区 → 布局 FB 实例 → 逐 POU 建函数 → 拼映像。 + * 寄存器分配:r0 结果、r1..r7 参数(调用约定区,输入只读)、r8+ 变量/临时; + * 跳转偏移相对下一条(目标 = 当前 + 1 + off)。失败统一经 fail() 写 + * err(前缀 "codegen error")。 + */ + class Builder { + public: + /** + * @brief 构造生成器 + * @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, const MachineConfig& cfg, + std::vector* image, std::string* err) + : proj_(proj), units_(units), link_(link), cfg_(cfg), image_(image), err_(err) { + // io 绑定分类(名已折小写;不创造变量,只影响操作码选择) + for (const 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 布局数据区 → 布局 FB 实例 → 逐 POU 建函数 → 拼映像 + * @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) { + continue; + } + FuncCtx f; + f.name = sc.name; + f.pou_name = sc.name; + if (!build_function(*pou, &f)) { + return false; + } + funcs_.push_back(std::move(f)); + } + return assemble_image(); + } + + private: + /** + * @brief 记录错误并返回失败。 + * @param msg 错误消息(自动加前缀 "codegen error: ") + * @return 恒 false(便于 return fail(...) 连写) + */ + bool fail(const std::string& msg) { + if (err_) { + *err_ = "codegen error: " + msg; + } + return false; + } + + /** + * @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)---- + /// 双寄存器指令(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) { + if (p.name == name) { + return &p; + } + } + } + return nullptr; + } + + // 每函数的编译态 + /** + * @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 名(结果寄存器映射) + }; + + // FB 实例:字段名 → 数据区槽号 + /** + * @brief FB 实例布局。 + * @details 字段槽号 = 实例基槽 + 字段序号(跨周期持久)。 + */ + struct InstFields { + 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; + if (static_cast(f.nlocals) + f.temp_used > f.nregs) { + f.nregs = f.nlocals + f.temp_used; + } + return r; + } + + /** + * @brief 编译一个 POU + * @details 支持 PROGRAM / FUNCTION / FUNCTION_BLOCK: + * FB 不生成业务字节码(调用点内联),只出空函数占位保持 fn_id 一致; + * 帧约定(见 Doc/compiler/寄存器码.md):结果 r0、输入 r1..r7(只读)、 + * 变量与临时全部从 r8 起 + */ + bool build_function(const POU& pou, FuncCtx* f) { + if (pou.kind == PouKind::FunctionBlock) { + f->nlocals = 8; + f->nregs = 8; + f->code.push_back(E_ret()); + return true; + } + 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) { + if (b.section == VarSection::External || b.section == VarSection::Global) { + continue; + } + 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) { + continue; + } + f->regs[d.name] = r++; + } + } + f->nlocals = r; + f->nregs = r; + for (const Stmt& st : pou.body) { + begin_stmt(*f); + if (!compile_stmt(*f, st)) { + return false; + } + } + f->code.push_back(E_ret()); + 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); + } + 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"); + } + // 内联 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(E_slot("STORE_GLOBAL", t, fit->second)); + return true; + } + } + const auto it = f.regs.find(st.target); + if (it == f.regs.end()) { + return store_target(f, st.target, *st.value); + } + if (f.is_function && it->second >= 1 && it->second <= 7) { + return fail("cannot write function input '" + st.target + "'"); + } + 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) { + 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(E_slot("STORE_GLOBAL", t, it->second)); + } + // 内建 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); + if (fb == nullptr) { + return fail("no FB type '" + inst->type_name + "'"); + } + 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) { + ch = static_cast(std::toupper(static_cast(ch))); + } + 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; + for (const Stmt& s : fb.body) { + begin_stmt(f); + if (!compile_stmt(f, s)) { + return false; + } + } + inline_fields_ = saved; + 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); + if (!compile_expr(f, *st.cond, t)) { + return false; + } + const size_t jf_idx = f.code.size(); + f.code.push_back(E_jc("JF", t, 0)); + 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(E_jmp(0)); + patch_jump(f, jmp_idx, loop); + patch_jump(f, jf_idx, f.code.size()); + 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}); + 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; + + 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(E_jc("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(E_jmp(0)); + end_jmps.push_back(jmp_idx); + } + patch_jump(f, jf_idx, f.code.size()); + } + 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 编译左值存储(全局槽)。 + * @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()) { + 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; + } + f.code.push_back(E_slot(store_name(target), tmp, static_cast(git->second))); + 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) { + 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) { + if (inline_fields_) { + const auto fit = inline_fields_->field_addr.find(e.name); + if (fit != inline_fields_->field_addr.end()) { + 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(E_rr("MOVE", rd, sit->second)); + return true; + } + const auto git = link_.global_index.find(e.name); + if (git != link_.global_index.end()) { + 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 + "'"); + } + 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(E_slot("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); + 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(E_rrr(arith_name(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)) { + return false; + } + const uint8_t r = alloc_temp(f); + if (!compile_expr(f, *e.rhs, r)) { + return false; + } + f.code.push_back(E_rrr(cmp_name(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)) { + return false; + } + f.code.push_back(E_rr("NOT", rd, t)); + return true; + } + if (e.kind == ExprKind::And || e.kind == ExprKind::Or) { + if (!compile_expr(f, *e.lhs, rd)) { + return false; + } + const char* jname = (e.kind == ExprKind::And) ? "JF" : "JT"; + const size_t jmp_idx = f.code.size(); + 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(E_rr("MOVE", rd, t)); + patch_jump(f, jmp_idx, f.code.size()); + return true; + } + if (e.kind == ExprKind::Call) { + 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(E_rr("MOVE", static_cast(1 + i), args[i])); + } + 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"); + } + + /** + * @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) { + return static_cast(i); + } + } + 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)); + 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)); + } + + // ---- 操作码名(MachineConfig 已强校验存在)---- + /// 四则运算操作码名(Add→"ADD" 等;未知回退 "ADD")。 + static const char* arith_name(ExprKind k) { + switch (k) { + case ExprKind::Add: return "ADD"; + case ExprKind::Sub: return "SUB"; + case ExprKind::Mul: return "MUL"; + case ExprKind::Div: return "DIV"; + default: return "ADD"; + } + } + /// 比较操作码名(Eq→"CMP_EQ" 等;未知回退 "CMP_EQ")。 + static const char* cmp_name(BinOp op) { + switch (op) { + 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"; + } + } + /// 加载操作码选择: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) { + return fail("data area exceeds slot range"); + } + bool has_init = false; + int64_t init = 0; + init_of(s.name, &has_init, &init); + const int64_t v = has_init ? init : 0; + const size_t off = static_cast(s.address) * 8; + data_.resize(off + 8, 0); + // 初值按类型元数据写(配置别名 → 基元宽度/浮点) + const TypeMeta tm = type_meta(cfg_, s.type_name); + if (!tm) { + return fail("no type in machine.toml for '" + s.type_name + "'"); + } + if (tm.is_float()) { + return fail("float initializer not supported yet"); + } + 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 (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); + } + } else { + data_[off] = v ? 1 : 0; + } + } + 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; + 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 + "'"); + } + InstFields inst; + 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; + } + cur += static_cast(lay->second.fields.size()); + instances_[sc.name + "/" + s.name] = inst; + any = true; + } + } + if (any) { + data_.resize(static_cast(cur) * 8, 0); + } + 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) { + 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; + } + + /** + * @brief 取常量表 id(无则追加) + * @param type_name 语言类型名("BOOL"/"INT"/"TIME")→ 配置 tag(契约) + * @param value 常量值 + * @return const_id(u16) + */ + uint16_t const_id(const char* type_name, int64_t value) { + 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); + } + } + consts_.push_back({tag, static_cast(value)}); + 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 + + static_cast(consts_.size()) * static_cast(kConstEntrySize); + uint32_t off_code = off_funcs + + 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; + } + const uint32_t off_data = off_code + code_total; + const uint32_t off_end = off_data + static_cast(data_.size()) + + static_cast(kSha256Size); + + std::vector& b = *image_; + b.assign(off_end, 0); + 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 = 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, 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); + 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); + 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 * kFuncRowSize; + put_le32(b, o, f.nregs); + put_le32(b, o + 8, static_cast(f.code.size())); + for (const Instr in : f.code) { + put_le32(b, c, in); + c += 4; + } + } + uint32_t acc = 0; + for (size_t i = 0; i < funcs_.size(); ++i) { + 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]; + } + + // 文件尾 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; + } + + /// 小端写 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); + } + } + + // ---- 成员 ---- + 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) { + Builder b(proj, units, link, cfg, image, err); + return b.run(); +} + +} // namespace compiler diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp new file mode 100644 index 0000000..98e3ee9 --- /dev/null +++ b/compiler/src/Lexer.cpp @@ -0,0 +1,489 @@ +/** + * @file Lexer.cpp + * @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" + +#include +#include +#include +#include + +namespace compiler { +namespace { + + /** + * @brief 关键字表:小写键 → Tok。 + * @details 与 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}, // 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 类型名(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}, + }; + + /** + * @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; // 未知单位 + } + + /** + * @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()) { + return false; + } + if (pos_ >= src_.size()) { + push(Tok::END, "", 0); + return true; + } + if (!lex_one()) { + return false; + } + } + } + + 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]; + std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)", + sf_.c_str(), line_, col_); + *err_ = "lex error: " + msg + buf; + } + 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_; + col_ = 1; + } else { + ++col_; + } + ++pos_; + } + + /** + * @brief 跳过空白与 (* *) 注释 + * + * @details 第一版注释不嵌套:遇到第一个 "*) 即结束 + * (IEC 允许嵌套,留待以后)。未闭合 → lex error。 + * @return true 正常;false 未闭合注释(err 已写) + */ + 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; + } + + /** + * @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; + t.text = text; + t.int_value = value; + t.source_file = sf_; + t.line = tok_line_; + t.col = tok_col_; + 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_; + tok_col_ = col_; + const char c = cur(); + + // TIME 字面量:T#(大小写不敏感)优先于标识符 + if ((c == 't' || c == 'T') && pos_ + 1 < src_.size() && src_[pos_ + 1] == '#') { + return lex_time(); + } + + // 标识符 / 关键字:统一折小写存入 text(内部形), + // 关键字命中即分类,否则 IDENT(用户定义名 / 内建类型名都走这里)。 + 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; + } + + // 整数:十进制 [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()))) { + 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 + "'"); + } + } + + /** + * @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 + 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_; ///< 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(); + Lexer l(source_file, content, out, err); + 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) { + 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/compiler/src/Linker.cpp b/compiler/src/Linker.cpp new file mode 100644 index 0000000..54a0a30 --- /dev/null +++ b/compiler/src/Linker.cpp @@ -0,0 +1,759 @@ +/** + * @file Linker.cpp + * @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" + +#include +#include +#include +#include +#include +#include +#include +#include + +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"; + 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 "?"; + } + + /** + * @brief 类型引用 → 文本名(符号表 / 类型比较用) + * @param t 类型引用 + * @return "bool" / "int" / "time" / FB 类型名(小写);未知返回 "?" + */ + 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 "?"; + } + + /** + * @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; + } + + /// 内建 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/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; + } + + /** + * @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; + 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: + /** + * @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_) { + 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; + } + + /** + * @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; + } + return std::filesystem::weakly_canonical(path) == gvl_path_; + } + + // ---- 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); + + 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 类型布局 ---- + + /** + * @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) { + 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 建局部符号并解析引用 ---- + + /** + * @brief 逐 POU 建局部符号并解析语句引用 + * @return true 成功;false(err 已写) + */ + 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; + } + + /** + * @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) { + return sc; + } + } + static LinkResult::PouScope kEmpty; + 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) { + 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; + } + + // ---- 引用解析 ---- + + /** + * @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) { + if (s.name == name) { + return &s; + } + } + 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) { + 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; + } + + /** + * @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) { + 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; + } + + /** + * @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) { + *kind = sc.kind; + return true; + } + } + return false; + } + + // ---- 4. 循环调用检测(函数依赖图 DFS)---- + + /** + * @brief 函数依赖图环检测入口 + * @details 对每个有出边的函数做 DFS;任一条路径回到栈上节点即环 + * @return true 无环;false 发现环(err 已写) + */ + bool check_cycles() { + std::set visiting; + std::set done; + for (const auto& [fn, _] : call_edges_) { + if (!dfs(fn, visiting, done)) { + return false; + } + } + 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)) { + 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 校验 ---- + + /** + * @brief io 绑定必须已在 GVL 声明 + * @details 绑定名来自 toml(原始大小写),与全局表比较前折小写; + * io 不创造变量,只做 channel/bit → 槽号映射 + * @return true 全部绑定合法;false(err 已写) + */ + bool check_io() { + for (const 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. 入口 ---- + + /** + * @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") { + return true; + } + } + return false; + } + + // ---- 成员 ---- + 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(); + out->global_index.clear(); + out->fb_types.clear(); + out->scopes.clear(); + out->fn_order.clear(); + Linker l(proj, units, out, err); + 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) { + 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/compiler/src/MachineConfig.cpp b/compiler/src/MachineConfig.cpp new file mode 100644 index 0000000..461dece --- /dev/null +++ b/compiler/src/MachineConfig.cpp @@ -0,0 +1,478 @@ +/** + * @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 { + +/** + * @brief 内建基元表。 + * @return 静态基元表(bit / int8..uint64 / float32 / float64,元数据在代码) + */ +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; +} + +/** + * @brief 按名称查内建基元。 + * @param name 基元名 + * @return 命中指针;未找到返回 nullptr + */ +const PrimType* MachineConfig::find_prim(const std::string& name) { + for (const PrimType& p : prims()) { + if (p.name == name) { + return &p; + } + } + return nullptr; +} + +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; + } + return msg; + } + + /** + * @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; + 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; + } + + /// @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]; + if (!nv || !nv.is_string()) { + fail(err, std::string("missing field '") + key + "' in " + what); + return false; + } + *out = nv.value_or(std::string()); + 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]; + if (!nv || !nv.is_integer()) { + fail(err, std::string("missing field '") + key + "' in " + what); + return false; + } + *out = nv.value_or(0); + 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]; + if (!nv || !nv.is_boolean()) { + fail(err, std::string("missing field '") + key + "' in " + what); + return false; + } + *out = nv.value_or(false); + return true; + } + +} // 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(); + 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; +} + +/** + * @brief 按配置类型名查类型行。 + * @param name 配置类型名 + * @return 命中指针;未找到返回 nullptr + */ +const ConfigType* MachineConfig::find_type(const std::string& name) const { + for (const ConfigType& t : types_) { + if (t.name == name) { + return &t; + } + } + 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) { + return &o; + } + } + 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) { + return &o; + } + } + 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) { + return &f; + } + } + return nullptr; +} + +} // namespace compiler diff --git a/compiler/src/Parser.cpp b/compiler/src/Parser.cpp new file mode 100644 index 0000000..12d326a --- /dev/null +++ b/compiler/src/Parser.cpp @@ -0,0 +1,1041 @@ +/** + * @file Parser.cpp + * @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 +#include +#include + +#include "compiler/Lexer.h" + +namespace compiler { +namespace { + + /** + * @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", + }; + + /** + * @brief 判断单词是否命中保留名清单 + * @param word 小写单词 + * @return true 命中(var_in_out / 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; + } + + /** + * @brief ST 递归下降解析器 + * + * @details 两阶段:构造时一次性 lex 成 token 流,再逐层解析; + * 不做流式交互。token 流只前移不回溯。 + */ + class Parser { + public: + /** + * @brief 构造解析器(先整体 lex 成 token 流) + * @param source_file 源文件名(报错用) + * @param content 源文本 + * @param out 解析结果 Unit(globals / pous) + * @param err 错误输出;可为 nullptr(静默) + */ + 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; + } + + /** + * @brief 顶层循环:解析直至 END + * + * @details 每轮要么是 GVL 形态的顶层 VAR_GLOBAL 段(无 POU 的文件), + * 要么是一个 POU。 + * @return true 成功(out 填满);false 语法错误(err 已写) + */ + 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: + // ---- 错误 ---- + + /** + * @brief 组装 "syntax error: (file, line, col)" 写入 err + * @details 位置取当前 token(指向出错处,不回溯到行首) + * @param msg 错误描述(不含前缀与位置) + * @return 恒 false(便于 return fail(...) 一行退出) + */ + 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 视图 ---- + + /** + * @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()) { + ++pos_; + } + 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 + "'"); + } + advance(); + return true; + } + + // ---- 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(); + 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; + } + + // 仅 FUNCTION 有返回类型:FUNCTION Add : INT + 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)); + } + + // 语句体直到配对 END_* + 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); + } + + /** + * @brief 当前 token 是否为五种变量段关键字之一 + * @return true 为 VAR / VAR_INPUT / VAR_OUTPUT / VAR_GLOBAL / VAR_EXTERNAL + */ + 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; + } + } + + /** + * @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; + case PouKind::Function: return Tok::END_FUNCTION; + case PouKind::FunctionBlock: return Tok::END_FUNCTION_BLOCK; + } + 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; + 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; + } + + /** + * @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) { + 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; + } + + /** + * @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: + 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"); + } + } + + /** + * @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: + 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::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)) { + return fail("'" + word + "' is not supported"); + } + t->kind = TypeKind::FbUser; + t->name = word; + advance(); + return true; + } + default: + return fail("expected type name"); + } + } + + // ---- 语句 ---- + + /** + * @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); + } + 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); + } + + /** + * @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)) { + 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, ";"); + } + + /** + * @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)) { + 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, ";"); + } + + /** + * @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 + 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"); + } + + /** + * @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 + 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"); + } + + /** + * @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)) { + Stmt s; + if (!parse_stmt(&s)) { + return false; + } + out->push_back(std::move(s)); + } + return true; + } + + // ---- 表达式(优先级爬升)---- + + /** + * @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())) { + return false; + } + while (check(Tok::OR)) { + advance(); + auto node = std::make_unique(); + node->kind = ExprKind::Or; // 短路语义保留到 12.8 编跳转 + 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; + } + + /** + * @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())) { + return false; + } + while (check(Tok::AND)) { + advance(); + auto node = std::make_unique(); + node->kind = ExprKind::And; // 短路语义保留到 12.8 编跳转 + 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; + } + + /** + * @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(); + e->kind = ExprKind::Not; + e->operand = std::make_unique(); + return parse_not(e->operand.get()); + } + 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())) { + 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; + } + // 右侧之后再出现比较符 → 链式,v1 拒绝 + 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; + } + + /** + * @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())) { + 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; + } + + /** + * @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())) { + 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; + } + + /** + * @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(); + e->kind = ExprKind::Neg; + e->operand = std::make_unique(); + return parse_unary(e->operand.get()); + } + 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: + 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(字段存在性由 12.6 校验) + advance(); + e->kind = ExprKind::Field; + e->name = word; + if (!parse_ident(&e->field)) { + return false; + } + return true; + } + if (check(Tok::LPAREN)) { + // 函数调用(FUNCTION,有返回值;与语句层 FB 调用区分) + 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"); + } + } + + /** + * @brief 解析标识符 + * @details 必须是 IDENT 且非保留名 + * @param out 输出小写标识符文本 + * @return true 成功;false 语法错误(err 已写) + */ + 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_; ///< 整段 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(); + out->pous.clear(); + Parser p(source_file, content, out, err); + 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) { + 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/compiler/src/Project.cpp b/compiler/src/Project.cpp new file mode 100644 index 0000000..3dce331 --- /dev/null +++ b/compiler/src/Project.cpp @@ -0,0 +1,527 @@ +/** + * @file Project.cpp + * @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" + +#include + +#include +#include +#include +#include +#include +#include + +#include "compiler/Stb.h" + +namespace compiler { +namespace { + + // ---- 错误收集:稳定类别前缀 + 行号 ---- + + /** + * @brief 写错误消息。 + * @details err 为 nullptr 时静默忽略(调用方可不关心原因)。 + * @param err 错误输出(可空) + * @param msg 完整错误消息(已带类别前缀与行号) + */ + void fail(std::string* err, const std::string& msg) { + if (err) { + *err = msg; + } + } + + /** + * @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; + } + + /** + * @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]) { + 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; + } + + /** + * @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]) { + 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; + } + + /** + * @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]) { + 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; + } + + /** + * @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) { + 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] ---- + + /** + * @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"}; + + 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] ---- + + /** + * @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"}; + + 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](可选)---- + + /** + * @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"}; + + 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]](可选)---- + + /** + * @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"}; + + 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; + } + 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; + } + + /** + * @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"}; + + 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 可选 + } + + // ---- 顶层 ---- + + /** + * @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)) { + 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); + } + + /** + * @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); + } + +} // 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); + + // 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); +} + +// ---- 文件集合与工程哈希 ---- + +/** + * @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() && + std::find(out.begin(), out.end(), p.gvl_file) == out.end()) { + out.push_back(p.gvl_file); + } + 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); + + // 路径排序(规格:路径只当排序键) + std::vector sorted = files; + std::sort(sorted.begin(), sorted.end()); + + std::filesystem::path base(p.base_dir); + uint64_t h = 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 = fnv1a64_update(h, reinterpret_cast(buf), + static_cast(n)); + } + } + } + *hash = h; + return true; +} + +} // namespace compiler diff --git a/compiler/src/Stb.cpp b/compiler/src/Stb.cpp new file mode 100644 index 0000000..75a85e2 --- /dev/null +++ b/compiler/src/Stb.cpp @@ -0,0 +1,481 @@ +/** + * @file Stb.cpp + * @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" + +#include +#include +#include + +#include "compiler/Project.h" + +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]; + h *= kFnvPrime; + } + 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) + | (static_cast(p[2]) << 16) + | (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) { + v |= static_cast(p[i]) << (8 * i); + } + return v; + } + + // ---- 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, + 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)); } + + /** + * @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; ///< 已吸收字节数(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) { + 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; + } + } + } + + /// 压缩一个满块(64 字节:w[0..63] 展开 + 64 轮)。 + 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; + } + + /** + * @brief 结束并输出摘要。 + * @param out 32 字节输出缓冲(大端) + */ + 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); + } + } + + /// 大端读 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]); + } + }; + +} // 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) { + out[i] = i < id.size() ? id[i] : '\0'; + } +} + +/** + * @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; + 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); + + // 段校验(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 - 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(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; +} + +/// 从 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_) { + const uint8_t* p = buf_ + offs_of_const() + i * kConstEntrySize; + e.tag = get_le32(p); + e.value = get_le64(p + 4); + } + 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_) { + 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; +} + +/// 字节码段起点(未 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 ""; + } + 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; +} + +/// 型号标识是否匹配 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; + } + 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; +} + +/** + * @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) { + if (err) { + *err = "cannot open for read: " + std::string(path); + } + return false; + } + out->assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + 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) { + 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; +} + +/** + * @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) { + 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; +} + +/** + * @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"); + 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/TypeInfo.cpp b/compiler/src/TypeInfo.cpp new file mode 100644 index 0000000..64f713c --- /dev/null +++ b/compiler/src/TypeInfo.cpp @@ -0,0 +1,57 @@ +/** + * @file TypeInfo.cpp + * @brief 语言类型元数据(machine.toml 别名 + 内建基元解析) + * @author + * @date 2026-08-21 + * + * @details type_meta 实现:先按名称(大小写不敏感)命中配置类型行, + * 再由 base 解析出内建基元,合成 TypeMeta。 + */ + +#include "compiler/TypeInfo.h" + +#include + +namespace compiler { + +namespace { + + /** + * @brief 转小写。 + * @param s 输入串 + * @return 全小写副本(逐字符 tolower,逐字节安全) + */ + 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 + +/** + * @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); + 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/compiler/src/Typecheck.cpp b/compiler/src/Typecheck.cpp new file mode 100644 index 0000000..41cba80 --- /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/compiler/src/main.cpp b/compiler/src/main.cpp new file mode 100644 index 0000000..c3575de --- /dev/null +++ b/compiler/src/main.cpp @@ -0,0 +1,265 @@ +/** + * @file main.cpp + * @brief STCompiler 可执行入口 + * @author + * @date 2026-08-21 + * + * @details 用法:STCompiler [-o out.stb] --machine ; + * --disasm 模式反汇编已编译映像。编译/反汇编必填 --machine(缺失报 + * "error: missing --machine ");打印模式(无 -o)不需要 + * 机器定义。错误统一打到 stderr,前缀 "error: "。 + */ + +#include +#include +#include +#include +#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" + +namespace { + /// 打印命令行用法(三种模式 + -o / --machine / --disasm / --help)。 + void usage() { + std::printf("usage: STCompiler [-o .stb] --machine \n" + " STCompiler .stb --disasm --machine \n" + " 解析工程并编译(词法 → 语法 → 链接 → 类型 → 寄存器码)\n" + " -o .stb 编译并写出映像文件\n" + " --machine 机器定义(machine.toml,编译路径必填)\n" + " --disasm 反汇编一个已编译的 .stb 映像(需要 --machine)\n" + " --help 打印本帮助\n"); + } + + /** + * @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; + if (!compiler::read_stb_file(path, &bytes, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + const compiler::StbView v = compiler::StbView::from(bytes); + if (!v.ok()) { + std::fprintf(stderr, "error: %s\n", v.error().c_str()); + return 1; + } + 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 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"); + 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 < 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]; + compiler::disasm(cfg, reinterpret_cast(base)[j], buf, + sizeof buf); + std::printf(" 0x%04x %s\n", + v.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; + } +} + +/** + * @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"); + usage(); + return 0; + } + if (std::strcmp(argv[1], "--help") == 0) { + usage(); + return 0; + } + + // 参数收集 + 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) { + if (std::strcmp(argv[i], "--disasm") == 0) { + disasm_mode = true; + } + } + + // --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]; + 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)) { + 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; + } + + // 编译路径:机器定义必填(编码/类型/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) { + 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, cfg, &image, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + if (!compiler::write_stb_file(out_path.c_str(), image, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + 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) { + compiler::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 (!compiler::write_sidecar_file(sidecar.string().c_str(), bindings, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + + 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.n_funcs(), v.n_globals()); + return 0; +} 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/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/executor/CMakeLists.txt b/executor/CMakeLists.txt new file mode 100644 index 0000000..0395530 --- /dev/null +++ b/executor/CMakeLists.txt @@ -0,0 +1,13 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(Executor + VERSION 0.1 + DESCRIPTION "字节码执行器:加载 .stb + sidecar 跑扫描周期") + +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 new file mode 100644 index 0000000..eceb121 --- /dev/null +++ b/executor/README.md @@ -0,0 +1,5 @@ +# executor + +BytecodeExecutor 模块:可执行入口。CMake 目标:`BytecodeExecutor`(`EXECUTABLE`),链接 `vm` 和 `isa`,**不链 `compiler`**。读取 `.stb` 时校验型号标识与 SHA-256。 + +职责与边界见 [`doc/executor/执行器入口.md`](../doc/executor/执行器入口.md)。 diff --git a/executor/src/main.cpp b/executor/src/main.cpp new file mode 100644 index 0000000..938969f --- /dev/null +++ b/executor/src/main.cpp @@ -0,0 +1,361 @@ +/** + * @file main.cpp + * @brief BytecodeExecutor 可执行入口 + * @author + * @date 2026-08-21 + * + * @details 12.10:加载 .stb + sidecar,按扫描周期执行。 + * - `BytecodeExecutor .stb [--cycles N] [--replay ] [--step]` + * - 只链 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/Encode.h" +#include "vm/Image.h" +#include "vm/Machine.h" + +namespace { + + /// 打印命令行用法。 + void usage() { + 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"); + } + + /** + * @brief sidecar 中的一条 I/O 绑定。 + * @details TOML 子集 `var`/`slot`/`channel`/`bit`,格式冻结于 + * Doc/isa/指令与映像.md;不创造变量,只把外部通道映射到数据区槽。 + */ + struct Binding { + 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; ///< 位号(展示用) + }; + + /** + * @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); + 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; + } + + /** + * @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; + } + + /** + * @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()) { + 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); + } + } + } + } + + /** + * @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) { + 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 + +/** + * @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"); + usage(); + return 0; + } + if (std::strcmp(argv[1], "--help") == 0) { + usage(); + return 0; + } + + const std::string stb_path = argv[1]; + // 参数:--cycles N / --replay / --step(缺省 cycles=10、无回放、非单步) + 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; + } + } + + // 读 .stb 映像(二进制)到字节缓冲 + std::string err; + std::vector bytes; + { + 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()); + } + // 结构校验(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()); + return 1; + } + vm::Machine machine; + if (!vm::Machine::create(bytes, &machine, &err)) { + std::fprintf(stderr, "error: %s\n", err.c_str()); + return 1; + } + + // sidecar 路径推导:.stb → .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; + } + + // 回放缓冲(当前行 0/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 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); + + // 单步模式:采样 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); + } + } + 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; + 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)); + } + std::printf("\n"); + if (f != vm::Fault::None) { + return 1; + } + } + return 0; +} diff --git a/host/README.md b/host/README.md deleted file mode 100644 index 17e7c9c..0000000 --- a/host/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# host - -可执行入口。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`)。 diff --git a/isa/CMakeLists.txt b/isa/CMakeLists.txt new file mode 100644 index 0000000..9df22ef --- /dev/null +++ b/isa/CMakeLists.txt @@ -0,0 +1,12 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(ISA + VERSION 0.1 + DESCRIPTION "指令集架构") + +add_library(isa STATIC + ./src/Encode.cpp) + +target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) \ No newline at end of file diff --git a/isa/README.md b/isa/README.md index f0166a5..137d01b 100644 --- a/isa/README.md +++ b/isa/README.md @@ -1,17 +1,64 @@ # isa -共享契约库。CMake 目标:`isa`(`STATIC` 或 `INTERFACE`),无依赖。 +**归执行器侧**,只管指令定义。CMake 目标:`isa`(`STATIC`),无依赖;被 `vm` 依赖。 -`compiler` 写映像、`vm` 读映像,两边只依赖本目录,彼此不链接。 +`compiler` **不依赖 isa**——指令 opcode / 类型元数据 / FB 布局全部来自 `compiler/machine.toml`(见 [`指令配置.md`](../doc/isa/指令配置.md))。`.stb` 映像读写由 `compiler`(写侧)与 `vm`(读侧)**各自实现**。 -## 职责 +```text +STCompiler → compiler (编码查 machine.toml,自带 .stb 写) +BytecodeExecutor → vm → isa (isa 只管指令;vm 自带 .stb 读) +``` -- 约 20 条寄存器指令的编码(`MOVE`、`LOADK`、跳转、`LOAD_I` / `STORE_Q`、`CALL` / `RET`、`CAL_TON` 等) -- 映像头:函数字节码、全局 / FB 数据槽、工程哈希 -- 定宽类型:第一版 `BOOL`、`INT`、`TIME` +**操作码、指令字以** [`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/`。 -解析器、符号表、扫描周期循环、I/O 采样。那些分别属于 `compiler`、`vm`、`host`。 +--- -指令表可作为内部 IR 的文本形态,不必当主语言。 +## 做 / 不做 + +**做:** 操作码与 32-bit 指令字、`BOOL`/`INT`/`TIME` 定宽与饱和、编解码、一条指令的文本转储。 + +**不做:** 映像读写(compiler/vm 各自实现)、toml/关键字/符号表/AST、VM 执行 switch(在 `vm`)、扫描周期、I/O 采样。 + +--- + +## 目录与 CMake + +```text +isa/ + CMakeLists.txt + README.md + include/isa/ + Types.h # 宽度、饱和 + Op.h # 操作码枚举 + OpFormat/OpClass + kOpDefs 表 + 助记符 + Instr.h # 32-bit 打包/拆字段(inline) + Encode.h # encode / decode / disasm + src/ + Encode.cpp +``` + +```cmake +add_library(isa STATIC src/Encode.cpp) +target_include_directories(isa PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +``` + +`vm`:`target_link_libraries(vm PUBLIC isa)`;`compiler` 不链 `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 表驱动,文本与规范文一致) + +## 测试 + +`tests/isa_test`(链 `isa`):饱和、操作码冻结值、指令打包、encode/decode 往返、disasm 文本、OpClass 分类。 + +## 验收 + +- `cmake --build` 过;`ctest` 过(`isa_roundtrip` 等 14 个用例全绿) +- `vm` 只 `#include `;`compiler` 不引用 `isa` +- 操作码有变:先改 `compiler/machine.toml` + 本目录 + 测试,再改两端 diff --git a/isa/include/isa/Encode.h b/isa/include/isa/Encode.h new file mode 100644 index 0000000..b4dfcfa --- /dev/null +++ b/isa/include/isa/Encode.h @@ -0,0 +1,54 @@ +/** + * @file Encode.h + * @brief 指令编解码与反汇编 + * @details 给 STCompiler --disasm 与测试往返使用;文本格式见 Doc/isa/指令与映像.md。 + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include +#include + +#include "isa/Instr.h" +#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; ///< 源寄存器 / 立即数低 8 位 / 偏移低 8 位 + uint8_t b; ///< 源寄存器 / 立即数高 8 位 / 偏移高 8 位 + }; + + /** + * @brief 编码:字段视图打包成一条 32 位指令字。 + * @param d 解码后的字段视图 + * @return 打包后的指令字;满足 encode(decode(w)) == w + */ + Instr encode(Decoded d); + + /** + * @brief 解码:指令字拆成字段视图。 + * @param w 32 位指令字 + * @return 各字段视图 + */ + Decoded decode(Instr w); + + /** + * @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 new file mode 100644 index 0000000..f9e4a6d --- /dev/null +++ b/isa/include/isa/Instr.h @@ -0,0 +1,175 @@ +/** + * @file Instr.h + * @brief 32-bit 指令字打包 / 拆字段 + * @details 指令字布局:`[ op:8 | rd:8 | a:8 | b:8 ]`,整体是一个小端 uint32_t。 + * 字段含义按操作码形态解释,见 Doc/isa/指令与映像.md。 + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include + +#include "isa/Op.h" + +namespace isa { + /// 指令字:`[ 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) + | (static_cast(a) << 16) + | (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); + } + + /** + * @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)); + } + + /** + * @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); + } + + /** + * @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); + } + + /** + * @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)); + } + + /** + * @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) { + 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) { + 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 new file mode 100644 index 0000000..a3684ac --- /dev/null +++ b/isa/include/isa/Op.h @@ -0,0 +1,165 @@ +/** + * @file Op.h + * @brief 操作码枚举、操作数形态与编译期指令表 + * @details 操作码数值即 .stb 指令编码。执行器(vm)只依赖本表; + * 编译器侧的登记见 Doc/isa/指令配置.md(compiler/machine.toml,与之强校验)。 + * @author + * @date 2026-08-19 + */ + +#pragma once + +#include + +namespace isa { + /** + * @brief 操作码枚举。 + * @details 数值即编码:冻结后不得改顺序、不得插值,增加指令只能追加到末尾, + * 并同步 Doc/isa/指令与映像.md、compiler/machine.toml 与测试。 + */ + 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 rs, slot + LOAD_M = 20, // LOAD_M rd, slot + 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_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 + }; + + /// 操作码总数(= 最大 opcode + 1,kOpDefs 的下标上界)。 + static const int kOpCount = 34; + + /** + * @brief 操作数形态(三层分类第二层,见 Doc/isa/指令配置.md)。 + * @details 决定操作数 a|b 的解释方式与反汇编文本格式。 + */ + 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) + }; + + /** + * @brief 操作对象大类(三层分类第一层)。 + */ + enum class OpClass : uint8_t { + Plain, // 无实例:寄存器 / 立即数 / 槽 / 分支 / 调用 + Instance, // 有实例:操作数据区实例块(内建 FB) + }; + + /** + * @brief 编译期指令表条目。 + * @details 一条指令的名称、操作数形态与大类;下标与 Op 数值一一对应。 + */ + struct OpDef { + 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 + {"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 + }; + + /** + * @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 new file mode 100644 index 0000000..104e783 --- /dev/null +++ b/isa/include/isa/Types.h @@ -0,0 +1,106 @@ +/** + * @file Types.h + * @brief 定宽类型与饱和算术 + * @details 虚拟机字长与类型契约的唯一来源。compiler 与 vm 共用同一套规则, + * 详见 Doc/isa/指令与映像.md。 + * @author + * @date 2026-08-18 + */ + +#pragma once + +#include +#include + +namespace isa { + namespace types { + /** + * @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; + + /** + * @brief 槽 / 常量表类型标记。 + * @details 数值冻结并写入 .stb 常量表:0=BOOL、1=INT、2=TIME(见映像规范), + * 不得改顺序、不得插值。 + */ + enum TypeTag : uint8_t { + Bool = 0, + Int = 1, + Time = 2, + }; + + // 饱和算术:夹到 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() + : sum > std::numeric_limits::max() ? std::numeric_limits::max() + : 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() + : diff > std::numeric_limits::max() ? std::numeric_limits::max() + : 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() + : prod > std::numeric_limits::max() ? std::numeric_limits::max() + : 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) { + if (b == 0) { + return a; + } + if (a == std::numeric_limits::min() && b == -1) { + return std::numeric_limits::max(); + } + return static_cast(a / b); + } + } +} diff --git a/isa/src/Encode.cpp b/isa/src/Encode.cpp new file mode 100644 index 0000000..db74a9a --- /dev/null +++ b/isa/src/Encode.cpp @@ -0,0 +1,92 @@ +/** + * @file Encode.cpp + * @brief 指令编解码与反汇编实现 + * @author + * @date 2026-08-19 + */ + +#include "isa/Encode.h" + +#include + +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); + d.rd = rd(w); + d.a = a(w); + d.b = b(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; + } + 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); + + // 表驱动:按操作数形态输出(文本与 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 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 OpFormat::IMM: + case OpFormat::SLOT: + snprintf(out, cap, "%s r%u, %u", m, + static_cast(rd(w)), static_cast(imm16(w))); + break; + case OpFormat::JMP: + snprintf(out, cap, "%s %+d", m, static_cast(off16(w))); + break; + case OpFormat::JC: + snprintf(out, cap, "%s r%u, %+d", m, + static_cast(rd(w)), static_cast(off16(w))); + break; + case OpFormat::CALL: + case OpFormat::CAL: + snprintf(out, cap, "%s %u", m, static_cast(imm16(w))); + break; + case OpFormat::NONE: + snprintf(out, cap, "%s", m); + break; + } +} + +} // namespace isa diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e05b6e8 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,123 @@ +# 自动化用例 +add_test(NAME stcompiler_version + COMMAND STCompiler) +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) + +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) + +# 词法测试: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) + +# 语法测试: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) + +# 链接测试:用例 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) + +# 类型检查测试:用例 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) + +# 寄存器码测试(12.8 切片 1):用例 01/02 出映像(REPO_ROOT 注入源目录绝对路径) +add_executable(codegen_test + ./src/codegen_test.cpp) + +target_link_libraries(codegen_test PRIVATE compiler isa) +target_compile_definitions(codegen_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +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) + +# 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) + +# 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/README.md b/tests/README.md index 8c563d4..630b834 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,7 +2,17 @@ 自动化用例,由顶层 `enable_testing()` 接入 CTest。 -可以 `add_test` 调用 `host`,或直接链接 `compiler` + `vm`。样例工程数据按「一份 toml + 若干 `.st`」分子目录存放,不要做成 library。 +可以 `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 完成标准:目录和期望表齐,此时大部分还编不过,属正常。实现阶段逐个点亮。 ## 第一版要冻的 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 diff --git a/tests/src/cases_test.cpp b/tests/src/cases_test.cpp new file mode 100644 index 0000000..2500ba6 --- /dev/null +++ b/tests/src/cases_test.cpp @@ -0,0 +1,178 @@ +/** + * @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; + } + 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, cfg, &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/tests/src/codegen_test.cpp b/tests/src/codegen_test.cpp new file mode 100644 index 0000000..2146d57 --- /dev/null +++ b/tests/src/codegen_test.cpp @@ -0,0 +1,734 @@ +/** + * @file codegen_test.cpp + * @brief 寄存器码测试(12.8 切片 1):用例 01/02 出映像 + * @author + * @date 2026-08-21 + */ + +#include +#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 "compiler/Stb.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 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)) { + 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, cfg, 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; + 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); + 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, cfg, 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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 compiler::StbView::FuncRow r = v.func_row(0); + CHECK(r.nregs == 8); // 帧基址(调用约定区) + 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + CHECK(v.n_funcs() == 1); + CHECK(v.n_consts() == 2); // TRUE 与 FALSE + + const compiler::StbView::FuncRow r = v.func_row(0); + 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 r8, 0") == 0); // a := TRUE(常量 0) + isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); + 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); + + // 常量表:0 = BOOL 1,1 = BOOL 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; +} + +// ---- 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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 compiler::StbView::FuncRow r = v.func_row(0); + 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 r8, 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + CHECK(v.n_globals() == 2); + + const compiler::StbView::FuncRow r = v.func_row(0); + 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 r8, 0") == 0); // q := I0_0(io.input → LOAD_I) + isa::disasm(reinterpret_cast(base)[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r9, r8") == 0); // 临时 ← q + isa::disasm(reinterpret_cast(base)[2], buf, sizeof buf); + 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; +} + +// ---- 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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) + + char buf[64]; + isa::disasm(reinterpret_cast(v.code_bytes())[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOAD_GLOBAL r8, 1") == 0); // x := I(槽 1) + 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; +} + +// ---- 7. 用例 03:短路 AND / OR 必须编跳转 ---- + +static bool test_case03() { + std::vector img; + std::string err; + CHECK(compile_case("03_short_circuit", &img, &err)); + + const compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + const compiler::StbView::FuncRow r = v.func_row(0); + CHECK(r.nregs == 12); // r8 起: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 r10, r8") == 0); + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r10, +2") == 0); + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r11, r9") == 0); + isa::disasm(ins[3], buf, sizeof buf); + 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 r10, r8") == 0); + isa::disasm(ins[5], buf, sizeof buf); + CHECK(std::strcmp(buf, "JT r10, +2") == 0); + isa::disasm(ins[6], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r11, r9") == 0); + isa::disasm(ins[7], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r10, r11") == 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + const compiler::StbView::FuncRow r = v.func_row(0); + 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 r10, r8") == 0); // 临时 ← a + isa::disasm(ins[1], buf, sizeof buf); + 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; +} + +// ---- 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + const compiler::StbView::FuncRow r = v.func_row(0); + CHECK(r.nregs == 13); // r8 起:sel out + 3 临时 + 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 r11, r8") == 0); + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r12, 0") == 0); + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "CMP_EQ r10, r11, r12") == 0); + isa::disasm(ins[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r10, +2") == 0); // 假 → elsif([6]) + isa::disasm(ins[4], buf, sizeof buf); + 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 r11, r8") == 0); + isa::disasm(ins[7], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r12, 2") == 0); + isa::disasm(ins[8], buf, sizeof buf); + CHECK(std::strcmp(buf, "CMP_EQ r10, r11, r12") == 0); + isa::disasm(ins[9], buf, sizeof buf); + CHECK(std::strcmp(buf, "JF r10, +2") == 0); // 假 → else([12]) + isa::disasm(ins[10], buf, sizeof buf); + 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 r9, 4") == 0); + isa::disasm(ins[13], buf, sizeof buf); + CHECK(std::strcmp(buf, "RET") == 0); + 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + const compiler::StbView::FuncRow r = v.func_row(0); + 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 r8, 0") == 0); // n := 0 + // L_loop [1]:n < 10 → r9 + isa::disasm(ins[1], buf, sizeof buf); + CHECK(std::strcmp(buf, "MOVE r10, r8") == 0); + isa::disasm(ins[2], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r11, 1") == 0); // 常量 1 = 10 + isa::disasm(ins[3], buf, sizeof buf); + CHECK(std::strcmp(buf, "CMP_LT r9, r10, r11") == 0); + isa::disasm(ins[4], buf, sizeof buf); + 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 r9, r8") == 0); + isa::disasm(ins[6], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r10, 2") == 0); // 常量 2 = 1 + isa::disasm(ins[7], buf, sizeof buf); + 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); + 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + const compiler::StbView::FuncRow r = v.func_row(0); + CHECK(r.nregs == 14); // r8 起: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 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; +} + +// ---- 12. 用例 08:TIME 字面量进常量表 ---- + +static bool test_case08() { + std::vector img; + std::string err; + CHECK(compile_case("08_time_literal", &img, &err)); + + const compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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 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()); + isa::disasm(ins[0], buf, sizeof buf); + CHECK(std::strcmp(buf, "LOADK r8, 0") == 0); + isa::disasm(ins[1], buf, sizeof buf); + 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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 compiler::StbView::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 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; + 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; +} + +// ---- 14. 用例 15:用户 FB 内联展开 ---- + +static bool test_case15() { + std::vector img; + std::string err; + CHECK(compile_case("15_fb_instance", &img, &err)); + + const compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + CHECK(v.n_funcs() == 2); // FB 占位 + MAIN + CHECK(v.entry_fn_id() == 1); + CHECK(v.data_len() == 24); // 实例 3 槽 × 8 字节定宽 + + 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; + 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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]; + 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, 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, 2") == 0); // t.Q(槽 2) + 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 compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + 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]; + 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 4") == 0); + isa::disasm(ins[13], buf, sizeof buf); + 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, 8") == 0); // c.CV(槽 8) + 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)); + compiler::MachineConfig cfg; + CHECK(load_cfg(&cfg)); + CHECK(codegen_project(p, units, link, cfg, &img, &err)); + + const compiler::StbView v = compiler::StbView::from(img); + CHECK(v.ok()); + CHECK(v.n_globals() == 4); + CHECK(v.n_funcs() == 2); + CHECK(v.entry_fn_id() == 1); + CHECK(v.data_len() == 56); // 7 槽 × 8 字节定宽 + + 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; + 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, 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, 5") == 0); // starter.stop(槽 5) + isa::disasm(im[13], buf, sizeof buf); + 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, 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); + 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; +} + +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; + 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; + 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; +} 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; +} diff --git a/tests/src/isa_test.cpp b/tests/src/isa_test.cpp new file mode 100644 index 0000000..5583c14 --- /dev/null +++ b/tests/src/isa_test.cpp @@ -0,0 +1,136 @@ +/** + * @file isa_test.cpp + * @brief isa 合同测试:饱和、编解码、disasm、哈希、映像、sidecar + * @author + * @date 2026-08-19 + */ + +#include +#include +#include +#include +#include + +#include "isa/Encode.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::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; +} + +// ---- 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; +} + +int main() { + if (!test_sat()) return 1; + if (!test_op()) return 1; + if (!test_instr()) return 1; + if (!test_encode()) return 1; + std::printf("isa_test: %d checks passed\n", g_checks); + return 0; +} 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; +} 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; +} diff --git a/tests/src/machine_test.cpp b/tests/src/machine_test.cpp new file mode 100644 index 0000000..a17bfb9 --- /dev/null +++ b/tests/src/machine_test.cpp @@ -0,0 +1,329 @@ +/** + * @file machine_test.cpp + * @brief machine.toml 加载与强校验测试(阶段 A 步骤 3) + * @author + * @date 2026-08-21 + */ + +#include +#include +#include +#include + +#include "compiler/MachineConfig.h" +#include "compiler/Stb.h" +#include "compiler/TypeInfo.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; +} + +// ---- 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; +} + +// ---- 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; +} 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; +} 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; +} diff --git a/tests/src/vm_test.cpp b/tests/src/vm_test.cpp new file mode 100644 index 0000000..d5df419 --- /dev/null +++ b/tests/src/vm_test.cpp @@ -0,0 +1,513 @@ +/** + * @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/Stb.h" +#include "compiler/Typecheck.h" +#include "isa/Encode.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; + 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)) { + 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, cfg, &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 = 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()) + 32; // SHA 尾占位 + + 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, 0x43545353u); + put32(4, 1u); + 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]; + } + // 型号标识(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; +} + +// ---- 1. 手工映像:标量 + 跳转 + 数据区 ---- + +static bool test_hand_scalar() { + // r8 := 5;r9 := r8 + 3;槽 0 ← r9;槽 1 → r10;JT 跳过一条 + const std::vector consts = { + {1, 5}, + {1, 3}, + {1, 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 = { + {1, 5}, + {1, 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; +} + +// ---- 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; + 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; + if (!test_verify()) return 1; + std::printf("vm_test: %d checks passed\n", g_checks); + return 0; +} 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` 浅克隆加快(本仓库不依赖子模块历史)。 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 diff --git a/vm/CMakeLists.txt b/vm/CMakeLists.txt new file mode 100644 index 0000000..c7a06ae --- /dev/null +++ b/vm/CMakeLists.txt @@ -0,0 +1,14 @@ +# CMake最低版本 +cmake_minimum_required(VERSION 3.21) + +# 工程名、版本号、工程描述 +project(VM + VERSION 0.1 + DESCRIPTION "寄存器虚拟机") + +add_library(vm STATIC + ./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/README.md b/vm/README.md index feba254..1136036 100644 --- a/vm/README.md +++ b/vm/README.md @@ -1,24 +1,5 @@ # vm -寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 `isa`。 +寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 `isa`(只管指令定义);自带 `.stb` 读实现 + 型号/SHA-256 校验。 -运行时是 **一台 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)。 diff --git a/vm/include/vm/Image.h b/vm/include/vm/Image.h new file mode 100644 index 0000000..33bb5bd --- /dev/null +++ b/vm/include/vm/Image.h @@ -0,0 +1,147 @@ +/** + * @file Image.h + * @brief vm 自带的 .stb 只读视图(执行器侧;compiler 各有实现) + * @author + * @date 2026-08-21 + * + * @details 格式契约见 Doc/isa/指令与映像.md(12.13 修订:头 104 = 72 + 型号标识[32], + * 文件尾 SHA-256[32])。解析头与段、补型号匹配与 SHA-256 校验都在本模块完成。 + */ + +#pragma once + +#include +#include +#include +#include + +namespace vm { + + /// 执行器内建支持型号(与 machine.toml [meta] 对齐;.stb 型号不匹配直接拒绝)。 + static const char* const kModelName = "STATOR"; + /// 执行器内建支持型号版本(与 machine.toml [meta] 对齐)。 + static const uint32_t kModelVersion = 1; + + /** + * @brief 映像头字段(104 字节;字段顺序与 Doc/isa/指令与映像.md 一致)。 + * @details 全部为小端值;型号标识 32 字节 @72 不在此结构内(见 model_id())。 + */ + struct ImageHeader { + 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; ///< 本函数寄存器数(函数头 nregs) + uint32_t code_offset = 0; ///< 相对字节码段起点(字节) + uint32_t code_len = 0; ///< 指令条数 + }; + + /** + * @brief 常量表一项(12 字节)。 + */ + struct ConstEntry { + 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; + + /// @return 字节码段起点;校验失败返回 nullptr。 + const uint8_t* code_bytes() const; + + /// @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; + + /// @return 文件尾 32 字节是否为内容(去尾后)的 SHA-256。 + bool sha_ok() 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 new file mode 100644 index 0000000..289b90d --- /dev/null +++ b/vm/include/vm/Machine.h @@ -0,0 +1,159 @@ +/** + * @file Machine.h + * @brief 寄存器虚拟机(12.9) + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/vm/扫描周期.md 与 Doc/vm/指令执行.md): + * - 只认 .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=结果) + * - 数据区:8 字节定宽槽(方案 a),指令 slot = 槽号,偏移 = slot × 8 + * - 可观察性(v1 预留):step / pc / fn_id / cycle_count / call_depth / reg + */ + +#pragma once + +#include +#include +#include + +#include "vm/Image.h" +#include "isa/Instr.h" + +namespace vm { + + /** + * @brief 周期/指令故障码。 + */ + enum class Fault { + 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 帧)。 + * @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。 + * @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 周期是否已结束(MAIN 的 RET 后为 true)。 + bool ended() const; + + /// @brief 映像头(dt_ms / cycle_limit 等)。 + const vm::ImageHeader& header() 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; + + /// @brief 当前帧寄存器数(函数头 nregs)。 + uint32_t nregs() const; + + /// @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; ///< 返回目标函数 + }; + + 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 后周期是否已结束 + + /// @brief 当前(栈顶)帧引用。 + Frame& cur_frame(); + /// @brief 当前(栈顶)帧常量引用。 + const Frame& cur_frame() const; + + /// @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 new file mode 100644 index 0000000..66bb53f --- /dev/null +++ b/vm/src/Image.cpp @@ -0,0 +1,386 @@ +/** + * @file Image.cpp + * @brief vm 自带的 .stb 只读视图实现(执行器侧;compiler 各有实现) + * @author + * @date 2026-08-21 + */ + +#include "vm/Image.h" + +#include +#include + +namespace vm { + +namespace { + + /// 映像魔数:"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) + | (static_cast(p[2]) << 16) + | (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) { + v |= static_cast(p[i]) << (8 * i); + } + return v; + } + + // ---- 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, + 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)); } + + /** + * @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; ///< 已吸收字节数(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) { + 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; + } + } + } + + /// 压缩一个满块(64 字节,w[0..63] 展开 + 64 轮)。 + 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; + } + + /** + * @brief 结束并输出摘要。 + * @param out 32 字节输出缓冲(大端) + */ + 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); + } + } + + /// 大端读 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) { + out[i] = i < id.size() ? id[i] : '\0'; + } + } + +} // 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; + 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 文件尾;这里仅保证有尾,sha_ok() 做完整性校验) + 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; +} + +/** + * @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) { + 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; +} + +/** + * @brief 读常量表一项。 + * @param i 常量下标 + * @return 该条目;越界(或校验失败)时全 0 + */ +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; +} + +/** + * @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 ""; + } + 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; +} + +/** + * @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; + } + 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 new file mode 100644 index 0000000..e247d31 --- /dev/null +++ b/vm/src/Machine.cpp @@ -0,0 +1,601 @@ +/** + * @file Machine.cpp + * @brief 寄存器虚拟机实现(12.9:译码 switch + CALL/RET 帧栈 + CAL_* 定时器) + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/vm/指令执行.md): + * - 取指:函数表 → code_offset(字节)→ u32;执行后 pc 先 +1,跳转再 pc += off + * - 数据区 8 字节定宽槽:偏移 = slot × 8 + * - 寄存器越界检查仅对把字段当寄存器的指令;a|b 作偏移/槽号/常量 id 时跳过 + */ + +#include "vm/Machine.h" + +#include + +#include "isa/Encode.h" +#include "isa/Instr.h" +#include "isa/Op.h" +#include "isa/Types.h" + +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); + if (!out->image_.ok()) { + if (err) { + *err = out->image_.error(); + } + 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()); + out->edge_prev_.assign(out->data_len() / 8 * 2, 0); + + // MAIN 帧(跨周期保留) + const uint32_t entry = out->image_.header().entry_fn_id; + const vm::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; +} + +/** + * @brief 跑一个扫描周期:MAIN pc=0 执行到 RET。 + * @return 故障(None = 正常完成) + * @details 循环执行 exec_one() 直到周期结束或故障;结束/故障后清空调用栈 + * (保留 MAIN 帧,PROGRAM 变量状态跨周期持久)。 + */ +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_; +} + +/** + * @brief 执行一条指令,停在指令边界。 + * @return true 继续;false 周期结束(MAIN 的 RET)或发生故障 + */ +bool Machine::step() { + if (ended_ || fault_ != Fault::None) { + return false; + } + return exec_one(); +} + +Fault Machine::fault() const { return fault_; } +bool Machine::ended() const { return ended_; } +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_; } +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]; } + +/** + * @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; + if (pc_ >= row.code_len) { + return isa::pack(isa::Op::RET, 0, 0, 0); + } + 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); + 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 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); + } 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); + if (!slot_get(slot, ®s[rd])) { + fault_ = Fault::BadSlot; + return false; + } + return true; + } + case isa::Op::STORE_Q: + case isa::Op::STORE_M: + case isa::Op::STORE_GLOBAL: { + const uint16_t slot = isa::imm16(w); + if (!slot_set(slot, regs[rd])) { + fault_ = Fault::BadSlot; + return false; + } + return true; + } + case isa::Op::CALL: + fault_ = do_call(isa::imm16(w)); + return fault_ == Fault::None; + case isa::Op::RET: + fault_ = do_ret(); + if (fault_ != Fault::None) { + return false; + } + 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: + fault_ = Fault::BadOp; + return false; + } +} + +/** + * @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()) { + 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; +} + +/** + * @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()) { + return false; + } + for (int i = 0; i < 8; ++i) { + data_[off + i] = static_cast((v >> (8 * i)) & 0xFFu); + } + 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; + } + if (frames_.size() >= 64) { + return Fault::StackOverflow; + } + const vm::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; +} + +/** + * @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:周期结束 + 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; +} + +/** + * @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) + // 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; + + 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; + 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 (is_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_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 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; + // 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; + 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 (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; + } + + return Fault::BadOp; +} + +} // namespace vm