三个 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) {
|
||||
|
||||
Reference in New Issue
Block a user