Files
Interpreter/compiler/src/Lexer.cpp
T

490 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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: <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"
#include <cctype>
#include <cstdio>
#include <fstream>
#include <string>
namespace compiler {
namespace {
/**
* @brief 关键字表:小写键 → Tok。
* @details 与 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 类型名(12.11 扩充为 8 个)
{"ton", Tok::TON},
{"tof", Tok::TOF},
{"tp", Tok::TP},
{"ctu", Tok::CTU},
{"ctd", Tok::CTD},
{"ctud", Tok::CTUD},
{"r_trig", Tok::R_TRIG},
{"f_trig", Tok::F_TRIG},
};
/**
* @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<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()) {
return false;
}
if (pos_ >= src_.size()) {
push(Tok::END, "", 0);
return true;
}
if (!lex_one()) {
return false;
}
}
}
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];
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<unsigned char>(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<unsigned char>(c)) || c == '_') {
const uint32_t start = pos_;
while (!at_end() &&
(std::isalnum(static_cast<unsigned char>(cur())) || cur() == '_')) {
advance();
}
std::string word = src_.substr(start, pos_ - start);
for (char& ch : word) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(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<unsigned char>(c))) {
const uint32_t start = pos_;
while (!at_end() && std::isdigit(static_cast<unsigned char>(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<uint64_t>(ch - '0');
if (v > (static_cast<uint64_t>(INT64_MAX) - d) / 10) {
return fail("integer literal too large");
}
v = v * 10 + d;
}
push(Tok::INT_LIT, digits, static_cast<int64_t>(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 → 10T#1s250ms → 1250T#2h → 7200000。
* @return true 成功(push TIME_LITint_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<unsigned char>(cur()))) {
uint64_t v = 0;
while (!at_end() && std::isdigit(static_cast<unsigned char>(cur()))) {
const uint64_t d = static_cast<uint64_t>(cur() - '0');
if (v > (static_cast<uint64_t>(INT64_MAX) - d) / 10) {
return fail("bad time literal");
}
v = v * 10 + d;
advance();
}
std::string unit;
while (!at_end() && std::isalpha(static_cast<unsigned char>(cur()))) {
unit += static_cast<char>(
std::tolower(static_cast<unsigned char>(cur())));
advance();
}
const int64_t mult = time_unit_ms(unit);
if (mult < 0) {
return fail("bad time literal");
}
total += static_cast<int64_t>(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<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();
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<Token>* 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<char>(in)),
std::istreambuf_iterator<char>());
return lex(path, content, out, err);
}
} // namespace compiler