/** * @file Lexer.cpp * @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" #include #include #include #include namespace compiler { namespace { // 关键字表:小写键 → 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}, // 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}, }; /** * @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; // 未知单位 } /** * @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()) { return false; } if (pos_ >= src_.size()) { push(Tok::END, "", 0); return true; } if (!lex_one()) { return false; } } } 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]; std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)", sf_.c_str(), line_, col_); *err_ = "lex error: " + msg + buf; } 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_; col_ = 1; } else { ++col_; } ++pos_; } /** * @brief 跳过空白与 (* *) 注释 * * @details 第一版注释不嵌套:遇到第一个 "*) 即结束 * (IEC 允许嵌套,留待以后)。未闭合 → lex error。 * @return true 正常;false 未闭合注释(err 已写) */ bool skip_space_and_comments() { while (!at_end()) { if (std::isspace(static_cast(cur()))) { advance(); } else if (cur() == '(' && pos_ + 1 < src_.size() && src_[pos_ + 1] == '*') { advance(); advance(); // 跳过 "(*" bool closed = false; while (!at_end()) { if (cur() == '*' && pos_ + 1 < src_.size() && src_[pos_ + 1] == ')') { advance(); advance(); closed = true; break; } advance(); } if (!closed) { return fail("unterminated comment"); } } else { break; } } 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; t.text = text; t.int_value = value; t.source_file = sf_; t.line = tok_line_; t.col = tok_col_; 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_; tok_col_ = col_; const char c = cur(); // TIME 字面量:T#(大小写不敏感)优先于标识符 if ((c == 't' || c == 'T') && pos_ + 1 < src_.size() && src_[pos_ + 1] == '#') { return lex_time(); } // 标识符 / 关键字:统一折小写存入 text(内部形), // 关键字命中即分类,否则 IDENT(用户定义名 / 内建类型名都走这里)。 if (std::isalpha(static_cast(c)) || c == '_') { const uint32_t start = pos_; while (!at_end() && (std::isalnum(static_cast(cur())) || cur() == '_')) { advance(); } std::string word = src_.substr(start, pos_ - start); for (char& ch : word) { ch = static_cast(std::tolower(static_cast(ch))); } for (const auto& kw : kKeywords) { if (word == kw.key) { push(kw.tok, kw.key, 0); return true; } } push(Tok::IDENT, word, 0); 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()))) { advance(); } const std::string digits = src_.substr(start, pos_ - start); uint64_t v = 0; for (const char ch : digits) { const uint64_t d = static_cast(ch - '0'); if (v > (static_cast(INT64_MAX) - d) / 10) { return fail("integer literal too large"); } v = v * 10 + d; } push(Tok::INT_LIT, digits, static_cast(v)); return true; } // 符号:两字符符(:= <> <= >=)优先于单字符; // 其余单字符各归一类,非法字符直接报错。 switch (c) { case ':': if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { advance(); advance(); push(Tok::ASSIGN, ":=", 0); } else { advance(); push(Tok::COLON, ":", 0); } return true; case '=': advance(); push(Tok::EQ, "=", 0); return true; case '<': if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { advance(); advance(); push(Tok::LE, "<=", 0); } else if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '>') { advance(); advance(); push(Tok::NE, "<>", 0); } else { advance(); push(Tok::LT, "<", 0); } return true; case '>': if (pos_ + 1 < src_.size() && src_[pos_ + 1] == '=') { advance(); advance(); push(Tok::GE, ">=", 0); } else { advance(); push(Tok::GT, ">", 0); } return true; case '+': advance(); push(Tok::PLUS, "+", 0); return true; case '-': advance(); push(Tok::MINUS, "-", 0); return true; case '*': advance(); push(Tok::STAR, "*", 0); return true; case '/': advance(); push(Tok::SLASH, "/", 0); return true; case '(': advance(); push(Tok::LPAREN, "(", 0); return true; case ')': advance(); push(Tok::RPAREN, ")", 0); return true; case ',': advance(); push(Tok::COMMA, ",", 0); return true; case ';': advance(); push(Tok::SEMI, ";", 0); return true; case '.': advance(); push(Tok::DOT, ".", 0); return true; default: return fail(std::string("illegal character '") + c + "'"); } } /** * @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 advance(); // # int64_t total = 0; bool any = false; while (!at_end() && std::isdigit(static_cast(cur()))) { uint64_t v = 0; while (!at_end() && std::isdigit(static_cast(cur()))) { const uint64_t d = static_cast(cur() - '0'); if (v > (static_cast(INT64_MAX) - d) / 10) { return fail("bad time literal"); } v = v * 10 + d; advance(); } std::string unit; while (!at_end() && std::isalpha(static_cast(cur()))) { unit += static_cast( std::tolower(static_cast(cur()))); advance(); } const int64_t mult = time_unit_ms(unit); if (mult < 0) { return fail("bad time literal"); } total += static_cast(v) * mult; any = true; } if (!any) { return fail("bad time literal"); } push(Tok::TIME_LIT, src_.substr(start, pos_ - start), total); return true; } // ---- 状态 ---- 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(); Lexer l(source_file, content, out, err); 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) { if (err) { *err = "lex error: cannot open file (" + path + ")"; } return false; } std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); return lex(path, content, out, err); } } // namespace compiler