细化第一版实现顺序,并与仓库目录对齐。

把第 12 节拆成可验收阶段,目录改为 isa/compiler/vm/host。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 16:51:45 +08:00
co-authored by Cursor
parent 2eb2468119
commit 50b0456198
2 changed files with 261 additions and 14 deletions
+1 -1
View File
@@ -4,6 +4,6 @@
| 文件 | 内容 | | 文件 | 内容 |
|---|---| |---|---|
| [`初步计划.md`](初步计划.md) | 定案:子集 ST、toml 工程、寄存器 VM、执行模型、ST 子集实现顺序 | | [`初步计划.md`](初步计划.md) | 定案:子集 ST、toml 工程、寄存器 VM、执行模型、ST 子集;第 12 节为分阶段实现顺序 |
仓库根目录 [`README.md`](../README.md) 只说明目录和模块边界;语言规则、映像与扫描周期的细节以本目录为准。 仓库根目录 [`README.md`](../README.md) 只说明目录和模块边界;语言规则、映像与扫描周期的细节以本目录为准。
+260 -13
View File
@@ -1,4 +1,4 @@
# 子集 ST + toml 工程 + 寄存器 VM # StScan子集 ST + toml 工程 + 寄存器 VM
独立方案说明。调研全文见 `PLC-DSL.md` 独立方案说明。调研全文见 `PLC-DSL.md`
@@ -269,14 +269,15 @@ RET
## 10. 目录 ## 10. 目录
仓库布局见根目录 `README.md`。实现时按四个 CMake 目标拆,样例不放仓库根:
```text ```text
project.toml # files / gvl.file / 可选 io isa/ # 指令编码、映像头、定宽类型(compiler 与 vm 的合同)
globals.st # 第一版唯一 GVL compiler/ # lexer parser symbols checker codegen linker
motor.st vm/ # 扫描周期、寄存器、映像、FB
main.st host/ # 喂 I、收 Q、Δt、回放
compiler/ # lexer parser symbols checker codegen linker examples/line1/ # 第 11 节最小样例(给人看)
vm/ # 扫描周期、寄存器、映像、FB tests/ # 冻规则用的正例 / 负例
host/ # 喂 I、收 Q、Δt、回放
``` ```
--- ---
@@ -322,8 +323,254 @@ END_PROGRAM
## 12. 实现顺序 ## 12. 实现顺序
1. 冻 toml 字段:`files` / `entry` / `cycle_limit` / `dt_ms` / `gvl.file` / 可选 `io.*` 原则:**先冻合同,再解析,再链接,最后写 VM**。`compiler` `vm` 不互链,合同只进 `isa`。每一阶段先有可运行的测试,再进入下一阶段。一人约 4~8 周做到第 11 节 `line1`
2. 冻关键字表 + 约 20 个样例程序
3. 冻约 20 条指令编码 第一版不做:图染色、SSA、WHILE 有界证明、异常表、闭包、优化、`REAL`、独立 `stc.exe`
4. 冻符号表字段:名字、种类、类型、地址、源文件
5. 递归下降解析 → 符号表与链接 → 最后写 VM 循环 ---
### 12.0 骨架(约 1 天)
CMake 四个目标能空编译:`isa``compiler``vm``host``host` 先打印版本即可。
- 落下 `examples/line1/`:第 3 节 toml + 第 11 节三个 `.st`
- `tests/` 接上 CTest,先放一个必过的空测试。
- 溢出策略当场选定并写进 `isa` 注释:**饱和**或**报错**,只留一种。
完成:`cmake --build` 过;`ctest` 能跑。
---
### 12.1 冻合同(约 2~3 天,先于任何解析器)
四张表写进代码或 `Doc/`,改表要同步改测试。未冻完不要写递归下降。
**toml 字段(只这些)**
| 字段 | 含义 | 缺省 / 约束 |
|---|---|---|
| `project.name` | 工程名 | 必填 |
| `project.entry` | 入口 | 第一版必须是 `program MAIN` |
| `project.cycle_limit` | 每周期指令上限 | 如 `100000`;超时是故障 |
| `project.dt_ms` | 本周期 Δt | 给 TON / TOF / CTU,不是 wall clock |
| `files.st` | 源文件列表 | 编译前全部已知 |
| `gvl.file` | 唯一允许 `VAR_GLOBAL` 的文件 | 已在 `files.st` 则不重复收录 |
| `[[io.input]]` / `[[io.output]]` | `var` + `channel` + `bit` | 可选;`var` 必须已在 GVL |
禁止 toml 里用 `[[globals]]` 再声明变量。
**关键字(第一版只认这些)**
`PROGRAM` `FUNCTION` `FUNCTION_BLOCK` `END_PROGRAM` `END_FUNCTION` `END_FUNCTION_BLOCK`
`VAR` `VAR_INPUT` `VAR_OUTPUT` `VAR_GLOBAL` `VAR_EXTERNAL` `END_VAR`
`BOOL` `INT` `TIME`
`IF` `ELSIF` `ELSE` `END_IF` `WHILE` `END_WHILE`
`AND` `OR` `NOT`
`TRUE` `FALSE`
`TON` `TOF` `CTU`
其余一律当标识符或直接拒绝(`VAR_IN_OUT``REF``CLASS` 等出明确错误)。
**符号表一行**
`name` / `kind` / `type` / `address` / `source_file`
`kind` 至少:`global` / `external` / `local` / `input` / `output` / `pou` / `fb_instance` / `const`
**指令(进 `isa`,约 20 条)**
第 7 节那张表:编码宽度、立即数格式、`fn_id` 是立即数、跳转是相对 offset。
映像头:函数表、常量表、全局槽数、FB 槽布局、`cycle_limit``dt_ms`、工程哈希。
工程哈希:收录的源文件排序后算,写入映像,回放时槽位必须一致。
完成:`isa` 能编解码一条指令;头文件即规范,不再另写一份互相打架的文档。
---
### 12.2 约 20 个样例(与 12.1 同时列出,随后逐个点亮)
每个用例一个子目录(`project.toml` + `.st`)。先写文件和期望(通过 / 报哪类错),再实现。`examples/line1` 给人看;负例和切片放 `tests/`
| # | 用例 | 期望 |
|---|---|---|
| 1 | 空 `PROGRAM MAIN` + `RET` | 通过 |
| 2 | `BOOL` 赋值、`TRUE`/`FALSE` | 通过 |
| 3 | `AND`/`OR` 短路(右侧有副作用时不执行) | 通过 |
| 4 | `IF` / `ELSIF` / `ELSE` | 通过 |
| 5 | `WHILE` 正常结束 | 通过 |
| 6 | `WHILE` 打满 `cycle_limit` | 运行时故障 |
| 7 | `INT` 四则 + 比较 | 通过 |
| 8 | `TIME` 字面量 | 通过 |
| 9 | GVL + `VAR_EXTERNAL` | 通过,同一槽 |
| 10 | 非 GVL 文件写 `VAR_GLOBAL` | 编译错误 |
| 11 | 同名 `VAR_GLOBAL` | 链接失败 |
| 12 | `io.var` 未在 GVL | 编译错误 |
| 13 | `FUNCTION` 纯计算 + `CALL` | 通过 |
| 14 | `FUNCTION` 写全局 | 编译错误 |
| 15 | FB 实例 + `fb.Q` + 命名输入 | 通过 |
| 16 | 未声明实例就调 FB | 编译错误 |
| 17 | `TON` + 固定 Δt | 到时 `Q` 为真 |
| 18 | `TOF` / `CTU` | 行为与 Δt 一致 |
| 19 | 函数体循环调用 / 无界递归 | 编译错误 |
| 20 | `line1`(第 11 节) | I 组合 → `Q0_0` 与急停语义一致 |
完成:目录和期望表齐;此时大部分还编不过,属正常。
---
### 12.3 toml 读取(`compiler`,约 2 天)
还不碰 `.st` 正文。
- 解析第 3 节字段;未知键报错。
- 拼出编译文件集合:`files.st` `gvl.file`,去重、记绝对/相对路径。
- 校验:缺文件、`entry` 不是 `program MAIN``io``var`/`channel`/`bit`
- 算出工程哈希(文件集合排序后);先对源文件算,映像还没有也没关系。
完成:对 `examples/line1/project.toml` 打出文件列表和哈希;缺文件的负例过。
---
### 12.4 词法(`compiler`,约 2 天)
- `(* *)` 注释、标识符、关键字、整数、`TIME` 字面量、`:=`、比较符、括号。
- 关键字与标识符分词;大小写规则写死(建议与 IEC 一样不敏感,实现时统一折成一种内部形)。
- 每个 token 带 `source_file` + 行列,供以后报错。
完成:把第 11 节三个文件 lex 成稳定 token 流;非法字符报错。
---
### 12.5 递归下降语法(`compiler`,约 46 天)
只出 AST,不分配寄存器、不写映像。
顺序:
1. POU 外壳:`PROGRAM` / `FUNCTION` / `FUNCTION_BLOCK` 与对应 `END_*`
2. 变量段:`VAR` / `VAR_INPUT` / `VAR_OUTPUT` / `VAR_GLOBAL` / `VAR_EXTERNAL`
3. 类型名只接受 `BOOL` `INT` `TIME`;内置 FB 名 `TON` `TOF` `CTU`
4. 语句:赋值、`IF``WHILE`、FB 调用 `fb(in := …)`、字段 `fb.Q`
5. 表达式:`NOT``AND`/`OR`(AST 里保留短路语义)、比较、加减乘除
拒绝时给明确错误,不要 silently 忽略:`VAR_IN_OUT`、指针/`REF``CLASS``ANY`、用户类型套娃。
完成:`line1` 三个文件能 parse;用例 10、14、19 若已能在语法/结构层拒绝则先红后绿。
---
### 12.6 符号表与链接(`compiler`,约 34 天)
```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`,约 57 天)
局部和 `VAR` 固定寄存器;表达式临时值往后编号;`nregs` 写进函数头。跳转先留洞,再回填。`AND`/`OR` **必须编成跳转**,禁止两边都算。
建议按指令面切片,每片都能 `isa` 编解码、必要时用临时 VM 桩执行:
1. `MOVE` `LOADK` `RET`,空 `MAIN` 出映像(对应用例 12
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`(用例 78
7. `CALL fn_id` + 有界帧(用例 13
8. FB 实例块 + 字段偏移;`CAL_TON` / `CAL_TOF` / `CAL_CTU`(用例 15、17、18
完成:`line1` 能写出映像(函数字节码 + 全局/FB 槽 + 工程哈希);跳转回填无悬空。
---
### 12.9 VM 循环(`vm`,约 34 天)
此时才写执行循环。只认 `isa` 映像。
```text
采样 I → pc=0 执行到 RET → 写回 Q → 用本周期 Δt 推进 TON/TOF/CTU
```
- 译码 + 大 `switch`;寄存器文件按函数头 `nregs` 分配。
- 每周期计指令数,超过 `cycle_limit` 报故障(用例 6)。
- 调用栈有界;`CALL`/`RET` 恢复 `pc` 与帧。
- 无 GC、无堆、无线程。同一映像 + 同一组 `I` + 同一 `Δt``Q`/`M`/FB 一致。
- 内置 FB`CAL_TON`/`CAL_TOF`/`CAL_CTU` 只用映像里的 Δt,不读系统时钟。
完成:对手工拼的小映像能跑赋值和跳转;再接编译器产出的映像。
---
### 12.10 host(约 23 天)
一条命令:读 toml → `compiler``vm` 跑 N 个周期。不拆第二个可执行文件。
-`[[io.*]]` 从宿主缓冲采样 / 写回,不创造变量。
-`dt_ms` 每周期传给 VM。
- 最小可观测:打印本周期 I/Q,或把寄存器/映像 dump 出来。
- 回放:读预先录制的 I 序列(文本即可),不接真实硬件。
完成:`host examples/line1/project.toml` 能编译并跑若干周期。
---
### 12.11 闭环与冻测试(约 1 周)
- 点亮第 12.2 节 20 个用例;负例检查报错类别,不查英文原文。
- `line1` 固定输入表:急停、start/stop 组合 → 期望 `Q0_0`
- 回放:同一 I 序列跑两遍,槽位与哈希一致。
- 单步:能停在指令边界,看到寄存器和 I/Q/M(可先做打印,不必做 UI)。
完成标准(第一版可交付):**`MAIN` + 一个 Motor FB + I/O + 全局**,扫描周期可重复。
---
### 12.12 建议日历
| 周 | 做完 |
|---|---|
| 1 | 12.012.4:骨架、合同、样例清单、toml、词法 |
| 2 | 12.5AST,能 parse `line1`,拒绝多余特性 |
| 3 | 12.6~12.7:链接、定址、类型、io 校验 |
| 4 | 12.8 前半 + 12.9 前半:赋值/短路/IF 出映像,VM 能跑 |
| 5 | 12.8 后半 + 12.10CALL、FB、host 跑起来 |
| 6 | 定时器/计数器、io、Δt、`line1` 语义 |
| 7~8 | 20 用例、回放、单步、补漏 |
周 4 结束时就应能跑一个「读 I、写 Q」的假 MAIN。不要等 FB 和 TON 齐了才第一次执行。
---
### 12.13 依赖方向(全程遵守)
```text
host → compiler → isa
host → vm → isa
```
- 指令或映像布局有变:先改 `isa` 和编解码测试,再改两端。
- 新 ST 特性:先加 12.2 的用例行,再改关键字 / AST / 检查 / 码 / VM。
- 不要为了一个 pass 新建 CMake 库。