Files
Interpreter/Doc/初步计划.md
T
Admin 6873b5ee0b 文档同步:machine.toml 配置驱动架构收尾。
- README.md:依赖方向(STCompiler→compiler 不链 isa)、模块表、isa 归执行器侧、
  数据流图(machine.toml + 型号/SHA)
- isa/README.md:重写——只管指令定义(Types/Op/Instr/Encode),删 Image 部分
- 指令与映像.md:依赖图与 CMake(compiler 不链 isa)
- 编译管线.md:管线首步加载 machine.toml、产物含型号/SHA
- 初步计划.md:12.10/12.13 修订(--machine、依赖方向)
- 扫描周期.md / 指令执行.md:12.9 步骤标注已落地、vm 读校验说明
- compiler/vm/executor README:依赖与边界同步
2026-08-21 22:39:27 +08:00

20 KiB
Raw Permalink Blame History

StScan:子集 ST + toml 工程 + 寄存器 VM

独立方案说明。调研全文见 PLC-DSL.md

定案: IEC 风格的子集 ST,用 .toml 组织多个 .st,编译成单一映像,由寄存器虚拟机按扫描周期执行。


1. 做什么、不做什么

多个 .st 源文件
    ↑ 由 project.toml 列出
    ↓
编译器(词法/语法 → 符号表 → 类型检查 → 寄存器码 → 链接)
    ↓
单一映像(函数字节码 + 全局/FB 数据槽 + 工程哈希)
    ↓
寄存器虚拟机,按扫描周期执行

  • 子集 ST(不是完整 IEC
  • .toml 管工程,不声明变量
  • 寄存器 VM + 扫描周期
  • 有界 CALL / RET
  • 全局只在指定 GVL 文件里声明

不做

  • 「很像 ST 的 Python」
  • 完整 IEC:指针、OOP、ANYVAR_IN_OUT、间接寻址
  • CPython 特化 / JIT / 真 Python 模块系统
  • 第一版不做梯形图编辑器(以后编译到同一套 IR)

指令表可作为内部 IR 的文本形态,不必当主语言。


2. 执行模型

每个扫描周期:
  1. 按 toml 的 io 绑定,采样 → I / 输入全局
  2. 从 PROGRAM MAIN 的 pc=0 执行到 RET
  3. 写回 Q / 输出全局
  4. 用本周期 Δt 推进内置 FBTON/TOF/TP/CTU/CTD/CTUD/R_TRIG/F_TRIG,不用 wall clock

运行时是 一台 VM、一个入口、一份映像。多文件只影响源码怎么切、怎么链接。


3. 工程文件(toml

toml 不声明 ST 变量。只做三件事:列出源文件、指定唯一 GVL、(可选)把已声明的全局接到硬件。

[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

规则:

  • 所有源文件编译前已知;无运行时加载。
  • gvl.file 自动并入编译集合(已在 files.st 里则不重复)。
  • 文件集合排序后算 工程哈希,写入映像,回放时槽位一致。
  • 同名 VAR_GLOBAL 链接失败,禁止覆盖。
  • [[io.*]]var 未在 GVL 中出现 → 编译错误。
  • 第一版禁止:模块级副作用、实例/函数体循环调用、函数指针、无界递归。

不要在 toml 里用 [[globals]] name/type/init 再声明变量。
名字和类型属于 ST;针脚和通道属于工程。


4. ST 子集(第一版)

保留

类别 内容
POU PROGRAM / FUNCTION / FUNCTION_BLOCK
变量段 VAR / VAR_INPUT / VAR_OUTPUT / VAR_GLOBAL / VAR_EXTERNAL
类型 BOOLINTTIME(以后再加 DINT / 定点数或 REAL
运算 :=AND OR NOT,比较,算术
控制 IF / ELSIF / ELSE / END_IFWHILE … END_WHILE
功能块 内置 TON / TOF / CTUfb(in := …),字段 fb.Q

WHILE 不做终止证明,靠 cycle_limit 卡死。
AND / OR 必须短路,编成跳转;两边都算会让定时器、FB 多执行。

第一版不做: 指针、REF、接口、类、VAR_IN_OUTANY、间接寻址、用户类型套娃、FUNCTION 改全局。


5. 全局变量

ST 有全局:VAR_GLOBAL 声明,VAR_EXTERNAL 引用。I / Q / M 也是全局映像。
FUNCTION 第一版禁止写全局。

5.1 在哪个文件声明

第一版:只有 globals.st 可以出现 VAR_GLOBAL
多文件指 PROGRAM / FB / FUNCTION 分文件,不是全局可以散落声明。

种类 放哪 几个文件
VAR_GLOBAL / CONSTANT / RETAIN globals.st 1 个
I / Q / M 同一份 globals.st 第一版不拆
FB / PROGRAM 的 VAR 各自的 .st 多个(局部 / 实例)
VAR_EXTERNAL 各 POU 的 .st 多个(只引用)

其他文件写 VAR_GLOBAL → 编译错误。

单文件的好处:声明顺序 = 地址顺序,不必再规定多 GVL 的排序和回放。

以后要拆,仍须 toml 显式列出全部 GVL,按类别拆,不按功能块拆:

[gvl]
files = ["io.st", "globals.st"]
io.st        只放 I/Q/M          (以后才拆)
globals.st   只放命名全局
motor.st     只有 FB + VAR_EXTERNAL
main.st      只有 PROGRAM

不要 motor_globals.stpump_globals.st 各写一套同名点。

5.2 寿命

种类 寿命
VAR / VAR_TEMP 本次调用或该 POU 内部
FB 的 VAR 跟着实例(各一份)
VAR_GLOBAL 全工程一份
I/Q/M 全局,绑定 I/O 映像

VAR_EXTERNAL 接到已有槽,不是新变量。
第一版按 globals.st 声明顺序定址;以后若 [gvl] files:先 toml 列表,再文件内顺序。

5.3 编译器数据流

project.toml
  files.st     → 要编译的源
  gvl.file     → 唯一允许 VAR_GLOBAL 的文件
  io.*         → 名字 → 硬件(可选,不创造变量)

globals.st
  VAR_GLOBAL / CONSTANT / I/Q/M  → 符号表 + 固定槽位

motor.st / main.st
  VAR_EXTERNAL → 同一槽

6. 多文件导出什么

导出 规则
FUNCTION 无内部状态;寄存器帧在有界调用栈上
FUNCTION_BLOCK 必须先声明实例
VAR_GLOBALI/Q/M 全工程一份,只在 GVL 里声明
常量 进常量表

CALL 目标都是立即数 fn_id,没有函数指针。


7. 寄存器机与指令

编码宽度、操作码取值、映像头与定宽类型以 isa/指令与映像.md 为准。本节只列助记符。

  • 局部 / VAR = 固定寄存器;表达式临时值往后编号;nregs 写进函数头。
  • I/Q/M 走映像指令,不是通用寄存器。
  • 每个 FB 实例一块固定布局,字段偏移编译期算死。
MOVE rd, rs
LOADK rd, const_id
NOT rd, rs
AND/OR rd, ra, rb          (* 已算好的值;短路用跳转 *)
ADD/SUB/MUL/DIV rd, ra, rb
CMP_xx rd, ra, rb          (* 结果 BOOL *)
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

寄存器机不会比栈机更确定。确定性来自类型、内存、时间和副作用。


8. 确定性与可控

  • 同一程序 + 同一组 I + 同一 ΔtQM、FB 状态完全一致。
  • 无 GC、无堆、无线程;变量编译期全部分配。
  • 定宽整数;溢出规则写死(饱和或报错,选一个)。
  • 第一版不要 REAL
  • 每周期指令上限(如 cycle_limit = 100000),超时是故障。
  • 可单步、可看寄存器和映像、可按录制的 I 回放。

9. 编译器

中等工程,不是算法难题。直通 pass,不上 LLVM:

读 toml、收齐 .st
  → 词法 / 递归下降语法
  → 收集导出符号
  → 解析 VAR_EXTERNAL、链接、定地址
  → 类型检查
  → 按 PROGRAM / FUNCTION / FB 编寄存器码
  → 回填跳转、写映像头与工程哈希
部分 难度
ST 解析 中低
拒多余特性
多文件链接
类型检查 低~中
寄存器码(短路、IF/WHILE
FB 布局与调用
VM

第一版不做:图染色分配、SSA、证明 WHILE 有界、异常表、闭包、优化。

一人约 48 周 做出 MAIN + 一个 Motor FB + I/O + 全局


10. 目录

仓库布局见根目录 README.md。实现时按模块拆 CMake 目标(三个库 + 两个可执行),样例不放仓库根:

isa/                 # 指令编码、映像头、定宽类型(compiler 与 vm 的合同)
compiler/            # lexer parser symbols checker codegen linkerSTCompiler 入口在 src/main.cpp
vm/                  # 扫描周期、寄存器、映像、FB(库)
executor/            # BytecodeExecutor:加载 .stb + sidecar,喂 I、收 Q、Δt、回放
examples/line1/      # 第 11 节最小样例(给人看)
tests/               # 冻规则用的正例 / 负例

11. 最小样例

project.toml 见第 3 节。

(* globals.st *)
VAR_GLOBAL
    EmergencyStop : BOOL := FALSE;
    I0_0 : BOOL;
    I0_1 : BOOL;
    Q0_0 : BOOL;
END_VAR

(* 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

(* main.st *)
PROGRAM MAIN
VAR
    starter : MotorStarter;
END_VAR
    starter(start := I0_0, stop := I0_1);
    Q0_0 := starter.Q;
END_PROGRAM

12. 实现顺序

原则:先冻合同,再解析,再链接,最后写 VMcompilervm 不互链,合同只进 isa。每一阶段先有可运行的测试,再进入下一阶段。一人约 4~8 周做到第 11 节 line1

第一版不做:图染色、SSA、WHILE 有界证明、异常表、闭包、优化、REAL


12.0 骨架(约 1 天)

CMake 三个库能空编译:isacompilervm;两个可执行 STCompilerBytecodeExecutor 先打印版本即可。

  • 落下 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_OUTREFCLASS 等出明确错误)。

符号表一行

name / kind / type / address / source_file
kind 至少:global / external / local / input / output / pou / fb_instance / const

指令(进 isa,约 20 条)

布局、操作码、映像、哈希、饱和以 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 MAINiovar/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 TP CTU CTD CTUD R_TRIG F_TRIG (12.11 修订:内置 FB 扩为 8 个)
  4. 语句:赋值、IFWHILE、FB 调用 fb(in := …)、字段 fb.Q
  5. 表达式:NOTAND/OR(AST 里保留短路语义)、比较、加减乘除

拒绝时给明确错误,不要 silently 忽略:VAR_IN_OUT、指针/REFCLASSANY、用户类型套娃。

完成:line1 三个文件能 parse;用例 10、14、19 若已能在语法/结构层拒绝则先红后绿。


12.6 符号表与链接(compiler,约 34 天)

收集导出 → 校验 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 等字段存在且类型对。
  • 第一版无隐式宽化;INTTIME 不混用。

完成:类型错误有稳定报文;合法 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/JFIF(用例 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 映像。

采样 I → pc=0 执行到 RET → 写回 Q → 用本周期 Δt 推进内置 FB
  • 译码 + 大 switch;寄存器文件按函数头 nregs 分配。
  • 每周期计指令数,超过 cycle_limit 报故障(用例 6)。
  • 调用栈有界;CALL/RET 恢复 pc 与帧。
  • 无 GC、无堆、无线程。同一映像 + 同一组 I + 同一 ΔtQ/M/FB 一致。
  • 内置 FBCAL_TON/CAL_TOF/CAL_CTU 只用映像里的 Δt,不读系统时钟。

完成:对手工拼的小映像能跑赋值和跳转;再接编译器产出的映像。


12.10 双可执行:STCompiler 与 BytecodeExecutor(约 23 天)

编译与执行拆成两个可执行,映像 .stb 是中间产物:

STCompiler <project.toml> -o <name>.stb --machine <machine.toml>
BytecodeExecutor <name>.stb [--cycles N]
  • STCompilercompiler不链 isa,编码查 machine.toml);BytecodeExecutorvm + isa不链 compiler
  • 产物两个文件:<name>.stb(头 104 含型号标识 + 段 + SHA-256 文件尾)+ <name>.runtime.tomlsidecarI/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), 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.5AST,能 parse line1,拒绝多余特性
3 12.6~12.7:链接、定址、类型、io 校验
4 12.8 前半 + 12.9 前半:赋值/短路/IF 出映像,VM 能跑
5 12.8 后半 + 12.10CALL、FB、两个可执行跑起来
6 定时器/计数器、io、Δt、line1 语义
78 20 用例、回放、单步、补漏

周 4 结束时就应能跑一个「读 I、写 Q」的假 MAIN。不要等 FB 和 TON 齐了才第一次执行。


12.13 依赖方向(全程遵守)

STCompiler      → compiler        (编码查 machine.toml;自带 .stb 写)
BytecodeExecutor → vm → isa        isa 只管指令;vm 自带 .stb 读)
  • 指令或映像布局有变:先改 isa 和编解码测试,再改两端。
  • 新 ST 特性:先加 12.2 的用例行,再改关键字 / AST / 检查 / 码 / VM。
  • 不要为了一个 pass 新建 CMake 库。