Compare commits
4
Commits
d4e2c1ae7c
...
36f2f5637c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f2f5637c | ||
|
|
6f02c18bbb | ||
|
|
93d2c96f20 | ||
|
|
4ba44459e2 |
@@ -0,0 +1,111 @@
|
||||
# 符号表与链接(12.6)
|
||||
|
||||
compiler 模块的符号收集与链接。输入:`Project`(toml)+ 各 `.st` 文件的 `Unit`(AST,见 [`语法.md`](语法.md));输出:链接结果(全局槽表、POU 符号表、函数表、FB 实例布局)。全工程定案见 [`初步计划.md`](../初步计划.md) 12.6。
|
||||
|
||||
## 做 / 不做
|
||||
|
||||
**做**
|
||||
|
||||
- 先扫完全部文件再解析引用(禁止边解析边执行)
|
||||
- 全局收集:仅 `gvl.file` 顶层 `VAR_GLOBAL`,声明顺序 = 全局槽顺序
|
||||
- `VAR_EXTERNAL` 接已有槽,种类/类型一致,不是新变量
|
||||
- `FUNCTION` 标无状态;`FUNCTION_BLOCK` 必须先有实例才能调用
|
||||
- `CALL` 目标解析成 `fn_id`(立即数),无函数指针
|
||||
- 校验:同名全局、未声明 `io.var`、重复 POU 名、未声明 FB 实例、函数循环调用 → 失败
|
||||
- FB 实例布局:按类型算字段清单(输入/输出/内部/内建 TON/TOF/CTU 固定字段)
|
||||
|
||||
**不做(第一版)**
|
||||
|
||||
- 寄存器分配(12.8)、类型检查(12.7)
|
||||
- 多 GVL 文件、I/Q/M 与全局分槽(v1 **单一数据区**,见下)
|
||||
|
||||
## 数据区定址(v1 定案)
|
||||
|
||||
- **单一数据区**:全部全局(含 I/Q/M)按 `globals.st` 声明顺序编槽号 0..n-1
|
||||
- 映像头 `n_globals` = 声明总数;`n_i` / `n_q` / `n_m` = 0(第一版不拆,见 [`初步计划.md`](../初步计划.md) 5.1)
|
||||
- `LOAD_I` / `STORE_Q` / `LOAD_GLOBAL` / `STORE_GLOBAL` 操作码不同,槽号指向同一数据区
|
||||
- io 绑定(toml)只负责 channel/bit → 槽号映射(sidecar),不创造变量
|
||||
|
||||
## 符号表一行
|
||||
|
||||
```cpp
|
||||
enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const };
|
||||
|
||||
struct Symbol {
|
||||
SymbolKind kind;
|
||||
std::string name; // 小写
|
||||
std::string type_name; // bool / int / time / fb 类型名(小写)
|
||||
uint32_t address; // 全局槽号(Global/External);POU 局部暂 0(12.8 分配)
|
||||
std::string source_file;
|
||||
uint32_t line, col;
|
||||
};
|
||||
```
|
||||
|
||||
`kind` 与 12.1 冻结表一致:`global` / `external` / `local` / `input` / `output` / `pou` / `fb_instance` / `const`(`const` 本期未用,保留)。
|
||||
|
||||
## 链接流程
|
||||
|
||||
```text
|
||||
1. 收集导出
|
||||
遍历全部文件:
|
||||
- 顶层 globals:仅 gvl.file 允许;同名 → duplicate global;声明顺序编槽号
|
||||
- POU 导出:program / function / fb 名登记;重复 → duplicate POU
|
||||
2. 逐 POU 建局部符号
|
||||
VAR / VAR_INPUT / VAR_OUTPUT → local / input / output
|
||||
VAR_EXTERNAL → 查全局表,必须存在、类型一致 → External(同一槽)
|
||||
POU 内 VAR_GLOBAL → error(仅 gvl 顶层允许)
|
||||
3. 语句引用解析
|
||||
- FB 调用 instance(...) → 局部表查实例,类型必须是 FB → 否则 undeclared FB instance
|
||||
- 字段读 fb.field → 实例类型有该字段(用户 FB 的 input/output;内建固定字段)
|
||||
- VarRef / 赋值左值 → 局部表或全局表查得到 → 否则 undeclared identifier
|
||||
- 函数调用 fn(...) → POU 表查得到且是 FUNCTION → 记依赖边;FUNCTION 不得调用 PROGRAM/FB
|
||||
4. 循环调用检测
|
||||
函数依赖图 DFS 找环 → recursive call(用例 19)
|
||||
5. io 校验
|
||||
io.var 必须在全局表 → 否则 io.var not in GVL(用例 12)
|
||||
6. 输出
|
||||
全局表 / POU 符号表 / 函数表(fn_id 按收集序)/ FB 实例布局
|
||||
```
|
||||
|
||||
## FB 实例布局
|
||||
|
||||
- 用户 FB:字段 = 全部 `VAR_INPUT` + `VAR_OUTPUT` + `VAR`(内部),按声明段序排
|
||||
- 内建(冻结,isa 规格):
|
||||
|
||||
| FB | 字段(名 → 类型) |
|
||||
|---|---|
|
||||
| TON | in BOOL, pt TIME, q BOOL, et TIME |
|
||||
| TOF | in BOOL, pt TIME, q BOOL, et TIME |
|
||||
| CTU | cu BOOL, r BOOL, pv INT, q BOOL, cv INT |
|
||||
|
||||
- 实例布局 = 类型字段表的一份拷贝;字段偏移 12.8 按对齐规则算
|
||||
|
||||
## 错误
|
||||
|
||||
稳定前缀 `link error`:
|
||||
|
||||
```text
|
||||
link error: duplicate global 'g1' (globals.st, line 4, col 1)
|
||||
link error: VAR_GLOBAL only allowed in gvl file (other.st, line 2, col 1)
|
||||
link error: io.var 'Nope' not in GVL (project.toml)
|
||||
link error: undeclared FB instance 'starter' (main.st, line 4, col 5)
|
||||
link error: recursive call involving 'a' (main.st, line 2, col 4)
|
||||
link error: undeclared identifier 'x' (main.st, line 3, col 3)
|
||||
```
|
||||
|
||||
## 完成标准
|
||||
|
||||
1. 用例 9(同一槽)、10(非 GVL 写全局)、11(同名全局)、12(io.var 未声明)、16(未声明实例)、19(循环调用)按表变绿
|
||||
2. 符号表能打印 `name kind type address file`
|
||||
3. 全部构建 + `ctest` 无回归
|
||||
|
||||
---
|
||||
|
||||
## 执行计划(单步确认)
|
||||
|
||||
1. **`Linker.h`(新建 `compiler/include/compiler/Linker.h`)**:`Symbol`/`SymbolKind` + 链接结果结构 + `link_project` 声明
|
||||
2. **`Linker.cpp` 前半**:收集导出(gvl 校验 / 全局定槽 / POU 登记)+ 逐 POU 局部符号
|
||||
3. **`Linker.cpp` 后半**:引用解析(FB 实例 / 字段 / 变量 / 函数)+ 循环检测 + io 校验
|
||||
4. **`linker_test`(`tests/src/linker_test.cpp` + CMake)**:用例 09/10/11/12/16/19 + 符号表打印断言
|
||||
5. **验证**:`cmake --build` + `ctest`(新增 `linker_links` 后 7/7 全绿)
|
||||
6. **提交**:`Linker.h/cpp` + `linker_test` + 文档
|
||||
@@ -29,6 +29,9 @@ TRUE FALSE
|
||||
TON TOF CTU
|
||||
```
|
||||
|
||||
> 12.5 修订:`THEN`、`DO` 补入关键字表(`IF ... THEN` / `WHILE ... DO` 语法需要),
|
||||
> 与 12.1 原表合并,其余不变。
|
||||
|
||||
其余单词一律当标识符;`VAR_IN_OUT`、`REF`、`CLASS` 等由语法层(12.5)出明确错误。
|
||||
|
||||
## Token
|
||||
|
||||
@@ -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<VarDecl> 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<Expr> lhs, rhs, operand; // 二目 / NOT / Neg
|
||||
std::vector<std::unique_ptr<Expr>> args; // Call 实参
|
||||
};
|
||||
|
||||
// 语句
|
||||
enum class StmtKind { Assign, If, While, FbCall };
|
||||
struct FbArg { std::string name; std::unique_ptr<Expr> value; };
|
||||
struct IfBranch { std::unique_ptr<Expr> cond; std::vector<Stmt> body; };
|
||||
|
||||
struct Stmt {
|
||||
StmtKind kind;
|
||||
// Assign
|
||||
std::string target; // 左值标识符
|
||||
bool target_is_field; // 左值是 fb.field(v1:只允许读取,禁止赋值)
|
||||
std::string field;
|
||||
std::unique_ptr<Expr> value;
|
||||
// If / While
|
||||
std::unique_ptr<Expr> cond;
|
||||
std::vector<Stmt> body; // IF/WHILE 体
|
||||
std::vector<IfBranch> elsifs;
|
||||
std::vector<Stmt> else_body;
|
||||
// FbCall
|
||||
std::string instance;
|
||||
std::vector<FbArg> args;
|
||||
};
|
||||
|
||||
enum class PouKind { Program, Function, FunctionBlock };
|
||||
struct POU {
|
||||
PouKind kind;
|
||||
std::string name;
|
||||
TypeRef result_type; // FUNCTION 才有
|
||||
std::vector<VarBlock> blocks;
|
||||
std::vector<Stmt> 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<VarBlock> globals; // 顶层 VAR_GLOBAL 段
|
||||
std::vector<POU> 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` + 文档
|
||||
@@ -367,6 +367,7 @@ CMake 三个库能空编译:`isa`、`compiler`、`vm`;两个可执行 `STCom
|
||||
`VAR` `VAR_INPUT` `VAR_OUTPUT` `VAR_GLOBAL` `VAR_EXTERNAL` `END_VAR`
|
||||
`BOOL` `INT` `TIME`
|
||||
`IF` `ELSIF` `ELSE` `END_IF` `WHILE` `END_WHILE`
|
||||
`THEN` `DO` (12.5 修订补入:`IF…THEN` / `WHILE…DO` 语法需要)
|
||||
`AND` `OR` `NOT`
|
||||
`TRUE` `FALSE`
|
||||
`TON` `TOF` `CTU`
|
||||
|
||||
@@ -9,6 +9,8 @@ doc/
|
||||
isa/指令与映像.md
|
||||
compiler/编译管线.md
|
||||
compiler/词法.md
|
||||
compiler/语法.md
|
||||
compiler/符号表与链接.md
|
||||
vm/扫描周期.md
|
||||
executor/执行器入口.md
|
||||
```
|
||||
@@ -19,6 +21,8 @@ doc/
|
||||
| [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** |
|
||||
| [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 |
|
||||
| [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 |
|
||||
| [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 |
|
||||
| [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 |
|
||||
| [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 |
|
||||
| [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 |
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ project(Compiler
|
||||
# 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里
|
||||
add_library(compiler STATIC
|
||||
./src/Lexer.cpp
|
||||
./src/Project.cpp)
|
||||
./src/Project.cpp
|
||||
./src/Parser.cpp
|
||||
./src/Linker.cpp)
|
||||
|
||||
target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include)
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace compiler {
|
||||
END_IF,
|
||||
WHILE,
|
||||
END_WHILE,
|
||||
THEN, // 12.5 修订:IF/WHILE 语法需要,补入关键字表
|
||||
DO, // 同上
|
||||
AND,
|
||||
OR,
|
||||
NOT,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file Linker.h
|
||||
* @brief 符号表与链接
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "compiler/Parser.h"
|
||||
#include "compiler/Project.h"
|
||||
|
||||
namespace compiler {
|
||||
|
||||
// 符号种类(与 Doc/初步计划.md 12.1 一致)
|
||||
enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const };
|
||||
|
||||
struct Symbol {
|
||||
SymbolKind kind = SymbolKind::Local;
|
||||
std::string name; // 小写
|
||||
std::string type_name; // bool / int / time / fb 类型名(小写)
|
||||
uint32_t address = 0; // 全局槽号(Global/External);局部暂 0(12.8 分配)
|
||||
std::string source_file;
|
||||
uint32_t line = 0;
|
||||
uint32_t col = 0;
|
||||
};
|
||||
|
||||
// FB 类型字段(实例布局用;v1 字段类型仅 BOOL/INT/TIME)
|
||||
struct FbField {
|
||||
std::string name;
|
||||
TypeKind type = TypeKind::Bool;
|
||||
};
|
||||
|
||||
struct FbLayout {
|
||||
std::string type_name; // 类型名(小写)
|
||||
std::vector<FbField> fields; // 段序:input → output → 内部 var
|
||||
};
|
||||
|
||||
// 一个源文件的 AST(供链接输入)
|
||||
struct SourceUnit {
|
||||
std::string path;
|
||||
Unit ast;
|
||||
};
|
||||
|
||||
// 链接结果
|
||||
struct LinkResult {
|
||||
// 全局槽表:下标即槽号(声明顺序)
|
||||
std::vector<Symbol> globals;
|
||||
std::map<std::string, uint32_t> global_index; // 名 → 槽号
|
||||
|
||||
// 用户 FB 类型布局(按类型名)
|
||||
std::map<std::string, FbLayout> fb_types;
|
||||
|
||||
// 每 POU 的局部符号与 FB 实例
|
||||
struct PouScope {
|
||||
std::string name;
|
||||
PouKind kind = PouKind::Program;
|
||||
std::vector<Symbol> syms; // local/input/output/external/fb_instance
|
||||
std::map<std::string, FbLayout> fb_instances; // 实例名 → 布局
|
||||
};
|
||||
std::vector<PouScope> scopes;
|
||||
|
||||
// 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序)
|
||||
std::vector<std::string> fn_order;
|
||||
};
|
||||
|
||||
// 链接一个工程:校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、
|
||||
// 函数循环调用检测、io 校验。失败返回 false,err 前缀 "link error"。
|
||||
bool link_project(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
LinkResult* out, std::string* err);
|
||||
|
||||
// 读取并解析一个 .st 文件成 SourceUnit
|
||||
bool load_unit(const std::string& path, SourceUnit* out, std::string* err);
|
||||
|
||||
// 符号表文本:每行 "name kind type address file"(全局表 + 各 POU 局部表)
|
||||
std::string dump_symbols(const LinkResult& r);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @file Parser.h
|
||||
* @brief ST 子集递归下降语法分析(AST 定义 + 入口)
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace compiler {
|
||||
|
||||
// ---- 类型引用 ----
|
||||
|
||||
enum class TypeKind { Bool, Int, Time, FbBuiltin, FbUser };
|
||||
|
||||
struct TypeRef {
|
||||
TypeKind kind = TypeKind::Bool;
|
||||
std::string name; // FbUser 时为用户 FB 类型名(小写)
|
||||
};
|
||||
|
||||
// ---- 变量段 ----
|
||||
|
||||
struct VarDecl {
|
||||
std::string name;
|
||||
TypeRef type;
|
||||
bool has_init = false; // 可选初值(v1 仅字面量)
|
||||
int64_t init_value = 0; // BOOL 0/1、INT 值、TIME 毫秒
|
||||
uint32_t line = 0;
|
||||
uint32_t col = 0;
|
||||
};
|
||||
|
||||
enum class VarSection { Local, Input, Output, Global, External };
|
||||
|
||||
struct VarBlock {
|
||||
VarSection section = VarSection::Local;
|
||||
std::vector<VarDecl> vars;
|
||||
};
|
||||
|
||||
// ---- 表达式 ----
|
||||
|
||||
enum class ExprKind {
|
||||
LitBool, // int_value 0/1
|
||||
LitInt, // int_value
|
||||
LitTime, // int_value 毫秒
|
||||
VarRef, // name
|
||||
Field, // name.field
|
||||
Not, // operand
|
||||
And, Or, // lhs, rhs(短路语义保留到 12.8)
|
||||
Cmp, Add, Sub, Mul, Div, // lhs, rhs,op
|
||||
Neg, // operand(一元负号)
|
||||
Call, // name + args(函数调用,有返回值)
|
||||
};
|
||||
|
||||
enum class BinOp { Eq, Ne, Lt, Le, Gt, Ge, Add, Sub, Mul, Div };
|
||||
|
||||
struct Expr {
|
||||
ExprKind kind = ExprKind::LitBool;
|
||||
BinOp op = BinOp::Eq; // 二目运算
|
||||
std::string name; // VarRef / Field 对象 / Call 函数名
|
||||
std::string field; // Field 字段名
|
||||
int64_t int_value = 0; // 字面量值
|
||||
std::unique_ptr<Expr> lhs;
|
||||
std::unique_ptr<Expr> rhs;
|
||||
std::unique_ptr<Expr> operand; // Not / Neg
|
||||
std::vector<std::unique_ptr<Expr>> args; // Call 实参
|
||||
};
|
||||
|
||||
// ---- 语句 ----
|
||||
|
||||
enum class StmtKind { Assign, If, While, FbCall };
|
||||
|
||||
struct Stmt; // 前置声明(IfBranch / Stmt 相互引用)
|
||||
|
||||
struct FbArg {
|
||||
std::string name;
|
||||
std::unique_ptr<Expr> value;
|
||||
};
|
||||
|
||||
struct IfBranch {
|
||||
std::unique_ptr<Expr> cond;
|
||||
std::vector<Stmt> body;
|
||||
};
|
||||
|
||||
// 前置声明:Stmt 内部自引用
|
||||
struct Stmt {
|
||||
StmtKind kind = StmtKind::Assign;
|
||||
// Assign
|
||||
std::string target; // 左值标识符
|
||||
bool target_is_field = false; // 左值 fb.field(v1 禁止赋值,语法层拒绝)
|
||||
std::string field;
|
||||
std::unique_ptr<Expr> value;
|
||||
// If / While
|
||||
std::unique_ptr<Expr> cond;
|
||||
std::vector<Stmt> body;
|
||||
std::vector<IfBranch> elsifs;
|
||||
std::vector<Stmt> else_body;
|
||||
// FbCall
|
||||
std::string instance;
|
||||
std::vector<FbArg> args;
|
||||
};
|
||||
|
||||
// ---- POU ----
|
||||
|
||||
enum class PouKind { Program, Function, FunctionBlock };
|
||||
|
||||
struct POU {
|
||||
PouKind kind = PouKind::Program;
|
||||
std::string name;
|
||||
TypeRef result_type; // FUNCTION 才有
|
||||
std::vector<VarBlock> blocks;
|
||||
std::vector<Stmt> body;
|
||||
std::string source_file;
|
||||
};
|
||||
|
||||
// 一个 .st 文件的解析结果:GVL 文件形态(无 POU)时只有 globals 非空
|
||||
struct Unit {
|
||||
std::vector<VarBlock> globals; // 顶层 VAR_GLOBAL 段(仅 GVL 文件)
|
||||
std::vector<POU> pous;
|
||||
};
|
||||
|
||||
// 解析一个 .st 文件的全部内容(顶层 VAR_GLOBAL 段或 POU)。
|
||||
// 失败返回 false,err 前缀 "syntax error",带文件与行列。
|
||||
bool parse_pous(const std::string& source_file, const std::string& content,
|
||||
Unit* out, std::string* err);
|
||||
|
||||
// 读文件后解析
|
||||
bool parse_pous_file(const std::string& path, Unit* out, std::string* err);
|
||||
}
|
||||
@@ -41,6 +41,8 @@ namespace {
|
||||
{"end_if", Tok::END_IF},
|
||||
{"while", Tok::WHILE},
|
||||
{"end_while", Tok::END_WHILE},
|
||||
{"then", Tok::THEN},
|
||||
{"do", Tok::DO},
|
||||
{"and", Tok::AND},
|
||||
{"or", Tok::OR},
|
||||
{"not", Tok::NOT},
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* @file Linker.cpp
|
||||
* @brief 符号表与链接
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*/
|
||||
|
||||
#include "compiler/Linker.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace compiler {
|
||||
namespace {
|
||||
|
||||
const char* kind_name(SymbolKind k) {
|
||||
switch (k) {
|
||||
case SymbolKind::Global: return "global";
|
||||
case SymbolKind::External: return "external";
|
||||
case SymbolKind::Local: return "local";
|
||||
case SymbolKind::Input: return "input";
|
||||
case SymbolKind::Output: return "output";
|
||||
case SymbolKind::Pou: return "pou";
|
||||
case SymbolKind::FbInstance: return "fb_instance";
|
||||
case SymbolKind::Const: return "const";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
std::string type_str(const TypeRef& t) {
|
||||
switch (t.kind) {
|
||||
case TypeKind::Bool: return "bool";
|
||||
case TypeKind::Int: return "int";
|
||||
case TypeKind::Time: return "time";
|
||||
case TypeKind::FbBuiltin:
|
||||
case TypeKind::FbUser: return t.name;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
bool is_scalar(TypeKind k) {
|
||||
return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time;
|
||||
}
|
||||
|
||||
// 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md)
|
||||
const FbLayout kTonLayout{"ton", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time},
|
||||
{"q", TypeKind::Bool}, {"et", TypeKind::Time}}};
|
||||
const FbLayout kTofLayout{"tof", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time},
|
||||
{"q", TypeKind::Bool}, {"et", TypeKind::Time}}};
|
||||
const FbLayout kCtuLayout{"ctu", {{"cu", TypeKind::Bool}, {"r", TypeKind::Bool},
|
||||
{"pv", TypeKind::Int}, {"q", TypeKind::Bool},
|
||||
{"cv", TypeKind::Int}}};
|
||||
|
||||
const FbLayout* builtin_layout(const std::string& name) {
|
||||
if (name == "ton") return &kTonLayout;
|
||||
if (name == "tof") return &kTofLayout;
|
||||
if (name == "ctu") return &kCtuLayout;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
class Linker {
|
||||
public:
|
||||
Linker(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
LinkResult* out, std::string* err)
|
||||
: proj_(proj), units_(units), out_(out), err_(err) {
|
||||
gvl_path_ = std::filesystem::weakly_canonical(
|
||||
std::filesystem::path(proj.base_dir) / proj.gvl_file);
|
||||
}
|
||||
|
||||
bool run() {
|
||||
if (!collect_exports()) return false;
|
||||
if (!collect_fb_types()) return false;
|
||||
if (!resolve_scopes()) return false;
|
||||
if (!check_cycles()) return false;
|
||||
if (!check_io()) return false;
|
||||
if (!has_program_main()) {
|
||||
return fail("", "missing entry 'program MAIN'", 0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool fail(const std::string& file, const std::string& msg,
|
||||
uint32_t line, uint32_t col) {
|
||||
if (err_) {
|
||||
char buf[256];
|
||||
if (line) {
|
||||
std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)",
|
||||
file.c_str(), line, col);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof buf, " (%s)", file.c_str());
|
||||
}
|
||||
*err_ = "link error: " + msg + buf;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_gvl_file(const std::string& path) const {
|
||||
if (proj_.gvl_file.empty()) {
|
||||
return false;
|
||||
}
|
||||
return std::filesystem::weakly_canonical(path) == gvl_path_;
|
||||
}
|
||||
|
||||
// ---- 1. 收集导出 ----
|
||||
|
||||
bool collect_exports() {
|
||||
for (const SourceUnit& u : units_) {
|
||||
const bool is_gvl = is_gvl_file(u.path);
|
||||
|
||||
for (const VarBlock& b : u.ast.globals) {
|
||||
if (!is_gvl) {
|
||||
return fail(u.path, "VAR_GLOBAL only allowed in gvl file",
|
||||
b.vars.empty() ? 0 : b.vars[0].line,
|
||||
b.vars.empty() ? 0 : b.vars[0].col);
|
||||
}
|
||||
for (const VarDecl& d : b.vars) {
|
||||
if (out_->global_index.count(d.name)) {
|
||||
return fail(u.path, "duplicate global '" + d.name + "'",
|
||||
d.line, d.col);
|
||||
}
|
||||
Symbol s;
|
||||
s.kind = SymbolKind::Global;
|
||||
s.name = d.name;
|
||||
s.type_name = type_str(d.type);
|
||||
s.address = static_cast<uint32_t>(out_->globals.size());
|
||||
s.source_file = u.path;
|
||||
s.line = d.line;
|
||||
s.col = d.col;
|
||||
out_->global_index[s.name] = s.address;
|
||||
out_->globals.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
for (const POU& p : u.ast.pous) {
|
||||
for (const LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.name == p.name) {
|
||||
return fail(u.path, "duplicate POU '" + p.name + "'",
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
LinkResult::PouScope sc;
|
||||
sc.name = p.name;
|
||||
sc.kind = p.kind;
|
||||
out_->scopes.push_back(std::move(sc));
|
||||
out_->fn_order.push_back(p.name);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 2. 用户 FB 类型布局 ----
|
||||
|
||||
bool collect_fb_types() {
|
||||
for (const SourceUnit& u : units_) {
|
||||
for (const POU& p : u.ast.pous) {
|
||||
if (p.kind != PouKind::FunctionBlock) {
|
||||
continue;
|
||||
}
|
||||
FbLayout lay;
|
||||
lay.type_name = p.name;
|
||||
for (const VarBlock& b : p.blocks) {
|
||||
// 字段 = input / output / 内部 var;external 与 global 不是字段
|
||||
if (b.section == VarSection::External ||
|
||||
b.section == VarSection::Global) {
|
||||
continue;
|
||||
}
|
||||
for (const VarDecl& d : b.vars) {
|
||||
if (!is_scalar(d.type.kind)) {
|
||||
return fail(u.path,
|
||||
"FB field type must be BOOL/INT/TIME ('" +
|
||||
d.name + "')",
|
||||
d.line, d.col);
|
||||
}
|
||||
FbField f;
|
||||
f.name = d.name;
|
||||
f.type = d.type.kind;
|
||||
lay.fields.push_back(f);
|
||||
}
|
||||
}
|
||||
out_->fb_types[p.name] = lay;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 3. 逐 POU 建局部符号并解析引用 ----
|
||||
|
||||
bool resolve_scopes() {
|
||||
for (SourceUnit& u : const_cast<std::vector<SourceUnit>&>(units_)) {
|
||||
for (const POU& p : u.ast.pous) {
|
||||
LinkResult::PouScope& sc = scope_of(p.name);
|
||||
if (!build_scope_syms(u.path, p, sc)) {
|
||||
return false;
|
||||
}
|
||||
for (const Stmt& st : p.body) {
|
||||
if (!check_stmt(u.path, sc, p, st)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LinkResult::PouScope& scope_of(const std::string& name) {
|
||||
for (LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.name == name) {
|
||||
return sc;
|
||||
}
|
||||
}
|
||||
static LinkResult::PouScope kEmpty;
|
||||
return kEmpty;
|
||||
}
|
||||
|
||||
bool build_scope_syms(const std::string& file, const POU& p,
|
||||
LinkResult::PouScope& sc) {
|
||||
for (const VarBlock& b : p.blocks) {
|
||||
for (const VarDecl& d : b.vars) {
|
||||
switch (b.section) {
|
||||
case VarSection::Global:
|
||||
return fail(file, "VAR_GLOBAL only allowed in gvl file",
|
||||
d.line, d.col);
|
||||
case VarSection::External: {
|
||||
const auto it = out_->global_index.find(d.name);
|
||||
if (it == out_->global_index.end()) {
|
||||
return fail(file, "undeclared external '" + d.name + "'",
|
||||
d.line, d.col);
|
||||
}
|
||||
const Symbol& g = out_->globals[it->second];
|
||||
if (g.type_name != type_str(d.type)) {
|
||||
return fail(file, "external type mismatch for '" + d.name + "'",
|
||||
d.line, d.col);
|
||||
}
|
||||
Symbol s;
|
||||
s.kind = SymbolKind::External;
|
||||
s.name = d.name;
|
||||
s.type_name = g.type_name;
|
||||
s.address = g.address;
|
||||
s.source_file = file;
|
||||
s.line = d.line;
|
||||
s.col = d.col;
|
||||
sc.syms.push_back(s);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const bool is_fb = d.type.kind == TypeKind::FbUser ||
|
||||
d.type.kind == TypeKind::FbBuiltin;
|
||||
Symbol s;
|
||||
s.kind = is_fb ? SymbolKind::FbInstance
|
||||
: (b.section == VarSection::Input
|
||||
? SymbolKind::Input
|
||||
: b.section == VarSection::Output
|
||||
? SymbolKind::Output
|
||||
: SymbolKind::Local);
|
||||
s.name = d.name;
|
||||
s.type_name = type_str(d.type);
|
||||
s.source_file = file;
|
||||
s.line = d.line;
|
||||
s.col = d.col;
|
||||
sc.syms.push_back(s);
|
||||
if (is_fb) {
|
||||
FbLayout lay;
|
||||
if (d.type.kind == TypeKind::FbBuiltin) {
|
||||
const FbLayout* bl = builtin_layout(d.type.name);
|
||||
if (!bl) {
|
||||
return fail(file, "unknown builtin FB '" + d.type.name + "'",
|
||||
d.line, d.col);
|
||||
}
|
||||
lay = *bl;
|
||||
} else {
|
||||
const auto it = out_->fb_types.find(d.type.name);
|
||||
if (it == out_->fb_types.end()) {
|
||||
return fail(file, "unknown FB type '" + d.type.name + "'",
|
||||
d.line, d.col);
|
||||
}
|
||||
lay = it->second;
|
||||
}
|
||||
sc.fb_instances[d.name] = lay;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 引用解析 ----
|
||||
|
||||
const Symbol* find_sym(const LinkResult::PouScope& sc,
|
||||
const std::string& name) const {
|
||||
for (const Symbol& s : sc.syms) {
|
||||
if (s.name == name) {
|
||||
return &s;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool check_stmt(const std::string& file, const LinkResult::PouScope& sc,
|
||||
const POU& pou, const Stmt& st) {
|
||||
switch (st.kind) {
|
||||
case StmtKind::Assign: {
|
||||
// FUNCTION 内对函数名赋值 = 结果值写入(用例 13 约定)
|
||||
const bool is_result_assign =
|
||||
pou.kind == PouKind::Function && st.target == pou.name;
|
||||
if (!is_result_assign && find_sym(sc, st.target) == nullptr &&
|
||||
out_->global_index.count(st.target) == 0) {
|
||||
return fail(file, "undeclared identifier '" + st.target + "'",
|
||||
0, 0);
|
||||
}
|
||||
return check_expr(file, sc, *st.value);
|
||||
}
|
||||
case StmtKind::FbCall: {
|
||||
const Symbol* inst = find_sym(sc, st.instance);
|
||||
if (inst == nullptr) {
|
||||
return fail(file, "undeclared FB instance '" + st.instance + "'",
|
||||
0, 0);
|
||||
}
|
||||
if (inst->kind != SymbolKind::FbInstance) {
|
||||
return fail(file, "'" + st.instance + "' is not an FB instance",
|
||||
0, 0);
|
||||
}
|
||||
const auto it = sc.fb_instances.find(st.instance);
|
||||
const FbLayout& lay = it->second;
|
||||
for (const FbArg& a : st.args) {
|
||||
bool found = false;
|
||||
for (const FbField& f : lay.fields) {
|
||||
if (f.name == a.name) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return fail(file,
|
||||
"unknown input '" + a.name + "' for FB '" +
|
||||
st.instance + "'",
|
||||
0, 0);
|
||||
}
|
||||
if (!check_expr(file, sc, *a.value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case StmtKind::If:
|
||||
if (!check_expr(file, sc, *st.cond)) {
|
||||
return false;
|
||||
}
|
||||
for (const Stmt& s : st.body) {
|
||||
if (!check_stmt(file, sc, pou, s)) return false;
|
||||
}
|
||||
for (const IfBranch& b : st.elsifs) {
|
||||
if (!check_expr(file, sc, *b.cond)) return false;
|
||||
for (const Stmt& s : b.body) {
|
||||
if (!check_stmt(file, sc, pou, s)) return false;
|
||||
}
|
||||
}
|
||||
for (const Stmt& s : st.else_body) {
|
||||
if (!check_stmt(file, sc, pou, s)) return false;
|
||||
}
|
||||
return true;
|
||||
case StmtKind::While:
|
||||
if (!check_expr(file, sc, *st.cond)) {
|
||||
return false;
|
||||
}
|
||||
for (const Stmt& s : st.body) {
|
||||
if (!check_stmt(file, sc, pou, s)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool check_expr(const std::string& file, const LinkResult::PouScope& sc,
|
||||
const Expr& e) {
|
||||
switch (e.kind) {
|
||||
case ExprKind::VarRef:
|
||||
if (find_sym(sc, e.name) == nullptr &&
|
||||
out_->global_index.count(e.name) == 0) {
|
||||
return fail(file, "undeclared identifier '" + e.name + "'", 0, 0);
|
||||
}
|
||||
return true;
|
||||
case ExprKind::Field: {
|
||||
const Symbol* inst = find_sym(sc, e.name);
|
||||
if (inst == nullptr || inst->kind != SymbolKind::FbInstance) {
|
||||
return fail(file, "undeclared FB instance '" + e.name + "'", 0, 0);
|
||||
}
|
||||
const auto it = sc.fb_instances.find(e.name);
|
||||
bool found = false;
|
||||
for (const FbField& f : it->second.fields) {
|
||||
if (f.name == e.field) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return fail(file, "unknown field '" + e.field + "' for FB '" +
|
||||
e.name + "'",
|
||||
0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case ExprKind::Call: {
|
||||
PouKind k = PouKind::Program;
|
||||
if (!find_function(e.name, &k)) {
|
||||
return fail(file, "undeclared function '" + e.name + "'", 0, 0);
|
||||
}
|
||||
if (k != PouKind::Function) {
|
||||
return fail(file, "'" + e.name + "' is not a function", 0, 0);
|
||||
}
|
||||
call_edges_[sc.name].insert(e.name);
|
||||
for (const auto& a : e.args) {
|
||||
if (!check_expr(file, sc, *a)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (e.lhs && !check_expr(file, sc, *e.lhs)) return false;
|
||||
if (e.rhs && !check_expr(file, sc, *e.rhs)) return false;
|
||||
if (e.operand && !check_expr(file, sc, *e.operand)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_function(const std::string& name, PouKind* kind) const {
|
||||
for (const LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.name == name) {
|
||||
*kind = sc.kind;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- 4. 循环调用检测(函数依赖图 DFS)----
|
||||
|
||||
bool check_cycles() {
|
||||
std::set<std::string> visiting;
|
||||
std::set<std::string> done;
|
||||
for (const auto& [fn, _] : call_edges_) {
|
||||
if (!dfs(fn, visiting, done)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool dfs(const std::string& fn, std::set<std::string>& visiting,
|
||||
std::set<std::string>& done) {
|
||||
if (done.count(fn)) {
|
||||
return true;
|
||||
}
|
||||
if (visiting.count(fn)) {
|
||||
return fail("", "recursive call involving '" + fn + "'", 0, 0);
|
||||
}
|
||||
visiting.insert(fn);
|
||||
const auto it = call_edges_.find(fn);
|
||||
if (it != call_edges_.end()) {
|
||||
for (const std::string& callee : it->second) {
|
||||
if (!dfs(callee, visiting, done)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
visiting.erase(fn);
|
||||
done.insert(fn);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 5. io 校验 ----
|
||||
|
||||
bool check_io() {
|
||||
for (const isa::IoBinding& b : proj_.io) {
|
||||
std::string key = b.var;
|
||||
for (char& ch : key) {
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
}
|
||||
if (out_->global_index.count(key) == 0) {
|
||||
return fail(proj_.name + "/project.toml",
|
||||
"io.var '" + b.var + "' not in GVL", 0, 0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 6. 入口 ----
|
||||
|
||||
bool has_program_main() const {
|
||||
for (const LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.kind == PouKind::Program && sc.name == "main") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const Project& proj_;
|
||||
const std::vector<SourceUnit>& units_;
|
||||
LinkResult* out_;
|
||||
std::string* err_;
|
||||
std::filesystem::path gvl_path_;
|
||||
std::map<std::string, std::set<std::string>> call_edges_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool link_project(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
LinkResult* out, std::string* err) {
|
||||
out->globals.clear();
|
||||
out->global_index.clear();
|
||||
out->fb_types.clear();
|
||||
out->scopes.clear();
|
||||
out->fn_order.clear();
|
||||
Linker l(proj, units, out, err);
|
||||
return l.run();
|
||||
}
|
||||
|
||||
bool load_unit(const std::string& path, SourceUnit* out, std::string* err) {
|
||||
out->path = path;
|
||||
return parse_pous_file(path, &out->ast, err);
|
||||
}
|
||||
|
||||
std::string dump_symbols(const LinkResult& r) {
|
||||
std::string out;
|
||||
for (const Symbol& s : r.globals) {
|
||||
out += s.name + " " + kind_name(s.kind) + " " + s.type_name + " " +
|
||||
std::to_string(s.address) + " " + s.source_file + "\n";
|
||||
}
|
||||
for (const LinkResult::PouScope& sc : r.scopes) {
|
||||
out += "[" + sc.name + "]\n";
|
||||
for (const Symbol& s : sc.syms) {
|
||||
out += s.name + " " + kind_name(s.kind) + " " + s.type_name + " " +
|
||||
std::to_string(s.address) + " " + s.source_file + "\n";
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace compiler
|
||||
@@ -0,0 +1,747 @@
|
||||
/**
|
||||
* @file Parser.cpp
|
||||
* @brief ST 子集递归下降语法分析
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*/
|
||||
|
||||
#include "compiler/Parser.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "compiler/Lexer.h"
|
||||
|
||||
namespace compiler {
|
||||
namespace {
|
||||
|
||||
// 明确拒绝的保留名(小写命中即报错,见 Doc/compiler/语法.md)
|
||||
const char* const kForbidden[] = {
|
||||
"var_in_out", "var_temp", "ref", "class", "any",
|
||||
"pointer", "interface", "method",
|
||||
};
|
||||
|
||||
bool is_forbidden(const std::string& word) {
|
||||
for (const char* f : kForbidden) {
|
||||
if (word == f) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
Parser(const std::string& source_file, const std::string& content,
|
||||
Unit* out, std::string* err)
|
||||
: err_(err), out_(out) {
|
||||
std::string lex_err;
|
||||
if (!lex(source_file, content, &tokens_, &lex_err)) {
|
||||
ok_ = false;
|
||||
*err_ = lex_err;
|
||||
return;
|
||||
}
|
||||
pos_ = 0;
|
||||
}
|
||||
|
||||
bool run() {
|
||||
if (!ok_) {
|
||||
return false;
|
||||
}
|
||||
while (!check(Tok::END)) {
|
||||
if (is_var_section()) {
|
||||
// 顶层变量段只允许 VAR_GLOBAL(GVL 文件形态)
|
||||
if (!check(Tok::VAR_GLOBAL)) {
|
||||
return fail("only VAR_GLOBAL allowed at file top level");
|
||||
}
|
||||
VarBlock b;
|
||||
if (!parse_var_block(&b)) {
|
||||
return false;
|
||||
}
|
||||
out_->globals.push_back(std::move(b));
|
||||
continue;
|
||||
}
|
||||
POU p;
|
||||
if (!parse_pou(&p)) {
|
||||
return false;
|
||||
}
|
||||
out_->pous.push_back(std::move(p));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// ---- 错误 ----
|
||||
|
||||
bool fail(const std::string& msg) {
|
||||
if (err_) {
|
||||
const Token& t = cur();
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)",
|
||||
t.source_file.c_str(), t.line, t.col);
|
||||
*err_ = "syntax error: " + msg + buf;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- token 视图 ----
|
||||
|
||||
const Token& cur() const { return tokens_[pos_]; }
|
||||
Tok type() const { return cur().type; }
|
||||
bool check(Tok t) const { return type() == t; }
|
||||
|
||||
const Token& advance() {
|
||||
const Token& t = tokens_[pos_];
|
||||
if (pos_ + 1 < tokens_.size()) {
|
||||
++pos_;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
bool expect(Tok t, const char* what) {
|
||||
if (!check(t)) {
|
||||
return fail(std::string("expected '") + what + "'");
|
||||
}
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- POU ----
|
||||
|
||||
bool parse_pou(POU* p) {
|
||||
if (check(Tok::PROGRAM)) {
|
||||
advance();
|
||||
p->kind = PouKind::Program;
|
||||
} else if (check(Tok::FUNCTION)) {
|
||||
advance();
|
||||
p->kind = PouKind::Function;
|
||||
} else if (check(Tok::FUNCTION_BLOCK)) {
|
||||
advance();
|
||||
p->kind = PouKind::FunctionBlock;
|
||||
} else {
|
||||
return fail("expected POU (PROGRAM / FUNCTION / FUNCTION_BLOCK)");
|
||||
}
|
||||
|
||||
if (!parse_ident(&p->name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p->kind == PouKind::Function) {
|
||||
if (!expect(Tok::COLON, ":")) {
|
||||
return false;
|
||||
}
|
||||
if (!parse_type(&p->result_type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (is_var_section()) {
|
||||
VarBlock b;
|
||||
if (!parse_var_block(&b)) {
|
||||
return false;
|
||||
}
|
||||
p->blocks.push_back(std::move(b));
|
||||
}
|
||||
|
||||
while (!check_end_for(p->kind)) {
|
||||
Stmt s;
|
||||
if (!parse_stmt(&s)) {
|
||||
return false;
|
||||
}
|
||||
p->body.push_back(std::move(s));
|
||||
}
|
||||
|
||||
const char* end_kw = nullptr;
|
||||
switch (p->kind) {
|
||||
case PouKind::Program: end_kw = "END_PROGRAM"; break;
|
||||
case PouKind::Function: end_kw = "END_FUNCTION"; break;
|
||||
case PouKind::FunctionBlock: end_kw = "END_FUNCTION_BLOCK"; break;
|
||||
}
|
||||
return expect(end_tok_for(p->kind), end_kw);
|
||||
}
|
||||
|
||||
bool is_var_section() const {
|
||||
switch (type()) {
|
||||
case Tok::VAR:
|
||||
case Tok::VAR_INPUT:
|
||||
case Tok::VAR_OUTPUT:
|
||||
case Tok::VAR_GLOBAL:
|
||||
case Tok::VAR_EXTERNAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Tok end_tok_for(PouKind k) const {
|
||||
switch (k) {
|
||||
case PouKind::Program: return Tok::END_PROGRAM;
|
||||
case PouKind::Function: return Tok::END_FUNCTION;
|
||||
case PouKind::FunctionBlock: return Tok::END_FUNCTION_BLOCK;
|
||||
}
|
||||
return Tok::END;
|
||||
}
|
||||
|
||||
bool check_end_for(PouKind k) const { return check(end_tok_for(k)); }
|
||||
|
||||
// ---- 变量段 ----
|
||||
|
||||
bool parse_var_block(VarBlock* b) {
|
||||
switch (type()) {
|
||||
case Tok::VAR: b->section = VarSection::Local; break;
|
||||
case Tok::VAR_INPUT: b->section = VarSection::Input; break;
|
||||
case Tok::VAR_OUTPUT: b->section = VarSection::Output; break;
|
||||
case Tok::VAR_GLOBAL: b->section = VarSection::Global; break;
|
||||
case Tok::VAR_EXTERNAL: b->section = VarSection::External; break;
|
||||
default:
|
||||
return fail("expected variable section");
|
||||
}
|
||||
advance();
|
||||
|
||||
while (!check(Tok::END_VAR)) {
|
||||
std::vector<VarDecl> line;
|
||||
if (!parse_vars_line(&line)) {
|
||||
return false;
|
||||
}
|
||||
for (VarDecl& d : line) {
|
||||
b->vars.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
advance(); // END_VAR
|
||||
return true;
|
||||
}
|
||||
|
||||
// ident (',' ident)* ':' type (':=' literal)? ';' —— 展开成多个 VarDecl
|
||||
bool parse_vars_line(std::vector<VarDecl>* out) {
|
||||
std::vector<VarDecl> names;
|
||||
while (true) {
|
||||
VarDecl d;
|
||||
d.line = cur().line;
|
||||
d.col = cur().col;
|
||||
if (!parse_ident(&d.name)) {
|
||||
return false;
|
||||
}
|
||||
names.push_back(std::move(d));
|
||||
if (!check(Tok::COMMA)) {
|
||||
break;
|
||||
}
|
||||
advance();
|
||||
}
|
||||
if (!expect(Tok::COLON, ":")) {
|
||||
return false;
|
||||
}
|
||||
TypeRef t;
|
||||
if (!parse_type(&t)) {
|
||||
return false;
|
||||
}
|
||||
bool has_init = false;
|
||||
int64_t init = 0;
|
||||
if (check(Tok::ASSIGN)) {
|
||||
advance();
|
||||
Expr e;
|
||||
if (!parse_literal_expr(&e)) {
|
||||
return false;
|
||||
}
|
||||
has_init = true;
|
||||
init = e.int_value;
|
||||
}
|
||||
if (!expect(Tok::SEMI, ";")) {
|
||||
return false;
|
||||
}
|
||||
for (VarDecl& d : names) {
|
||||
d.type = t;
|
||||
d.has_init = has_init;
|
||||
d.init_value = init;
|
||||
out->push_back(std::move(d));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 初值只允许字面量(BOOL / INT / TIME / TRUE / FALSE)
|
||||
bool parse_literal_expr(Expr* e) {
|
||||
switch (type()) {
|
||||
case Tok::TRUE:
|
||||
e->kind = ExprKind::LitBool;
|
||||
e->int_value = 1;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::FALSE:
|
||||
e->kind = ExprKind::LitBool;
|
||||
e->int_value = 0;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::INT_LIT:
|
||||
e->kind = ExprKind::LitInt;
|
||||
e->int_value = cur().int_value;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::TIME_LIT:
|
||||
e->kind = ExprKind::LitTime;
|
||||
e->int_value = cur().int_value;
|
||||
advance();
|
||||
return true;
|
||||
default:
|
||||
return fail("expected literal initializer");
|
||||
}
|
||||
}
|
||||
|
||||
// 类型:BOOL | INT | TIME | TON | TOF | CTU | ident(用户 FB 类型)
|
||||
bool parse_type(TypeRef* t) {
|
||||
switch (type()) {
|
||||
case Tok::BOOL:
|
||||
t->kind = TypeKind::Bool;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::INT:
|
||||
t->kind = TypeKind::Int;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::TIME:
|
||||
t->kind = TypeKind::Time;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::TON:
|
||||
t->kind = TypeKind::FbBuiltin;
|
||||
t->name = "ton";
|
||||
advance();
|
||||
return true;
|
||||
case Tok::TOF:
|
||||
t->kind = TypeKind::FbBuiltin;
|
||||
t->name = "tof";
|
||||
advance();
|
||||
return true;
|
||||
case Tok::CTU:
|
||||
t->kind = TypeKind::FbBuiltin;
|
||||
t->name = "ctu";
|
||||
advance();
|
||||
return true;
|
||||
case Tok::IDENT: {
|
||||
const std::string word = cur().text;
|
||||
if (is_forbidden(word)) {
|
||||
return fail("'" + word + "' is not supported");
|
||||
}
|
||||
t->kind = TypeKind::FbUser;
|
||||
t->name = word;
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return fail("expected type name");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 语句 ----
|
||||
|
||||
bool parse_stmt(Stmt* s) {
|
||||
if (check(Tok::IF)) {
|
||||
return parse_if(s);
|
||||
}
|
||||
if (check(Tok::WHILE)) {
|
||||
return parse_while(s);
|
||||
}
|
||||
if (!check(Tok::IDENT)) {
|
||||
return fail("expected statement");
|
||||
}
|
||||
|
||||
// ident '(' → FB 调用;ident ('.' ident)? ':=' → 赋值
|
||||
const size_t save = pos_;
|
||||
advance();
|
||||
const bool is_call = check(Tok::LPAREN);
|
||||
pos_ = save;
|
||||
return is_call ? parse_fb_call(s) : parse_assign(s);
|
||||
}
|
||||
|
||||
bool parse_assign(Stmt* s) {
|
||||
s->kind = StmtKind::Assign;
|
||||
if (!parse_ident(&s->target)) {
|
||||
return false;
|
||||
}
|
||||
if (check(Tok::DOT)) {
|
||||
return fail("assignment to FB field is not supported");
|
||||
}
|
||||
if (!expect(Tok::ASSIGN, ":=")) {
|
||||
return false;
|
||||
}
|
||||
s->value = std::make_unique<Expr>();
|
||||
if (!parse_expr(s->value.get())) {
|
||||
return false;
|
||||
}
|
||||
return expect(Tok::SEMI, ";");
|
||||
}
|
||||
|
||||
bool parse_fb_call(Stmt* s) {
|
||||
s->kind = StmtKind::FbCall;
|
||||
if (!parse_ident(&s->instance)) {
|
||||
return false;
|
||||
}
|
||||
if (!expect(Tok::LPAREN, "(")) {
|
||||
return false;
|
||||
}
|
||||
if (!check(Tok::RPAREN)) {
|
||||
while (true) {
|
||||
FbArg a;
|
||||
if (!parse_ident(&a.name)) {
|
||||
return false;
|
||||
}
|
||||
if (!expect(Tok::ASSIGN, ":=")) {
|
||||
return false;
|
||||
}
|
||||
a.value = std::make_unique<Expr>();
|
||||
if (!parse_expr(a.value.get())) {
|
||||
return false;
|
||||
}
|
||||
s->args.push_back(std::move(a));
|
||||
if (!check(Tok::COMMA)) {
|
||||
break;
|
||||
}
|
||||
advance();
|
||||
}
|
||||
}
|
||||
if (!expect(Tok::RPAREN, ")")) {
|
||||
return false;
|
||||
}
|
||||
return expect(Tok::SEMI, ";");
|
||||
}
|
||||
|
||||
bool parse_if(Stmt* s) {
|
||||
s->kind = StmtKind::If;
|
||||
advance(); // IF
|
||||
s->cond = std::make_unique<Expr>();
|
||||
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<Expr>();
|
||||
if (!parse_expr(b.cond.get())) {
|
||||
return false;
|
||||
}
|
||||
if (!expect(Tok::THEN, "THEN")) {
|
||||
return false;
|
||||
}
|
||||
if (!parse_body(&b.body)) {
|
||||
return false;
|
||||
}
|
||||
s->elsifs.push_back(std::move(b));
|
||||
}
|
||||
if (check(Tok::ELSE)) {
|
||||
advance();
|
||||
if (!parse_body(&s->else_body)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return expect(Tok::END_IF, "END_IF");
|
||||
}
|
||||
|
||||
bool parse_while(Stmt* s) {
|
||||
s->kind = StmtKind::While;
|
||||
advance(); // WHILE
|
||||
s->cond = std::make_unique<Expr>();
|
||||
if (!parse_expr(s->cond.get())) {
|
||||
return false;
|
||||
}
|
||||
if (!expect(Tok::DO, "DO")) {
|
||||
return false;
|
||||
}
|
||||
if (!parse_body(&s->body)) {
|
||||
return false;
|
||||
}
|
||||
return expect(Tok::END_WHILE, "END_WHILE");
|
||||
}
|
||||
|
||||
bool parse_body(std::vector<Stmt>* out) {
|
||||
while (!check(Tok::ELSIF) && !check(Tok::ELSE) && !check(Tok::END_IF) &&
|
||||
!check(Tok::END_WHILE) && !check(Tok::END)) {
|
||||
Stmt s;
|
||||
if (!parse_stmt(&s)) {
|
||||
return false;
|
||||
}
|
||||
out->push_back(std::move(s));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 表达式(优先级爬升)----
|
||||
|
||||
bool parse_expr(Expr* e) { return parse_or(e); }
|
||||
|
||||
bool parse_or(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_and(lhs.get())) {
|
||||
return false;
|
||||
}
|
||||
while (check(Tok::OR)) {
|
||||
advance();
|
||||
auto node = std::make_unique<Expr>();
|
||||
node->kind = ExprKind::Or;
|
||||
node->lhs = std::move(lhs);
|
||||
node->rhs = std::make_unique<Expr>();
|
||||
if (!parse_and(node->rhs.get())) {
|
||||
return false;
|
||||
}
|
||||
lhs = std::move(node);
|
||||
}
|
||||
*e = std::move(*lhs);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_and(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_not(lhs.get())) {
|
||||
return false;
|
||||
}
|
||||
while (check(Tok::AND)) {
|
||||
advance();
|
||||
auto node = std::make_unique<Expr>();
|
||||
node->kind = ExprKind::And;
|
||||
node->lhs = std::move(lhs);
|
||||
node->rhs = std::make_unique<Expr>();
|
||||
if (!parse_not(node->rhs.get())) {
|
||||
return false;
|
||||
}
|
||||
lhs = std::move(node);
|
||||
}
|
||||
*e = std::move(*lhs);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_not(Expr* e) {
|
||||
if (check(Tok::NOT)) {
|
||||
advance();
|
||||
e->kind = ExprKind::Not;
|
||||
e->operand = std::make_unique<Expr>();
|
||||
return parse_not(e->operand.get());
|
||||
}
|
||||
return parse_cmp(e);
|
||||
}
|
||||
|
||||
bool parse_cmp(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
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<Expr>();
|
||||
if (!parse_add(rhs.get())) {
|
||||
return false;
|
||||
}
|
||||
if (is_cmp_op(type())) {
|
||||
return fail("chained comparison not supported");
|
||||
}
|
||||
e->kind = ExprKind::Cmp;
|
||||
e->op = op;
|
||||
e->lhs = std::move(lhs);
|
||||
e->rhs = std::move(rhs);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_cmp_op(Tok t) const {
|
||||
return t == Tok::EQ || t == Tok::NE || t == Tok::LT || t == Tok::LE ||
|
||||
t == Tok::GT || t == Tok::GE;
|
||||
}
|
||||
|
||||
bool parse_add(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
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<Expr>();
|
||||
node->kind = k;
|
||||
node->lhs = std::move(lhs);
|
||||
node->rhs = std::make_unique<Expr>();
|
||||
if (!parse_mul(node->rhs.get())) {
|
||||
return false;
|
||||
}
|
||||
lhs = std::move(node);
|
||||
}
|
||||
*e = std::move(*lhs);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_mul(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
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<Expr>();
|
||||
node->kind = k;
|
||||
node->lhs = std::move(lhs);
|
||||
node->rhs = std::make_unique<Expr>();
|
||||
if (!parse_unary(node->rhs.get())) {
|
||||
return false;
|
||||
}
|
||||
lhs = std::move(node);
|
||||
}
|
||||
*e = std::move(*lhs);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_unary(Expr* e) {
|
||||
if (check(Tok::MINUS)) {
|
||||
advance();
|
||||
e->kind = ExprKind::Neg;
|
||||
e->operand = std::make_unique<Expr>();
|
||||
return parse_unary(e->operand.get());
|
||||
}
|
||||
return parse_primary(e);
|
||||
}
|
||||
|
||||
bool parse_primary(Expr* e) {
|
||||
switch (type()) {
|
||||
case Tok::INT_LIT:
|
||||
e->kind = ExprKind::LitInt;
|
||||
e->int_value = cur().int_value;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::TIME_LIT:
|
||||
e->kind = ExprKind::LitTime;
|
||||
e->int_value = cur().int_value;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::TRUE:
|
||||
e->kind = ExprKind::LitBool;
|
||||
e->int_value = 1;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::FALSE:
|
||||
e->kind = ExprKind::LitBool;
|
||||
e->int_value = 0;
|
||||
advance();
|
||||
return true;
|
||||
case Tok::LPAREN: {
|
||||
advance();
|
||||
if (!parse_expr(e)) {
|
||||
return false;
|
||||
}
|
||||
return expect(Tok::RPAREN, ")");
|
||||
}
|
||||
case Tok::IDENT: {
|
||||
const std::string word = cur().text;
|
||||
if (is_forbidden(word)) {
|
||||
return fail("'" + word + "' is not supported");
|
||||
}
|
||||
advance();
|
||||
if (check(Tok::DOT)) {
|
||||
// fb.field
|
||||
advance();
|
||||
e->kind = ExprKind::Field;
|
||||
e->name = word;
|
||||
if (!parse_ident(&e->field)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (check(Tok::LPAREN)) {
|
||||
// 函数调用(FUNCTION,有返回值)
|
||||
e->kind = ExprKind::Call;
|
||||
e->name = word;
|
||||
advance();
|
||||
if (!check(Tok::RPAREN)) {
|
||||
while (true) {
|
||||
auto arg = std::make_unique<Expr>();
|
||||
if (!parse_expr(arg.get())) {
|
||||
return false;
|
||||
}
|
||||
e->args.push_back(std::move(arg));
|
||||
if (!check(Tok::COMMA)) {
|
||||
break;
|
||||
}
|
||||
advance();
|
||||
}
|
||||
}
|
||||
return expect(Tok::RPAREN, ")");
|
||||
}
|
||||
e->kind = ExprKind::VarRef;
|
||||
e->name = word;
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return fail("expected expression");
|
||||
}
|
||||
}
|
||||
|
||||
bool parse_ident(std::string* out) {
|
||||
if (!check(Tok::IDENT)) {
|
||||
return fail("expected identifier");
|
||||
}
|
||||
if (is_forbidden(cur().text)) {
|
||||
return fail("'" + cur().text + "' is not supported");
|
||||
}
|
||||
*out = cur().text;
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 成员 ----
|
||||
|
||||
std::string* err_;
|
||||
std::vector<Token> tokens_;
|
||||
Unit* out_;
|
||||
size_t pos_ = 0;
|
||||
bool ok_ = true;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool parse_pous(const std::string& source_file, const std::string& content,
|
||||
Unit* out, std::string* err) {
|
||||
out->globals.clear();
|
||||
out->pous.clear();
|
||||
Parser p(source_file, content, out, err);
|
||||
return p.run();
|
||||
}
|
||||
|
||||
bool parse_pous_file(const std::string& path, Unit* out, std::string* err) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
if (err) {
|
||||
*err = "syntax error: cannot open file (" + path + ")";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::string content((std::istreambuf_iterator<char>(in)),
|
||||
std::istreambuf_iterator<char>());
|
||||
return parse_pous(path, content, out, err);
|
||||
}
|
||||
|
||||
} // namespace compiler
|
||||
@@ -34,3 +34,25 @@ target_compile_definitions(lexer_test PRIVATE
|
||||
|
||||
add_test(NAME lexer_tokens
|
||||
COMMAND lexer_test)
|
||||
|
||||
# 语法测试:line1 AST 结构 + 负例(REPO_ROOT 注入源目录绝对路径)
|
||||
add_executable(parser_test
|
||||
./src/parser_test.cpp)
|
||||
|
||||
target_link_libraries(parser_test PRIVATE compiler)
|
||||
target_compile_definitions(parser_test PRIVATE
|
||||
REPO_ROOT="${CMAKE_SOURCE_DIR}")
|
||||
|
||||
add_test(NAME parser_syntax
|
||||
COMMAND parser_test)
|
||||
|
||||
# 链接测试:用例 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)
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @file linker_test.cpp
|
||||
* @brief 链接测试:用例 09/10/11/12/16/19 + line1 正例
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<SourceUnit> 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<SourceUnit> 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<SourceUnit> 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;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* @file parser_test.cpp
|
||||
* @brief 语法测试:line1 AST 结构 + 负例
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user