三个 compiler 源文件补 Doxygen 注释:函数功能与参数说明。
- Lexer.cpp:13 个函数 @brief/@param/@return(含 T# 字面量、行列语义) - Parser.cpp:24 个函数(含表达式优先级爬升、token 前瞻区分) - Linker.cpp:24 个函数(含六步流程、三色 DFS 环检测、io 校验) - 文件头统一:设计说明 + 函数清单;代码零改动,ctest 7/7 通过
This commit is contained in:
+146
-19
@@ -3,6 +3,28 @@
|
||||
* @brief ST 子集词法分析
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*
|
||||
* @details 设计说明(详见 Doc/compiler/词法.md):
|
||||
* - 主循环:跳过空白/注释 → 文件末尾推 END → 否则 lex_one 识别一个 token
|
||||
* - 大小写:关键字匹配与标识符存储统一折小写(与 IEC 一致)
|
||||
* - 行列:line/col 从 1 起;token 记录【起始】位置(消费会推进行列,
|
||||
* 故 lex_one 入口先捕获 tok_line_/tok_col_)
|
||||
* - 错误:稳定前缀 "lex error" + 文件 + 行列,供测试按类别匹配
|
||||
*
|
||||
* 函数清单:
|
||||
* - time_unit_ms TIME 字面量单位 → 毫秒倍数(未知返回 -1)
|
||||
* - Lexer::Lexer (构造)存源文本/文件名/输出指针,行列初始 (1,1)
|
||||
* - Lexer::run 主循环:跳空白注释 → 文件尾推 END → lex_one
|
||||
* - Lexer::fail 组装 "lex error: <msg> (file, line, col)" 返回 false
|
||||
* - Lexer::cur 当前字符(不消费)
|
||||
* - Lexer::at_end 是否已到源文末尾
|
||||
* - Lexer::advance 消费一个字符并推进行列(\n 行+1 列复位)
|
||||
* - Lexer::skip_space_and_comments 跳过空白与 (* *) 注释(不嵌套,未闭合报错)
|
||||
* - Lexer::push 用 token 起始位置产出 token 入流
|
||||
* - Lexer::lex_one 识别一个 token(T# → 标识符/关键字 → 整数 → 符号 → 报错)
|
||||
* - Lexer::lex_time 解析 T#<int><单位>... 段式 TIME 字面量,累加毫秒
|
||||
* - lex 对外入口:清空输出 → 构造 Lexer → run
|
||||
* - lex_file 读文件后调 lex;打不开算 lex error
|
||||
*/
|
||||
|
||||
#include "compiler/Lexer.h"
|
||||
@@ -15,61 +37,91 @@
|
||||
namespace compiler {
|
||||
namespace {
|
||||
|
||||
// 25 个关键字:小写键 → Tok(大小写不敏感,见 Doc/compiler/词法.md)
|
||||
// 关键字表:小写键 → Tok。
|
||||
// 与 Doc/compiler/词法.md 的冻结表一致(12.5 修订补入 then/do);
|
||||
// 数值即 token 类型,只追加不删改。
|
||||
const struct {
|
||||
const char* key;
|
||||
Tok tok;
|
||||
} kKeywords[] = {
|
||||
// POU 外壳
|
||||
{"program", Tok::PROGRAM},
|
||||
{"function", Tok::FUNCTION},
|
||||
{"function_block", Tok::FUNCTION_BLOCK},
|
||||
{"end_program", Tok::END_PROGRAM},
|
||||
{"end_function", Tok::END_FUNCTION},
|
||||
{"end_function_block", Tok::END_FUNCTION_BLOCK},
|
||||
// 变量段
|
||||
{"var", Tok::VAR},
|
||||
{"var_input", Tok::VAR_INPUT},
|
||||
{"var_output", Tok::VAR_OUTPUT},
|
||||
{"var_global", Tok::VAR_GLOBAL},
|
||||
{"var_external", Tok::VAR_EXTERNAL},
|
||||
{"end_var", Tok::END_VAR},
|
||||
// 类型
|
||||
{"bool", Tok::BOOL},
|
||||
{"int", Tok::INT},
|
||||
{"time", Tok::TIME},
|
||||
// 控制
|
||||
{"if", Tok::IF},
|
||||
{"elsif", Tok::ELSIF},
|
||||
{"else", Tok::ELSE},
|
||||
{"end_if", Tok::END_IF},
|
||||
{"while", Tok::WHILE},
|
||||
{"end_while", Tok::END_WHILE},
|
||||
{"then", Tok::THEN},
|
||||
{"do", Tok::DO},
|
||||
{"then", Tok::THEN}, // 12.5 修订:IF…THEN 语法需要
|
||||
{"do", Tok::DO}, // 12.5 修订:WHILE…DO 语法需要
|
||||
// 逻辑
|
||||
{"and", Tok::AND},
|
||||
{"or", Tok::OR},
|
||||
{"not", Tok::NOT},
|
||||
// 字面量
|
||||
{"true", Tok::TRUE},
|
||||
{"false", Tok::FALSE},
|
||||
// 内建 FB 类型名(v1 冻结三件套)
|
||||
{"ton", Tok::TON},
|
||||
{"tof", Tok::TOF},
|
||||
{"ctu", Tok::CTU},
|
||||
};
|
||||
|
||||
// TIME 字面量单位 → 毫秒倍数(不敏感)
|
||||
/**
|
||||
* @brief TIME 字面量单位换算为毫秒倍数
|
||||
* @param unit 单位字符串(小写):ms / s / m / h / d
|
||||
* @return 对应毫秒倍数;未知单位返回 -1
|
||||
*/
|
||||
int64_t time_unit_ms(const std::string& unit) {
|
||||
if (unit == "ms") return 1;
|
||||
if (unit == "s") return 1000;
|
||||
if (unit == "m") return 60000;
|
||||
if (unit == "h") return 3600000;
|
||||
if (unit == "d") return 86400000;
|
||||
return -1;
|
||||
return -1; // 未知单位
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ST 单遍扫描器
|
||||
*
|
||||
* @details pos_ 指向当前字符,line_/col_ 是它所在的 1 起行列。
|
||||
* 所有成员函数不抛异常,出错即返回 false 并写 err_。
|
||||
*/
|
||||
class Lexer {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造扫描器
|
||||
* @param source_file 源文件名(报错与 token 的 source_file 用)
|
||||
* @param content 源文本(引用外部存储,不拷贝)
|
||||
* @param out token 流输出(run 前应为空)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
*/
|
||||
Lexer(const std::string& source_file, const std::string& content,
|
||||
std::vector<Token>* out, std::string* err)
|
||||
: src_(content), sf_(source_file), out_(out), err_(err),
|
||||
pos_(0), line_(1), col_(1) {}
|
||||
|
||||
/**
|
||||
* @brief 主循环:跳空白/注释 → 文件尾推 END → lex_one
|
||||
* @return true 成功(out 以 END 收尾);false 词法错误(err 已写)
|
||||
*/
|
||||
bool run() {
|
||||
while (true) {
|
||||
if (!skip_space_and_comments()) {
|
||||
@@ -86,6 +138,11 @@ namespace {
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 组装 "lex error: <msg> (file, line, col)" 写入 err
|
||||
* @param msg 错误描述(不含前缀与位置)
|
||||
* @return 恒 false(便于 return fail(...) 一行退出)
|
||||
*/
|
||||
bool fail(const std::string& msg) {
|
||||
if (err_) {
|
||||
char buf[128];
|
||||
@@ -96,9 +153,24 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当前字符(不消费)
|
||||
* @return src_[pos_];调用前需保证 !at_end()
|
||||
*/
|
||||
char cur() const { return src_[pos_]; }
|
||||
|
||||
/**
|
||||
* @brief 是否已到源文末尾
|
||||
* @return pos_ >= src_.size()
|
||||
*/
|
||||
bool at_end() const { return pos_ >= src_.size(); }
|
||||
|
||||
/**
|
||||
* @brief 消费一个字符并推进行列
|
||||
*
|
||||
* @details '\n' → 行 +1、列复位 1;其他字符 → 列 +1。
|
||||
* 注意:token 的起始行列要在此前捕获(见 lex_one)。
|
||||
*/
|
||||
void advance() {
|
||||
if (cur() == '\n') {
|
||||
++line_;
|
||||
@@ -109,7 +181,13 @@ namespace {
|
||||
++pos_;
|
||||
}
|
||||
|
||||
// 跳过空白与 (* *) 注释(不嵌套);未闭合返回 false
|
||||
/**
|
||||
* @brief 跳过空白与 (* *) 注释
|
||||
*
|
||||
* @details 第一版注释不嵌套:遇到第一个 "*) 即结束
|
||||
* (IEC 允许嵌套,留待以后)。未闭合 → lex error。
|
||||
* @return true 正常;false 未闭合注释(err 已写)
|
||||
*/
|
||||
bool skip_space_and_comments() {
|
||||
while (!at_end()) {
|
||||
if (std::isspace(static_cast<unsigned char>(cur()))) {
|
||||
@@ -139,6 +217,15 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 产出 token 入流
|
||||
*
|
||||
* @details 位置用 tok_line_/tok_col_(本 token 起始位置,
|
||||
* 而非 push 时刻的位置——此时消费已推进了行列)。
|
||||
* @param type token 类型
|
||||
* @param text 文本(标识符/关键字的小写形;字面量原文)
|
||||
* @param value 字面量值(INT_LIT / TIME_LIT 毫秒;其他为 0)
|
||||
*/
|
||||
void push(Tok type, const std::string& text, int64_t value) {
|
||||
Token t;
|
||||
t.type = type;
|
||||
@@ -150,6 +237,17 @@ namespace {
|
||||
out_->push_back(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 识别一个 token(当前字符开始)
|
||||
*
|
||||
* @details 分支顺序有讲究:
|
||||
* 1. T#(TIME 字面量)必须优先于标识符——'T' 是字母,否则会被
|
||||
* 标识符分支吃掉;"timer" 这类普通词不受影响(只匹配 T#)。
|
||||
* 2. 标识符/关键字(含下划线,字母/数字/下划线组成)。
|
||||
* 3. 整数(纯数字;负数不是字面量,-5 是 MINUS + 5)。
|
||||
* 4. 符号::= <> <= >= 最长匹配优先。
|
||||
* @return true 成功(已消费并 push);false 非法字符(err 已写)
|
||||
*/
|
||||
bool lex_one() {
|
||||
// 记录本 token 起始位置(push 时消费已经推进了行列)
|
||||
tok_line_ = line_;
|
||||
@@ -161,7 +259,8 @@ namespace {
|
||||
return lex_time();
|
||||
}
|
||||
|
||||
// 标识符 / 关键字(统一折小写)
|
||||
// 标识符 / 关键字:统一折小写存入 text(内部形),
|
||||
// 关键字命中即分类,否则 IDENT(用户定义名 / 内建类型名都走这里)。
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
|
||||
const uint32_t start = pos_;
|
||||
while (!at_end() &&
|
||||
@@ -182,7 +281,8 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 整数
|
||||
// 整数:十进制 [0-9]+,int64_t 存储;
|
||||
// 溢出检查在累加前做(v 上限 INT64_MAX)。
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
const uint32_t start = pos_;
|
||||
while (!at_end() && std::isdigit(static_cast<unsigned char>(cur()))) {
|
||||
@@ -201,7 +301,8 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 符号(最长匹配优先)
|
||||
// 符号:两字符符(:= <> <= >=)优先于单字符;
|
||||
// 其余单字符各归一类,非法字符直接报错。
|
||||
switch (c) {
|
||||
case ':':
|
||||
if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') {
|
||||
@@ -282,7 +383,14 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// T#<int><unit>... 段式解析,累加毫秒
|
||||
/**
|
||||
* @brief 解析 T#<整数><单位> 段式 TIME 字面量
|
||||
*
|
||||
* @details 段可多个,每段 = 数字 + 单位(ms/s/m/h/d,大小写不敏感):
|
||||
* T#10ms → 10;T#1s250ms → 1250;T#2h → 7200000。
|
||||
* @return true 成功(push TIME_LIT,int_value 为毫秒);
|
||||
* false 缺段 / 未知单位 / 数字溢出(err 已写)
|
||||
*/
|
||||
bool lex_time() {
|
||||
const uint32_t start = pos_;
|
||||
advance(); // T
|
||||
@@ -319,19 +427,31 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string& src_;
|
||||
const std::string& sf_;
|
||||
std::vector<Token>* out_;
|
||||
std::string* err_;
|
||||
size_t pos_;
|
||||
uint32_t line_;
|
||||
uint32_t col_;
|
||||
uint32_t tok_line_ = 1;
|
||||
uint32_t tok_col_ = 1;
|
||||
// ---- 状态 ----
|
||||
const std::string& src_; // 源文本(外部所有,不拷贝)
|
||||
const std::string& sf_; // 源文件名(报错用)
|
||||
std::vector<Token>* out_; // token 流输出
|
||||
std::string* err_; // 错误输出(可空)
|
||||
size_t pos_; // 当前字符下标
|
||||
uint32_t line_; // 当前行(1 起)
|
||||
uint32_t col_; // 当前列(1 起)
|
||||
uint32_t tok_line_ = 1; // 本 token 起始行(push 时用)
|
||||
uint32_t tok_col_ = 1; // 本 token 起始列(push 时用)
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 对源文本做词法分析(对外入口)
|
||||
*
|
||||
* @details 成功时 out 以 END token 结尾;失败时 out 内容不保证
|
||||
* (调用方以返回值为准)。
|
||||
* @param source_file 源文件名(写入每个 token 的 source_file)
|
||||
* @param content 源文本
|
||||
* @param out 输出 token 流(先清空)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
* @return true 成功;false 失败(err 前缀 "lex error")
|
||||
*/
|
||||
bool lex(const std::string& source_file, const std::string& content,
|
||||
std::vector<Token>* out, std::string* err) {
|
||||
out->clear();
|
||||
@@ -339,6 +459,13 @@ bool lex(const std::string& source_file, const std::string& content,
|
||||
return l.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 读取文件后词法分析
|
||||
* @param path 文件路径
|
||||
* @param out 输出 token 流
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
* @return true 成功;false 失败(文件打不开也算 lex error)
|
||||
*/
|
||||
bool lex_file(const std::string& path, std::vector<Token>* out, std::string* err) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
|
||||
+201
-6
@@ -3,6 +3,38 @@
|
||||
* @brief 符号表与链接
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*
|
||||
* @details 设计说明(详见 Doc/compiler/符号表与链接.md):
|
||||
* - 流程:收集导出(gvl 校验/全局定槽/POU 登记)→ 用户 FB 类型布局
|
||||
* → 逐 POU 局部符号与引用解析 → 函数调用图环检测 → io 校验 → 入口校验
|
||||
* - 数据区定址:v1 单一数据区,全局(含 I/Q/M)按声明顺序编槽号
|
||||
* - 错误:稳定前缀 "link error" + 文件(+ 行列可选)
|
||||
*
|
||||
* 函数清单:
|
||||
* - kind_name 符号种类 → 文本名(global/external/local/…)
|
||||
* - type_str 类型引用 → 文本名(bool/int/time/fb 名)
|
||||
* - is_scalar 是否为标量类型(BOOL/INT/TIME)
|
||||
* - builtin_layout 内建 FB 名 → 冻结布局(ton/tof/ctu),未知返回 nullptr
|
||||
* - Linker::Linker (构造)存工程/源文件/输出指针,规范化 gvl 路径
|
||||
* - Linker::run 链接主流程(6 步,任一步失败即停)
|
||||
* - Linker::fail 组装 "link error: <msg> (file[, line, col])" 返回 false
|
||||
* - Linker::is_gvl_file 路径是否工程指定的 gvl 文件(规范化比较)
|
||||
* - collect_exports 收集全局(仅 gvl 顶层)与 POU 导出,重复名拒绝
|
||||
* - collect_fb_types 收集用户 FUNCTION_BLOCK 的字段布局
|
||||
* - resolve_scopes 逐 POU 建局部符号并解析语句引用
|
||||
* - scope_of 按 POU 名取作用域(找不到返回静态空作用域)
|
||||
* - build_scope_syms 建一个 POU 的局部符号(含 EXTERNAL 接槽 / FB 实例布局)
|
||||
* - find_sym 在 POU 局部符号里按名查找
|
||||
* - check_stmt 校验一条语句(赋值/FB 调用/IF/WHILE 递归)
|
||||
* - check_expr 校验一个表达式(变量/字段/函数调用/递归子树)
|
||||
* - find_function 按名查 POU 表并回传种类
|
||||
* - check_cycles 函数依赖图 DFS 环检测入口
|
||||
* - dfs 单个函数的环检测(visiting/done 三色标记)
|
||||
* - check_io io 绑定必须已在 GVL(大小写折叠比较)
|
||||
* - has_program_main 必须存在 PROGRAM main(入口)
|
||||
* - link_project 对外入口:清空输出 → 构造 Linker → run
|
||||
* - load_unit 读取并解析一个 .st 成 SourceUnit
|
||||
* - dump_symbols 符号表文本:每行 "name kind type address file"
|
||||
*/
|
||||
|
||||
#include "compiler/Linker.h"
|
||||
@@ -19,6 +51,12 @@
|
||||
namespace compiler {
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief 符号种类 → 文本名(dump_symbols 用)
|
||||
* @param k 符号种类
|
||||
* @return "global" / "external" / "local" / "input" / "output" /
|
||||
* "pou" / "fb_instance" / "const";未知返回 "?"
|
||||
*/
|
||||
const char* kind_name(SymbolKind k) {
|
||||
switch (k) {
|
||||
case SymbolKind::Global: return "global";
|
||||
@@ -33,6 +71,11 @@ namespace {
|
||||
return "?";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 类型引用 → 文本名(符号表 / 类型比较用)
|
||||
* @param t 类型引用
|
||||
* @return "bool" / "int" / "time" / FB 类型名(小写);未知返回 "?"
|
||||
*/
|
||||
std::string type_str(const TypeRef& t) {
|
||||
switch (t.kind) {
|
||||
case TypeKind::Bool: return "bool";
|
||||
@@ -44,6 +87,11 @@ namespace {
|
||||
return "?";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 是否为标量类型(BOOL / INT / TIME)
|
||||
* @param k 类型种类
|
||||
* @return true 标量(FB 字段只允许标量,见 collect_fb_types)
|
||||
*/
|
||||
bool is_scalar(TypeKind k) {
|
||||
return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time;
|
||||
}
|
||||
@@ -57,6 +105,11 @@ namespace {
|
||||
{"pv", TypeKind::Int}, {"q", TypeKind::Bool},
|
||||
{"cv", TypeKind::Int}}};
|
||||
|
||||
/**
|
||||
* @brief 内建 FB 名 → 冻结布局
|
||||
* @param name FB 类型名(小写)
|
||||
* @return 布局指针(ton / tof / ctu);未知名字返回 nullptr
|
||||
*/
|
||||
const FbLayout* builtin_layout(const std::string& name) {
|
||||
if (name == "ton") return &kTonLayout;
|
||||
if (name == "tof") return &kTofLayout;
|
||||
@@ -64,15 +117,34 @@ namespace {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 链接器
|
||||
*
|
||||
* @details 六步流程见 run()。错误即停(首个错误优先,不做恢复)。
|
||||
*/
|
||||
class Linker {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造链接器
|
||||
* @param proj 工程定义(toml 解析结果;io 绑定 / gvl 文件 / base_dir)
|
||||
* @param units 全部源文件的 AST(compile_files 集合逐一 load_unit)
|
||||
* @param out 链接结果(调用方先构造空 LinkResult)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
*/
|
||||
Linker(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
LinkResult* out, std::string* err)
|
||||
: proj_(proj), units_(units), out_(out), err_(err) {
|
||||
// gvl 路径规范化一次,供 is_gvl_file 比较(容忍相对/绝对写法差异)
|
||||
gvl_path_ = std::filesystem::weakly_canonical(
|
||||
std::filesystem::path(proj.base_dir) / proj.gvl_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 链接主流程(6 步)
|
||||
* @details 1. 收集导出 → 2. 用户 FB 类型布局 → 3. 逐 POU 符号与引用
|
||||
* → 4. 循环调用检测 → 5. io 校验 → 6. 入口校验
|
||||
* @return true 链接成功;false 任一步失败(err 已写)
|
||||
*/
|
||||
bool run() {
|
||||
if (!collect_exports()) return false;
|
||||
if (!collect_fb_types()) return false;
|
||||
@@ -86,6 +158,14 @@ namespace {
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 组装 "link error: <msg> (file[, line, col])" 写入 err
|
||||
* @param file 出错文件(可为空串,用于无位置错误)
|
||||
* @param msg 错误描述(不含前缀与位置)
|
||||
* @param line 行列(1 起);0 表示不带行列只带文件
|
||||
* @param col 列号
|
||||
* @return 恒 false(便于 return fail(...) 一行退出)
|
||||
*/
|
||||
bool fail(const std::string& file, const std::string& msg,
|
||||
uint32_t line, uint32_t col) {
|
||||
if (err_) {
|
||||
@@ -101,6 +181,12 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 路径是否工程指定的 gvl 文件
|
||||
* @param path 待比较路径
|
||||
* @return true 是 gvl 文件(规范化后与 gvl_path_ 相同);
|
||||
* 工程未配置 gvl 时恒 false
|
||||
*/
|
||||
bool is_gvl_file(const std::string& path) const {
|
||||
if (proj_.gvl_file.empty()) {
|
||||
return false;
|
||||
@@ -110,6 +196,11 @@ namespace {
|
||||
|
||||
// ---- 1. 收集导出 ----
|
||||
|
||||
/**
|
||||
* @brief 收集导出:顶层全局(仅 gvl 文件)与 POU 登记
|
||||
* @details 全局按声明顺序编槽号(下标即槽号);同名全局 / 同名 POU 拒绝
|
||||
* @return true 成功;false(err 已写)
|
||||
*/
|
||||
bool collect_exports() {
|
||||
for (const SourceUnit& u : units_) {
|
||||
const bool is_gvl = is_gvl_file(u.path);
|
||||
@@ -157,6 +248,12 @@ namespace {
|
||||
|
||||
// ---- 2. 用户 FB 类型布局 ----
|
||||
|
||||
/**
|
||||
* @brief 收集用户 FUNCTION_BLOCK 的字段布局
|
||||
* @details 字段 = input / output / 内部 var(段序);
|
||||
* external 与 global 不是字段;字段类型仅允许标量
|
||||
* @return true 成功;false(FB 字段类型非法,err 已写)
|
||||
*/
|
||||
bool collect_fb_types() {
|
||||
for (const SourceUnit& u : units_) {
|
||||
for (const POU& p : u.ast.pous) {
|
||||
@@ -192,6 +289,10 @@ namespace {
|
||||
|
||||
// ---- 3. 逐 POU 建局部符号并解析引用 ----
|
||||
|
||||
/**
|
||||
* @brief 逐 POU 建局部符号并解析语句引用
|
||||
* @return true 成功;false(err 已写)
|
||||
*/
|
||||
bool resolve_scopes() {
|
||||
for (SourceUnit& u : const_cast<std::vector<SourceUnit>&>(units_)) {
|
||||
for (const POU& p : u.ast.pous) {
|
||||
@@ -209,6 +310,11 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按 POU 名取作用域
|
||||
* @param name POU 名(小写)
|
||||
* @return 对应 PouScope 引用;找不到返回静态空作用域(不应发生)
|
||||
*/
|
||||
LinkResult::PouScope& scope_of(const std::string& name) {
|
||||
for (LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.name == name) {
|
||||
@@ -219,6 +325,16 @@ namespace {
|
||||
return kEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 建一个 POU 的局部符号
|
||||
* @details VAR/INPUT/OUTPUT → 对应种类;VAR_EXTERNAL → 查全局表接同一槽
|
||||
* (类型必须一致);FB 类型 → FbInstance + 布局(内建取冻结表,
|
||||
* 用户类型查 fb_types);POU 内 VAR_GLOBAL → 拒绝
|
||||
* @param file 出错文件
|
||||
* @param p POU AST
|
||||
* @param sc 输出作用域(syms / fb_instances 被填充)
|
||||
* @return true 成功;false(err 已写)
|
||||
*/
|
||||
bool build_scope_syms(const std::string& file, const POU& p,
|
||||
LinkResult::PouScope& sc) {
|
||||
for (const VarBlock& b : p.blocks) {
|
||||
@@ -294,6 +410,12 @@ namespace {
|
||||
|
||||
// ---- 引用解析 ----
|
||||
|
||||
/**
|
||||
* @brief 在 POU 局部符号里按名查找
|
||||
* @param sc 作用域
|
||||
* @param name 符号名(小写)
|
||||
* @return 符号指针;未找到返回 nullptr
|
||||
*/
|
||||
const Symbol* find_sym(const LinkResult::PouScope& sc,
|
||||
const std::string& name) const {
|
||||
for (const Symbol& s : sc.syms) {
|
||||
@@ -304,6 +426,17 @@ namespace {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 校验一条语句
|
||||
* @details Assign:左值必须已声明(FUNCTION 内函数名赋值除外);
|
||||
* FbCall:实例必须存在且是 FB,命名实参必须是该实例的字段;
|
||||
* If / While:递归校验条件与各分支体
|
||||
* @param file 出错文件
|
||||
* @param sc 当前 POU 作用域
|
||||
* @param pou 当前 POU(函数名赋值判定用)
|
||||
* @param st 语句 AST
|
||||
* @return true 合法;false(err 已写)
|
||||
*/
|
||||
bool check_stmt(const std::string& file, const LinkResult::PouScope& sc,
|
||||
const POU& pou, const Stmt& st) {
|
||||
switch (st.kind) {
|
||||
@@ -379,6 +512,16 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 校验一个表达式
|
||||
* @details VarRef:局部或全局表查得到;Field:对象必须是 FB 实例
|
||||
* 且字段存在;Call:目标必须存在且是 FUNCTION(记调用边);
|
||||
* 其他节点递归校验子树
|
||||
* @param file 出错文件
|
||||
* @param sc 当前 POU 作用域
|
||||
* @param e 表达式 AST
|
||||
* @return true 合法;false(err 已写)
|
||||
*/
|
||||
bool check_expr(const std::string& file, const LinkResult::PouScope& sc,
|
||||
const Expr& e) {
|
||||
switch (e.kind) {
|
||||
@@ -431,6 +574,12 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按名查 POU 表并回传种类
|
||||
* @param name POU 名(小写)
|
||||
* @param kind 输出该 POU 的种类(PROGRAM / FUNCTION / FUNCTION_BLOCK)
|
||||
* @return true 找到;false 未找到
|
||||
*/
|
||||
bool find_function(const std::string& name, PouKind* kind) const {
|
||||
for (const LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.name == name) {
|
||||
@@ -443,6 +592,11 @@ namespace {
|
||||
|
||||
// ---- 4. 循环调用检测(函数依赖图 DFS)----
|
||||
|
||||
/**
|
||||
* @brief 函数依赖图环检测入口
|
||||
* @details 对每个有出边的函数做 DFS;任一条路径回到栈上节点即环
|
||||
* @return true 无环;false 发现环(err 已写)
|
||||
*/
|
||||
bool check_cycles() {
|
||||
std::set<std::string> visiting;
|
||||
std::set<std::string> done;
|
||||
@@ -454,6 +608,13 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 单个函数的环检测(三色标记 DFS)
|
||||
* @param fn 当前函数名
|
||||
* @param visiting 栈上节点(灰色,正在展开)
|
||||
* @param done 已确认无环的节点(黑色)
|
||||
* @return true 无环;false 命中灰色节点 = 环(err 已写)
|
||||
*/
|
||||
bool dfs(const std::string& fn, std::set<std::string>& visiting,
|
||||
std::set<std::string>& done) {
|
||||
if (done.count(fn)) {
|
||||
@@ -478,6 +639,12 @@ namespace {
|
||||
|
||||
// ---- 5. io 校验 ----
|
||||
|
||||
/**
|
||||
* @brief io 绑定必须已在 GVL 声明
|
||||
* @details 绑定名来自 toml(原始大小写),与全局表比较前折小写;
|
||||
* io 不创造变量,只做 channel/bit → 槽号映射
|
||||
* @return true 全部绑定合法;false(err 已写)
|
||||
*/
|
||||
bool check_io() {
|
||||
for (const isa::IoBinding& b : proj_.io) {
|
||||
std::string key = b.var;
|
||||
@@ -494,6 +661,10 @@ namespace {
|
||||
|
||||
// ---- 6. 入口 ----
|
||||
|
||||
/**
|
||||
* @brief 必须存在 PROGRAM main(与 toml entry = "program MAIN" 呼应)
|
||||
* @return true 存在
|
||||
*/
|
||||
bool has_program_main() const {
|
||||
for (const LinkResult::PouScope& sc : out_->scopes) {
|
||||
if (sc.kind == PouKind::Program && sc.name == "main") {
|
||||
@@ -503,16 +674,27 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Project& proj_;
|
||||
const std::vector<SourceUnit>& units_;
|
||||
LinkResult* out_;
|
||||
std::string* err_;
|
||||
std::filesystem::path gvl_path_;
|
||||
std::map<std::string, std::set<std::string>> call_edges_;
|
||||
// ---- 成员 ----
|
||||
const Project& proj_; // 工程定义(toml)
|
||||
const std::vector<SourceUnit>& units_; // 全部源文件 AST
|
||||
LinkResult* out_; // 链接结果
|
||||
std::string* err_; // 错误输出(可空)
|
||||
std::filesystem::path gvl_path_; // 规范化后的 gvl 路径
|
||||
std::map<std::string, std::set<std::string>> call_edges_; // 调用者 → 被调函数集
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 链接一个工程(对外入口)
|
||||
* @details 校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、
|
||||
* 函数循环调用检测、io 校验、入口校验
|
||||
* @param proj 工程定义(toml 解析结果)
|
||||
* @param units 全部源文件的 AST(compile_files 集合逐一 load_unit 得到)
|
||||
* @param out 链接结果(各字段先清空后填充)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
* @return true 成功;false 失败(err 前缀 "link error")
|
||||
*/
|
||||
bool link_project(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
LinkResult* out, std::string* err) {
|
||||
out->globals.clear();
|
||||
@@ -524,11 +706,24 @@ bool link_project(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
return l.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 读取并解析一个 .st 文件成 SourceUnit
|
||||
* @param path 文件路径
|
||||
* @param out 输出 SourceUnit(path + ast)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
* @return true 成功;false 失败(文件打不开 / 语法错误,透传 lex/syntax error)
|
||||
*/
|
||||
bool load_unit(const std::string& path, SourceUnit* out, std::string* err) {
|
||||
out->path = path;
|
||||
return parse_pous_file(path, &out->ast, err);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 符号表文本(完成标准:能打印 name kind type address file)
|
||||
* @param r 链接结果
|
||||
* @return 多行文本:全局表逐行 "name kind type address file",
|
||||
* 每个 POU 以 "[名字]" 开头后接其局部符号行
|
||||
*/
|
||||
std::string dump_symbols(const LinkResult& r) {
|
||||
std::string out;
|
||||
for (const Symbol& s : r.globals) {
|
||||
|
||||
+280
-14
@@ -3,6 +3,47 @@
|
||||
* @brief ST 子集递归下降语法分析
|
||||
* @author
|
||||
* @date 2026-08-21
|
||||
*
|
||||
* @details 设计说明(文法详见 Doc/compiler/语法.md):
|
||||
* - 输入:lexer 产出的 token 流(含末尾 END);输出:Unit(顶层全局段 + POU 列表)
|
||||
* - 递归下降 + 每层一个函数,token 流只前移不回溯(FB 调用/赋值用一 token 前瞻区分)
|
||||
* - 表达式按优先级爬升:or → and → not → cmp → add → mul → unary → primary,
|
||||
* AND/OR 的短路语义保留在 AST(12.8 编成跳转)
|
||||
* - 错误:稳定前缀 "syntax error" + 当前 token 的文件与行列
|
||||
*
|
||||
* 函数清单:
|
||||
* - is_forbidden 命中保留名(var_in_out/ref/class/any…)→ true
|
||||
* - Parser::Parser (构造)先整体 lex 成 token 流;词法失败透传 lex error
|
||||
* - Parser::run 顶层循环:顶层 VAR_GLOBAL 段(GVL 形态)或 POU 直到 END
|
||||
* - Parser::fail 组装 "syntax error: <msg> (file, line, col)" 返回 false
|
||||
* - Parser::cur/type/check token 视图:当前 token / 类型 / 是否指定类型
|
||||
* - Parser::advance 前进一个 token(END 处不动)
|
||||
* - Parser::expect 断言当前是 t,否则报 "expected 'what'"
|
||||
* - parse_pou POU 外壳:PROGRAM/FUNCTION[:类型]/FUNCTION_BLOCK + 段 + 体 + END_*
|
||||
* - is_var_section 当前 token 是否五种变量段关键字之一
|
||||
* - end_tok_for POU 种类 → 配对结束关键字
|
||||
* - check_end_for 是否已到本 POU 的配对结束关键字(语句体循环出口)
|
||||
* - parse_var_block VAR_* … END_VAR:逐行收集声明,定段种类
|
||||
* - parse_vars_line 一行声明展开成多个 VarDecl(共享类型与初值)
|
||||
* - parse_literal_expr 初值:只吃字面量(TRUE/FALSE/INT/TIME)
|
||||
* - parse_type 类型名:BOOL/INT/TIME/TON/TOF/CTU/标识符
|
||||
* - parse_stmt 语句分发:IF/WHILE 直入;IDENT 前瞻区分 FB 调用 vs 赋值
|
||||
* - parse_assign ident := expr ;(fb.field 左值拒绝)
|
||||
* - parse_fb_call ident(名 := expr, …); 命名实参 FB 调用
|
||||
* - parse_if IF … THEN … (ELSIF …)* (ELSE …)? END_IF
|
||||
* - parse_while WHILE … DO … END_WHILE
|
||||
* - parse_body 连续语句,遇 ELSIF/ELSE/END_IF/END_WHILE/END 停
|
||||
* - parse_expr 表达式入口 → parse_or
|
||||
* - parse_or/and OR/AND 层(短路保留在 AST,左结合)
|
||||
* - parse_not NOT 前缀一元(可连写)
|
||||
* - parse_cmp 比较层(a<b<c 链式拒绝)
|
||||
* - is_cmp_op 是否比较运算符 token
|
||||
* - parse_add/mul 加减/乘除层(左结合)
|
||||
* - parse_unary 一元负号(-5 = Neg(LitInt 5))
|
||||
* - parse_primary 原子:字面量/括号/ident(变量 | fb.field | 函数调用)
|
||||
* - parse_ident 标识符(非保留名,返回小写)
|
||||
* - parse_pous 对外入口:清空输出 → 构造 Parser → run
|
||||
* - parse_pous_file 读文件后调 parse_pous;打不开算 syntax error
|
||||
*/
|
||||
|
||||
#include "compiler/Parser.h"
|
||||
@@ -16,12 +57,19 @@
|
||||
namespace compiler {
|
||||
namespace {
|
||||
|
||||
// 明确拒绝的保留名(小写命中即报错,见 Doc/compiler/语法.md)
|
||||
// 明确拒绝的保留名(小写命中即报错,见 Doc/compiler/语法.md)。
|
||||
// 这些词在词法层不是关键字(lex 成 IDENT),必须在语法层显式拦截,
|
||||
// 避免 VAR_IN_OUT / REF / CLASS 等被静默当作用户标识符。
|
||||
const char* const kForbidden[] = {
|
||||
"var_in_out", "var_temp", "ref", "class", "any",
|
||||
"pointer", "interface", "method",
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 判断单词是否命中保留名清单
|
||||
* @param word 小写单词
|
||||
* @return true 命中(var_in_out / ref / class / any / pointer / interface / method)
|
||||
*/
|
||||
bool is_forbidden(const std::string& word) {
|
||||
for (const char* f : kForbidden) {
|
||||
if (word == f) {
|
||||
@@ -31,8 +79,21 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ST 递归下降解析器
|
||||
*
|
||||
* @details 两阶段:构造时一次性 lex 成 token 流,再逐层解析;
|
||||
* 不做流式交互。token 流只前移不回溯。
|
||||
*/
|
||||
class Parser {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造解析器(先整体 lex 成 token 流)
|
||||
* @param source_file 源文件名(报错用)
|
||||
* @param content 源文本
|
||||
* @param out 解析结果 Unit(globals / pous)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
*/
|
||||
Parser(const std::string& source_file, const std::string& content,
|
||||
Unit* out, std::string* err)
|
||||
: err_(err), out_(out) {
|
||||
@@ -45,6 +106,13 @@ namespace {
|
||||
pos_ = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 顶层循环:解析直至 END
|
||||
*
|
||||
* @details 每轮要么是 GVL 形态的顶层 VAR_GLOBAL 段(无 POU 的文件),
|
||||
* 要么是一个 POU。
|
||||
* @return true 成功(out 填满);false 语法错误(err 已写)
|
||||
*/
|
||||
bool run() {
|
||||
if (!ok_) {
|
||||
return false;
|
||||
@@ -74,6 +142,12 @@ namespace {
|
||||
private:
|
||||
// ---- 错误 ----
|
||||
|
||||
/**
|
||||
* @brief 组装 "syntax error: <msg> (file, line, col)" 写入 err
|
||||
* @details 位置取当前 token(指向出错处,不回溯到行首)
|
||||
* @param msg 错误描述(不含前缀与位置)
|
||||
* @return 恒 false(便于 return fail(...) 一行退出)
|
||||
*/
|
||||
bool fail(const std::string& msg) {
|
||||
if (err_) {
|
||||
const Token& t = cur();
|
||||
@@ -87,10 +161,30 @@ namespace {
|
||||
|
||||
// ---- token 视图 ----
|
||||
|
||||
/**
|
||||
* @brief 当前 token(不消费)
|
||||
* @return tokens_[pos_] 的引用
|
||||
*/
|
||||
const Token& cur() const { return tokens_[pos_]; }
|
||||
|
||||
/**
|
||||
* @brief 当前 token 的类型
|
||||
* @return cur().type
|
||||
*/
|
||||
Tok type() const { return cur().type; }
|
||||
|
||||
/**
|
||||
* @brief 当前 token 是否为指定类型
|
||||
* @param t 期望的 token 类型
|
||||
* @return true 匹配
|
||||
*/
|
||||
bool check(Tok t) const { return type() == t; }
|
||||
|
||||
/**
|
||||
* @brief 前进一个 token
|
||||
* @details END 处原地不动(pos_ 永不越界)
|
||||
* @return 前进前的 token 引用
|
||||
*/
|
||||
const Token& advance() {
|
||||
const Token& t = tokens_[pos_];
|
||||
if (pos_ + 1 < tokens_.size()) {
|
||||
@@ -99,6 +193,12 @@ namespace {
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 断言当前 token 是 t,否则报错
|
||||
* @param t 期望的 token 类型
|
||||
* @param what 错误信息中的名字(如 "END_IF")
|
||||
* @return true 匹配并前进;false 不匹配(err 已写)
|
||||
*/
|
||||
bool expect(Tok t, const char* what) {
|
||||
if (!check(t)) {
|
||||
return fail(std::string("expected '") + what + "'");
|
||||
@@ -109,6 +209,14 @@ namespace {
|
||||
|
||||
// ---- POU ----
|
||||
|
||||
/**
|
||||
* @brief 解析 POU 外壳
|
||||
*
|
||||
* @details PROGRAM / FUNCTION [: 返回类型] / FUNCTION_BLOCK,
|
||||
* 后接 0+ 变量段、0+ 语句,最后必须是配对 END_*。
|
||||
* @param p 输出 POU
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_pou(POU* p) {
|
||||
if (check(Tok::PROGRAM)) {
|
||||
advance();
|
||||
@@ -127,6 +235,7 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 仅 FUNCTION 有返回类型:FUNCTION Add : INT
|
||||
if (p->kind == PouKind::Function) {
|
||||
if (!expect(Tok::COLON, ":")) {
|
||||
return false;
|
||||
@@ -136,6 +245,7 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// 变量段可多个、可缺省,按声明顺序保留
|
||||
while (is_var_section()) {
|
||||
VarBlock b;
|
||||
if (!parse_var_block(&b)) {
|
||||
@@ -144,6 +254,7 @@ namespace {
|
||||
p->blocks.push_back(std::move(b));
|
||||
}
|
||||
|
||||
// 语句体直到配对 END_*
|
||||
while (!check_end_for(p->kind)) {
|
||||
Stmt s;
|
||||
if (!parse_stmt(&s)) {
|
||||
@@ -161,6 +272,10 @@ namespace {
|
||||
return expect(end_tok_for(p->kind), end_kw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当前 token 是否为五种变量段关键字之一
|
||||
* @return true 为 VAR / VAR_INPUT / VAR_OUTPUT / VAR_GLOBAL / VAR_EXTERNAL
|
||||
*/
|
||||
bool is_var_section() const {
|
||||
switch (type()) {
|
||||
case Tok::VAR:
|
||||
@@ -174,6 +289,12 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief POU 种类 → 配对结束关键字
|
||||
* @param k POU 种类
|
||||
* @return PROGRAM → END_PROGRAM、FUNCTION → END_FUNCTION、
|
||||
* FUNCTION_BLOCK → END_FUNCTION_BLOCK
|
||||
*/
|
||||
Tok end_tok_for(PouKind k) const {
|
||||
switch (k) {
|
||||
case PouKind::Program: return Tok::END_PROGRAM;
|
||||
@@ -183,10 +304,20 @@ namespace {
|
||||
return Tok::END;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当前 token 是否已到本 POU 的配对结束关键字
|
||||
* @param k POU 种类
|
||||
* @return true 语句体循环应停止
|
||||
*/
|
||||
bool check_end_for(PouKind k) const { return check(end_tok_for(k)); }
|
||||
|
||||
// ---- 变量段 ----
|
||||
|
||||
/**
|
||||
* @brief 解析 VAR_* … END_VAR 变量段
|
||||
* @param b 输出 VarBlock(section + vars)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_var_block(VarBlock* b) {
|
||||
switch (type()) {
|
||||
case Tok::VAR: b->section = VarSection::Local; break;
|
||||
@@ -212,7 +343,12 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ident (',' ident)* ':' type (':=' literal)? ';' —— 展开成多个 VarDecl
|
||||
/**
|
||||
* @brief 解析一行声明:ident (',' ident)* ':' type (':=' literal)? ';'
|
||||
* @details 同一行多个名字共享类型与初值,展开成多个 VarDecl
|
||||
* @param out 输出 VarDecl 列表
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_vars_line(std::vector<VarDecl>* out) {
|
||||
std::vector<VarDecl> names;
|
||||
while (true) {
|
||||
@@ -258,7 +394,12 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 初值只允许字面量(BOOL / INT / TIME / TRUE / FALSE)
|
||||
/**
|
||||
* @brief 解析初值字面量
|
||||
* @details 只允许字面量(TRUE/FALSE/INT/TIME);表达式初值 v1 不做
|
||||
* @param e 输出 Expr(LitBool / LitInt / LitTime)
|
||||
* @return true 成功;false 非字面量(err 已写)
|
||||
*/
|
||||
bool parse_literal_expr(Expr* e) {
|
||||
switch (type()) {
|
||||
case Tok::TRUE:
|
||||
@@ -286,7 +427,13 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// 类型:BOOL | INT | TIME | TON | TOF | CTU | ident(用户 FB 类型)
|
||||
/**
|
||||
* @brief 解析类型名
|
||||
* @details 内建标量 BOOL/INT/TIME、内建 FB TON/TOF/CTU、
|
||||
* 或标识符(用户 FB 类型,存在性由 12.6 链接层校验)
|
||||
* @param t 输出 TypeRef
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_type(TypeRef* t) {
|
||||
switch (type()) {
|
||||
case Tok::BOOL:
|
||||
@@ -333,6 +480,14 @@ namespace {
|
||||
|
||||
// ---- 语句 ----
|
||||
|
||||
/**
|
||||
* @brief 语句分发
|
||||
* @details IF / WHILE 关键字直入;IDENT 用一 token 前瞻区分:
|
||||
* ident '(' → FB 调用(无返回值,语句形态);
|
||||
* ident ('.' ident)? ':=' → 赋值
|
||||
* @param s 输出 Stmt
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_stmt(Stmt* s) {
|
||||
if (check(Tok::IF)) {
|
||||
return parse_if(s);
|
||||
@@ -352,6 +507,12 @@ namespace {
|
||||
return is_call ? parse_fb_call(s) : parse_assign(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析赋值语句:ident ':=' expr ';'
|
||||
* @details 左值仅标识符;ident.ident 左值(fb.field 赋值)v1 明确拒绝
|
||||
* @param s 输出 Stmt(kind=Assign)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_assign(Stmt* s) {
|
||||
s->kind = StmtKind::Assign;
|
||||
if (!parse_ident(&s->target)) {
|
||||
@@ -370,6 +531,13 @@ namespace {
|
||||
return expect(Tok::SEMI, ";");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析 FB 调用语句:ident '(' arg (',' arg)* ')' ';'
|
||||
* @details 全部命名实参(位置实参 v1 不做);
|
||||
* 字段名与类型由 12.6 / 12.7 校验
|
||||
* @param s 输出 Stmt(kind=FbCall)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_fb_call(Stmt* s) {
|
||||
s->kind = StmtKind::FbCall;
|
||||
if (!parse_ident(&s->instance)) {
|
||||
@@ -404,6 +572,11 @@ namespace {
|
||||
return expect(Tok::SEMI, ";");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析 IF 语句:IF cond THEN body (ELSIF cond THEN body)* (ELSE body)? END_IF
|
||||
* @param s 输出 Stmt(kind=If)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_if(Stmt* s) {
|
||||
s->kind = StmtKind::If;
|
||||
advance(); // IF
|
||||
@@ -441,6 +614,12 @@ namespace {
|
||||
return expect(Tok::END_IF, "END_IF");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析 WHILE 语句:WHILE cond DO body END_WHILE
|
||||
* @details 终止性由 cycle_limit 保证,不做证明
|
||||
* @param s 输出 Stmt(kind=While)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_while(Stmt* s) {
|
||||
s->kind = StmtKind::While;
|
||||
advance(); // WHILE
|
||||
@@ -457,6 +636,13 @@ namespace {
|
||||
return expect(Tok::END_WHILE, "END_WHILE");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析语句体(连续语句)
|
||||
* @details 遇 ELSIF / ELSE / END_IF / END_WHILE / END 停止;
|
||||
* 停止 token 由调用方消费,这里不越界
|
||||
* @param out 输出 Stmt 列表
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_body(std::vector<Stmt>* out) {
|
||||
while (!check(Tok::ELSIF) && !check(Tok::ELSE) && !check(Tok::END_IF) &&
|
||||
!check(Tok::END_WHILE) && !check(Tok::END)) {
|
||||
@@ -471,8 +657,20 @@ namespace {
|
||||
|
||||
// ---- 表达式(优先级爬升)----
|
||||
|
||||
/**
|
||||
* @brief 表达式入口
|
||||
* @details 优先级从低到高:or < and < not < cmp < add < mul < unary < primary。
|
||||
* 每个二目层用「局部链 + 末尾 move」累积左结合树(避免自引用指针)。
|
||||
* @param e 输出 Expr 根
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_expr(Expr* e) { return parse_or(e); }
|
||||
|
||||
/**
|
||||
* @brief OR 层:and_expr (OR and_expr)*(左结合)
|
||||
* @param e 输出 Expr 根(kind=Or 链或透传下层)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_or(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_and(lhs.get())) {
|
||||
@@ -481,7 +679,7 @@ namespace {
|
||||
while (check(Tok::OR)) {
|
||||
advance();
|
||||
auto node = std::make_unique<Expr>();
|
||||
node->kind = ExprKind::Or;
|
||||
node->kind = ExprKind::Or; // 短路语义保留到 12.8 编跳转
|
||||
node->lhs = std::move(lhs);
|
||||
node->rhs = std::make_unique<Expr>();
|
||||
if (!parse_and(node->rhs.get())) {
|
||||
@@ -493,6 +691,11 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AND 层:not_expr (AND not_expr)*(左结合)
|
||||
* @param e 输出 Expr 根(kind=And 链或透传下层)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_and(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_not(lhs.get())) {
|
||||
@@ -501,7 +704,7 @@ namespace {
|
||||
while (check(Tok::AND)) {
|
||||
advance();
|
||||
auto node = std::make_unique<Expr>();
|
||||
node->kind = ExprKind::And;
|
||||
node->kind = ExprKind::And; // 短路语义保留到 12.8 编跳转
|
||||
node->lhs = std::move(lhs);
|
||||
node->rhs = std::make_unique<Expr>();
|
||||
if (!parse_not(node->rhs.get())) {
|
||||
@@ -513,6 +716,12 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief NOT 层:NOT not_expr | cmp_expr
|
||||
* @details NOT 是前缀一元,可连写(NOT NOT x);比 AND/OR 紧
|
||||
* @param e 输出 Expr 根(kind=Not 或透传 cmp)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_not(Expr* e) {
|
||||
if (check(Tok::NOT)) {
|
||||
advance();
|
||||
@@ -523,6 +732,12 @@ namespace {
|
||||
return parse_cmp(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 比较层:add_expr (比较符 add_expr)?
|
||||
* @details 比较不连锁(a < b < c 拒绝);结果类型 BOOL 由 12.7 校验
|
||||
* @param e 输出 Expr 根(kind=Cmp 或透传 add)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_cmp(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_add(lhs.get())) {
|
||||
@@ -537,7 +752,7 @@ namespace {
|
||||
case Tok::GT: op = BinOp::Gt; break;
|
||||
case Tok::GE: op = BinOp::Ge; break;
|
||||
default:
|
||||
*e = std::move(*lhs);
|
||||
*e = std::move(*lhs); // 无比较符 → 原样上抛
|
||||
return true;
|
||||
}
|
||||
advance();
|
||||
@@ -545,6 +760,7 @@ namespace {
|
||||
if (!parse_add(rhs.get())) {
|
||||
return false;
|
||||
}
|
||||
// 右侧之后再出现比较符 → 链式,v1 拒绝
|
||||
if (is_cmp_op(type())) {
|
||||
return fail("chained comparison not supported");
|
||||
}
|
||||
@@ -555,11 +771,21 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 是否比较运算符 token(= <> < <= > >=)
|
||||
* @param t token 类型
|
||||
* @return true 是比较运算符
|
||||
*/
|
||||
bool is_cmp_op(Tok t) const {
|
||||
return t == Tok::EQ || t == Tok::NE || t == Tok::LT || t == Tok::LE ||
|
||||
t == Tok::GT || t == Tok::GE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加减层:mul_expr (('+'|'-') mul_expr)*(左结合)
|
||||
* @param e 输出 Expr 根(kind=Add/Sub 链或透传下层)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_add(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_mul(lhs.get())) {
|
||||
@@ -588,6 +814,11 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 乘除层:unary (('*'|'/') unary)*(左结合)
|
||||
* @param e 输出 Expr 根(kind=Mul/Div 链或透传下层)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_mul(Expr* e) {
|
||||
std::unique_ptr<Expr> lhs = std::make_unique<Expr>();
|
||||
if (!parse_unary(lhs.get())) {
|
||||
@@ -616,6 +847,12 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 一元负号层:'-' unary | primary
|
||||
* @details 可连写(- - x);注意 -5 不是字面量,而是 Neg(LitInt 5)
|
||||
* @param e 输出 Expr 根(kind=Neg 或透传 primary)
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_unary(Expr* e) {
|
||||
if (check(Tok::MINUS)) {
|
||||
advance();
|
||||
@@ -626,6 +863,13 @@ namespace {
|
||||
return parse_primary(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 原子:字面量 / 括号表达式 / ident
|
||||
* @details ident 后的 '.' 或 '(' 决定形态(变量 | fb.field | 函数调用),
|
||||
* 与语句层的区分逻辑一致
|
||||
* @param e 输出 Expr
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_primary(Expr* e) {
|
||||
switch (type()) {
|
||||
case Tok::INT_LIT:
|
||||
@@ -662,7 +906,7 @@ namespace {
|
||||
}
|
||||
advance();
|
||||
if (check(Tok::DOT)) {
|
||||
// fb.field
|
||||
// fb.field(字段存在性由 12.6 校验)
|
||||
advance();
|
||||
e->kind = ExprKind::Field;
|
||||
e->name = word;
|
||||
@@ -672,7 +916,7 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
if (check(Tok::LPAREN)) {
|
||||
// 函数调用(FUNCTION,有返回值)
|
||||
// 函数调用(FUNCTION,有返回值;与语句层 FB 调用区分)
|
||||
e->kind = ExprKind::Call;
|
||||
e->name = word;
|
||||
advance();
|
||||
@@ -700,6 +944,12 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析标识符
|
||||
* @details 必须是 IDENT 且非保留名
|
||||
* @param out 输出小写标识符文本
|
||||
* @return true 成功;false 语法错误(err 已写)
|
||||
*/
|
||||
bool parse_ident(std::string* out) {
|
||||
if (!check(Tok::IDENT)) {
|
||||
return fail("expected identifier");
|
||||
@@ -714,15 +964,24 @@ namespace {
|
||||
|
||||
// ---- 成员 ----
|
||||
|
||||
std::string* err_;
|
||||
std::vector<Token> tokens_;
|
||||
Unit* out_;
|
||||
size_t pos_ = 0;
|
||||
bool ok_ = true;
|
||||
std::string* err_; // 错误输出(可空)
|
||||
std::vector<Token> tokens_; // 整段 token 流(构造时一次性 lex 完成)
|
||||
Unit* out_; // 解析结果
|
||||
size_t pos_ = 0; // 当前 token 下标
|
||||
bool ok_ = true; // 词法阶段是否成功
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 对源文本做语法分析(对外入口)
|
||||
* @details 成功时 out 填满 globals/pous;失败时 out 内容不保证(以返回值为准)
|
||||
* @param source_file 源文件名(报错用)
|
||||
* @param content 源文本
|
||||
* @param out 输出 Unit(globals / pous,先清空)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
* @return true 成功;false 失败(err 前缀 "syntax error")
|
||||
*/
|
||||
bool parse_pous(const std::string& source_file, const std::string& content,
|
||||
Unit* out, std::string* err) {
|
||||
out->globals.clear();
|
||||
@@ -731,6 +990,13 @@ bool parse_pous(const std::string& source_file, const std::string& content,
|
||||
return p.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 读取文件后语法分析
|
||||
* @param path 文件路径
|
||||
* @param out 输出 Unit(globals / pous)
|
||||
* @param err 错误输出;可为 nullptr(静默)
|
||||
* @return true 成功;false 失败(文件打不开也算 syntax error)
|
||||
*/
|
||||
bool parse_pous_file(const std::string& path, Unit* out, std::string* err) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
|
||||
Reference in New Issue
Block a user