diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index 4131e17..85d0d1f 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -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: (file, line, col)" 返回 false + * - Lexer::cur 当前字符(不消费) + * - Lexer::at_end 是否已到源文末尾 + * - Lexer::advance 消费一个字符并推进行列(\n 行+1 列复位) + * - Lexer::skip_space_and_comments 跳过空白与 (* *) 注释(不嵌套,未闭合报错) + * - Lexer::push 用 token 起始位置产出 token 入流 + * - Lexer::lex_one 识别一个 token(T# → 标识符/关键字 → 整数 → 符号 → 报错) + * - Lexer::lex_time 解析 T#<单位>... 段式 TIME 字面量,累加毫秒 + * - lex 对外入口:清空输出 → 构造 Lexer → run + * - lex_file 读文件后调 lex;打不开算 lex error */ #include "compiler/Lexer.h" @@ -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* 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: (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(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(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(c))) { const uint32_t start = pos_; while (!at_end() && std::isdigit(static_cast(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#... 段式解析,累加毫秒 + /** + * @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* 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* out_; // token 流输出 + std::string* err_; // 错误输出(可空) + size_t pos_; // 当前字符下标 + uint32_t line_; // 当前行(1 起) + uint32_t col_; // 当前列(1 起) + uint32_t tok_line_ = 1; // 本 token 起始行(push 时用) + uint32_t tok_col_ = 1; // 本 token 起始列(push 时用) }; } // namespace +/** + * @brief 对源文本做词法分析(对外入口) + * + * @details 成功时 out 以 END token 结尾;失败时 out 内容不保证 + * (调用方以返回值为准)。 + * @param source_file 源文件名(写入每个 token 的 source_file) + * @param content 源文本 + * @param out 输出 token 流(先清空) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "lex error") + */ bool lex(const std::string& source_file, const std::string& content, std::vector* out, std::string* err) { out->clear(); @@ -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* out, std::string* err) { std::ifstream in(path, std::ios::binary); if (!in) { diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp index c3ebaa3..e4fce44 100644 --- a/compiler/src/Linker.cpp +++ b/compiler/src/Linker.cpp @@ -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: (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& 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: (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&>(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 visiting; std::set 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& visiting, std::set& 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& units_; - LinkResult* out_; - std::string* err_; - std::filesystem::path gvl_path_; - std::map> call_edges_; + // ---- 成员 ---- + const Project& proj_; // 工程定义(toml) + const std::vector& units_; // 全部源文件 AST + LinkResult* out_; // 链接结果 + std::string* err_; // 错误输出(可空) + std::filesystem::path gvl_path_; // 规范化后的 gvl 路径 + std::map> call_edges_; // 调用者 → 被调函数集 }; } // namespace +/** + * @brief 链接一个工程(对外入口) + * @details 校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、 + * 函数循环调用检测、io 校验、入口校验 + * @param proj 工程定义(toml 解析结果) + * @param units 全部源文件的 AST(compile_files 集合逐一 load_unit 得到) + * @param out 链接结果(各字段先清空后填充) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "link error") + */ bool link_project(const Project& proj, const std::vector& units, LinkResult* out, std::string* err) { out->globals.clear(); @@ -524,11 +706,24 @@ bool link_project(const Project& proj, const std::vector& 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) { diff --git a/compiler/src/Parser.cpp b/compiler/src/Parser.cpp index 6778934..38a570d 100644 --- a/compiler/src/Parser.cpp +++ b/compiler/src/Parser.cpp @@ -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: (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 (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* out) { std::vector 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* 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 lhs = std::make_unique(); if (!parse_and(lhs.get())) { @@ -481,7 +679,7 @@ namespace { while (check(Tok::OR)) { advance(); auto node = std::make_unique(); - node->kind = ExprKind::Or; + node->kind = ExprKind::Or; // 短路语义保留到 12.8 编跳转 node->lhs = std::move(lhs); node->rhs = std::make_unique(); 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 lhs = std::make_unique(); if (!parse_not(lhs.get())) { @@ -501,7 +704,7 @@ namespace { while (check(Tok::AND)) { advance(); auto node = std::make_unique(); - node->kind = ExprKind::And; + node->kind = ExprKind::And; // 短路语义保留到 12.8 编跳转 node->lhs = std::move(lhs); node->rhs = std::make_unique(); 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 lhs = std::make_unique(); 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 lhs = std::make_unique(); 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 lhs = std::make_unique(); 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 tokens_; - Unit* out_; - size_t pos_ = 0; - bool ok_ = true; + std::string* err_; // 错误输出(可空) + std::vector tokens_; // 整段 token 流(构造时一次性 lex 完成) + Unit* out_; // 解析结果 + size_t pos_ = 0; // 当前 token 下标 + bool ok_ = true; // 词法阶段是否成功 }; } // namespace +/** + * @brief 对源文本做语法分析(对外入口) + * @details 成功时 out 填满 globals/pous;失败时 out 内容不保证(以返回值为准) + * @param source_file 源文件名(报错用) + * @param content 源文本 + * @param out 输出 Unit(globals / pous,先清空) + * @param err 错误输出;可为 nullptr(静默) + * @return true 成功;false 失败(err 前缀 "syntax error") + */ bool parse_pous(const std::string& source_file, const std::string& content, Unit* out, std::string* err) { out->globals.clear(); @@ -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) {