diff --git a/Doc/compiler/词法.md b/Doc/compiler/词法.md new file mode 100644 index 0000000..8a38b98 --- /dev/null +++ b/Doc/compiler/词法.md @@ -0,0 +1,127 @@ +# 词法(12.4) + +compiler 模块的词法分析。输入:单个 `.st` 源文件内容;输出:稳定 token 流(带 `source_file` + 行列)。语法与字段表见 [`初步计划.md`](../初步计划.md) 12.1、12.4。 + +## 做 / 不做 + +**做** + +- `(* *)` 注释、标识符、关键字、整数、`TIME` 字面量、`:=`、比较符、括号 +- 关键字与标识符统一折成**小写**内部形(与 IEC 一致,大小写不敏感) +- 每个 token 带 `source_file` + 行列,供以后报错 +- 非法字符 → `lex error` 硬错误(带行列) + +**不做(第一版)** + +- 字符串字面量、`REAL` 字面量、十六进制/科学计数 +- 注释嵌套(`(*` 到第一个 `*)` 结束,未闭合报错) +- 浮点数、`_` 数字分隔符 + +## 关键字表(12.1 冻结,只认这些) + +```text +PROGRAM FUNCTION FUNCTION_BLOCK END_PROGRAM END_FUNCTION END_FUNCTION_BLOCK +VAR VAR_INPUT VAR_OUTPUT VAR_GLOBAL VAR_EXTERNAL END_VAR +BOOL INT TIME +IF ELSIF ELSE END_IF WHILE END_WHILE +AND OR NOT +TRUE FALSE +TON TOF CTU +``` + +其余单词一律当标识符;`VAR_IN_OUT`、`REF`、`CLASS` 等由语法层(12.5)出明确错误。 + +## Token + +```cpp +enum class Tok { + // 关键字(25 个,逐一对应上表) + ... + // 字面量 + IDENT, // text = 小写名 + INT_LIT, // int_value + TIME_LIT, // int_value = 毫秒 + // 符号 + ASSIGN, // := + EQ, NE, LT, LE, GT, GE, // = <> < <= > >= + PLUS, MINUS, STAR, SLASH, // + - * / + LPAREN, RPAREN, COMMA, SEMI, // ( ) , ; + COLON, DOT, // : . + END, // 文件结束 +}; + +struct Token { + Tok type; + std::string text; // 标识符/关键字的小写文本;字面量原文 + int64_t int_value; // INT_LIT / TIME_LIT(毫秒) + std::string source_file; + uint32_t line; // 从 1 起 + uint32_t col; // 从 1 起 +}; +``` + +## 规则 + +### 大小写 + +- 关键字匹配**大小写不敏感**(`program` / `PROGRAM` / `Program` 同义) +- 标识符统一折小写存入 `text`(内部形);原始大小写不保留(v1 无字符串字面量,报错靠行列定位) + +### 字面量 + +- 整数:`[0-9]+`,`int64_t` 存储;超 `INT64_MAX` → `lex error` +- `TIME`:`T#`(不敏感)+ 一个或多个 `<整数><单位>` 段,单位不敏感:`ms s m h d`,累加为毫秒: + - `T#10ms` → 10 + - `T#1s250ms` → 1250 + - `T#2h` → 7200000 + - 缺单位 / 未知单位 / 空段 → `lex error` +- 负数不是字面量:`-5` 是 `MINUS` + 整数,由语法层处理 + +### 符号 + +- `:=` 优先于 `:`;`<>` 优先于 `<`/`>`;`<=`/`>=` 单独匹配 + +### 注释 + +- `(* ... *)`,**不嵌套**;未闭合 → `lex error` +- 注释内不产 token + +## 错误 + +稳定前缀 `lex error`,带文件与行列: + +```text +lex error: illegal character '@' (motor.st, line 3, col 7) +lex error: unterminated comment (main.st, line 1, col 1) +lex error: bad time literal 'T#3q' (main.st, line 4, col 10) +``` + +## 完成标准 + +1. 第 11 节三个文件(`examples/line1/*.st`)lex 成稳定 token 流;`lexer_test` 断言数量、类型、小写化、行列、`END` 收尾 +2. 非法字符 / 未闭合注释 / 坏 TIME 字面量 → `lex error` 负例过 +3. 全部构建 + `ctest` 无回归 + +--- + +## 执行计划(单步确认) + +1. **`Lexer.h`(新建 `compiler/include/compiler/Lexer.h`)** + - `Tok` 枚举:25 关键字 + `IDENT`/`INT_LIT`/`TIME_LIT` + 符号(`:= = <> < <= > >= + - * / ( ) , ; : .`)+ `END` + - `Token { type, text, int_value, source_file, line, col }`(行列从 1 起) + - `bool lex_file(path, out, err)` / `bool lex(source_file, content, out, err)` + +2. **`Lexer.cpp`(重写占位)** + - 25 关键字小写映射表,大小写不敏感,标识符统一折小写 + - `(* *)` 注释不嵌套、未闭合报错;非法字符报错 + - 整数 `[0-9]+`(超 `INT64_MAX` 报错);`T#<单位>` 段式解析累加毫秒(`ms s m h d`) + - `:=` / `<>` / `<=` / `>=` 最长匹配优先 + - 错误前缀 `lex error`,带 file/line/col + +3. **`lexer_test`(`tests/src/lexer_test.cpp` + CMake,链 `compiler` 库)** + - line1 三文件:token 数量 / 类型序列 / 小写化 / 行列 / `END` 收尾断言 + - 负例:`@` 非法字符、未闭合注释、`T#3q` 坏字面量、超大整数 → `lex error` 类别 + +4. **验证**:`cmake --build` + `ctest`(新增 `lexer_test` 后 5/5 全绿) + +5. **提交**:`Lexer.h/cpp` + `lexer_test` + 文档 diff --git a/Doc/索引.md b/Doc/索引.md index 1e278c4..3411680 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -8,6 +8,7 @@ doc/ 初步计划.md isa/指令与映像.md compiler/编译管线.md + compiler/词法.md vm/扫描周期.md executor/执行器入口.md ``` @@ -17,6 +18,7 @@ doc/ | [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | +| [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | diff --git a/compiler/include/compiler/Lexer.h b/compiler/include/compiler/Lexer.h new file mode 100644 index 0000000..153f5ac --- /dev/null +++ b/compiler/include/compiler/Lexer.h @@ -0,0 +1,89 @@ +/** + * @file Lexer.h + * @brief ST 子集词法分析 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include + +namespace compiler { + + // token 类型。关键字与 token 集见 Doc/compiler/词法.md。 + enum class Tok { + // 关键字(25 个,与 12.1 冻结表一致) + PROGRAM, + FUNCTION, + FUNCTION_BLOCK, + END_PROGRAM, + END_FUNCTION, + END_FUNCTION_BLOCK, + VAR, + VAR_INPUT, + VAR_OUTPUT, + VAR_GLOBAL, + VAR_EXTERNAL, + END_VAR, + BOOL, + INT, + TIME, + IF, + ELSIF, + ELSE, + END_IF, + WHILE, + END_WHILE, + AND, + OR, + NOT, + TRUE, + FALSE, + TON, + TOF, + CTU, + // 字面量 + IDENT, // text = 小写名 + INT_LIT, // int_value + TIME_LIT, // int_value = 毫秒 + // 符号 + ASSIGN, // := + EQ, // = + NE, // <> + LT, // < + LE, // <= + GT, // > + GE, // >= + PLUS, // + + MINUS, // - + STAR, // * + SLASH, // / + LPAREN, // ( + RPAREN, // ) + COMMA, // , + SEMI, // ; + COLON, // : + DOT, // . + END, // 文件结束 + }; + + struct Token { + Tok type; + std::string text; // 标识符/关键字的小写文本;字面量原文 + int64_t int_value; // INT_LIT / TIME_LIT(毫秒) + std::string source_file; + uint32_t line; // 从 1 起 + uint32_t col; // 从 1 起 + }; + + // 对源文本 lex 成 token 流(含末尾 END)。失败返回 false, + // err 前缀 "lex error",带文件与行列。 + bool lex(const std::string& source_file, const std::string& content, + std::vector* out, std::string* err); + + // 读文件后 lex(文件不存在 / 读取失败也算 lex error) + bool lex_file(const std::string& path, std::vector* out, std::string* err); +} diff --git a/compiler/src/Lexer.cpp b/compiler/src/Lexer.cpp index f73d4d0..c35616e 100644 --- a/compiler/src/Lexer.cpp +++ b/compiler/src/Lexer.cpp @@ -1,8 +1,353 @@ /** * @file Lexer.cpp - * @brief 词法分析(12.4 实现) - * @date 2026-08-19 + * @brief ST 子集词法分析 + * @author + * @date 2026-08-21 */ +#include "compiler/Lexer.h" + +#include +#include +#include +#include + namespace compiler { +namespace { + + // 25 个关键字:小写键 → Tok(大小写不敏感,见 Doc/compiler/词法.md) + const struct { + const char* key; + Tok tok; + } kKeywords[] = { + {"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}, + {"and", Tok::AND}, + {"or", Tok::OR}, + {"not", Tok::NOT}, + {"true", Tok::TRUE}, + {"false", Tok::FALSE}, + {"ton", Tok::TON}, + {"tof", Tok::TOF}, + {"ctu", Tok::CTU}, + }; + + // TIME 字面量单位 → 毫秒倍数(不敏感) + 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; + } + + class Lexer { + public: + 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) {} + + 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: + 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; + } + + char cur() const { return src_[pos_]; } + bool at_end() const { return pos_ >= src_.size(); } + + void advance() { + if (cur() == '\n') { + ++line_; + col_ = 1; + } else { + ++col_; + } + ++pos_; + } + + // 跳过空白与 (* *) 注释(不嵌套);未闭合返回 false + 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; + } + + 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); + } + + 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(); + } + + // 标识符 / 关键字(统一折小写) + 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; + } + + // 整数 + 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 + "'"); + } + } + + // T#... 段式解析,累加毫秒 + 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_; + std::string* err_; + size_t pos_; + uint32_t line_; + uint32_t col_; + uint32_t tok_line_ = 1; + uint32_t tok_col_ = 1; + }; + +} // namespace + +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(); } + +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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7765d03..abc0ccb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,3 +23,14 @@ target_compile_definitions(compiler_test PRIVATE add_test(NAME compiler_toml COMMAND compiler_test) + +# 词法测试:token 流 + 负例(REPO_ROOT 注入源目录绝对路径) +add_executable(lexer_test + ./src/lexer_test.cpp) + +target_link_libraries(lexer_test PRIVATE compiler) +target_compile_definitions(lexer_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME lexer_tokens + COMMAND lexer_test) diff --git a/tests/src/lexer_test.cpp b/tests/src/lexer_test.cpp new file mode 100644 index 0000000..2e0cca5 --- /dev/null +++ b/tests/src/lexer_test.cpp @@ -0,0 +1,210 @@ +/** + * @file lexer_test.cpp + * @brief 词法测试:line1 token 流 + 负例 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include + +#include "compiler/Lexer.h" + +#ifndef REPO_ROOT +#define REPO_ROOT "." +#endif + +static int g_checks = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + return false; \ + } \ + ++g_checks; \ + } while (0) + +// ---- 1. 基本 token 分类与大小写折叠 ---- + +static bool test_basic() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "PROGRAM MAIN\nVAR\n a, b : BOOL;\nEND_VAR\nEND_PROGRAM", &ts, &err)); + CHECK(ts.size() == 12); + CHECK(ts[0].type == Tok::PROGRAM); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "main"); + CHECK(ts[2].type == Tok::VAR); + CHECK(ts[3].type == Tok::IDENT && ts[3].text == "a"); + CHECK(ts[4].type == Tok::COMMA); + CHECK(ts[5].type == Tok::IDENT && ts[5].text == "b"); + CHECK(ts[6].type == Tok::COLON); + CHECK(ts[7].type == Tok::BOOL); + CHECK(ts[8].type == Tok::SEMI); + CHECK(ts[9].type == Tok::END_VAR); + CHECK(ts[10].type == Tok::END_PROGRAM); + CHECK(ts[11].type == Tok::END); + CHECK(ts[11].line == 5); + return true; +} + +// ---- 2. 大小写不敏感 ---- + +static bool test_case() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "PrOgRaM mAiN eNd_PrOgRaM", &ts, &err)); + CHECK(ts[0].type == Tok::PROGRAM); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "main"); + CHECK(ts[2].type == Tok::END_PROGRAM); + CHECK(lex("t.st", "T#10MS t#1s250Ms", &ts, &err)); + CHECK(ts[0].type == Tok::TIME_LIT && ts[0].int_value == 10); + CHECK(ts[1].type == Tok::TIME_LIT && ts[1].int_value == 1250); + return true; +} + +// ---- 3. 符号 ---- + +static bool test_symbols() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "a := b <> c <= d >= e < f > g + h - i * j / k ( ) , ; : .", + &ts, &err)); + CHECK(ts[0].type == Tok::IDENT); + CHECK(ts[1].type == Tok::ASSIGN); + CHECK(ts[3].type == Tok::NE); + CHECK(ts[5].type == Tok::LE); + CHECK(ts[7].type == Tok::GE); + CHECK(ts[9].type == Tok::LT); + CHECK(ts[11].type == Tok::GT); + CHECK(ts[13].type == Tok::PLUS); + CHECK(ts[15].type == Tok::MINUS); + CHECK(ts[17].type == Tok::STAR); + CHECK(ts[19].type == Tok::SLASH); + CHECK(ts[21].type == Tok::LPAREN); + CHECK(ts[22].type == Tok::RPAREN); + CHECK(ts[23].type == Tok::COMMA); + CHECK(ts[24].type == Tok::SEMI); + CHECK(ts[25].type == Tok::COLON); + CHECK(ts[26].type == Tok::DOT); + return true; +} + +// ---- 4. 注释 ---- + +static bool test_comments() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("t.st", "(* 整行注释 *)\nPROGRAM (* 行尾注释 *) MAIN", &ts, &err)); + CHECK(ts.size() == 3); + CHECK(ts[0].type == Tok::PROGRAM); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "main"); + CHECK(ts[2].type == Tok::END); + return true; +} + +// ---- 5. 行列号 ---- + +static bool test_pos() { + using namespace compiler; + std::vector ts; + std::string err; + CHECK(lex("f.st", "a\n b : INT;", &ts, &err)); + CHECK(ts[0].type == Tok::IDENT && ts[0].line == 1 && ts[0].col == 1); + CHECK(ts[1].type == Tok::IDENT && ts[1].line == 2 && ts[1].col == 3); + CHECK(ts[2].type == Tok::COLON && ts[2].line == 2 && ts[2].col == 5); + CHECK(ts[3].type == Tok::INT && ts[3].line == 2 && ts[3].col == 7); + CHECK(ts[0].source_file == "f.st"); + return true; +} + +// ---- 6. 负例 ---- + +static bool expect_lex_err(const std::string& src, const char* prefix) { + std::vector ts; + std::string err; + if (compiler::lex("t.st", src, &ts, &err)) { + std::printf("FAIL: lexed ok: %s\n", src.c_str()); + return false; + } + if (err.find(prefix) != 0) { + std::printf("FAIL: want '%s', got '%s'\n", prefix, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +static bool test_negative() { + if (!expect_lex_err("a @ b", "lex error")) return false; + if (!expect_lex_err("(* unclosed", "lex error")) return false; + if (!expect_lex_err("T#3q", "lex error")) return false; + if (!expect_lex_err("T#", "lex error")) return false; + if (!expect_lex_err("99999999999999999999", "lex error")) return false; + return true; +} + +// ---- 7. line1 三个文件 ---- + +static bool test_line1() { + using namespace compiler; + const char* files[] = {"globals.st", "motor.st", "main.st"}; + const int expect_count[] = {21, 34, 27}; + for (int i = 0; i < 3; ++i) { + std::vector ts; + std::string err; + const std::string path = + std::string(REPO_ROOT) + "/examples/line1/" + files[i]; + CHECK(lex_file(path, &ts, &err)); + CHECK(ts.size() == static_cast(expect_count[i])); + CHECK(ts.back().type == Tok::END); + } + + // globals.st 局部抽查 + std::vector ts; + std::string err; + CHECK(lex_file(std::string(REPO_ROOT) + "/examples/line1/globals.st", &ts, &err)); + CHECK(ts[0].type == Tok::VAR_GLOBAL); + CHECK(ts[1].type == Tok::IDENT && ts[1].text == "emergencystop"); + CHECK(ts[3].type == Tok::BOOL); + CHECK(ts[4].type == Tok::ASSIGN); + CHECK(ts[5].type == Tok::FALSE); + CHECK(ts[ts.size() - 2].type == Tok::END_VAR); + + // main.st 抽查:FB 调用与字段访问 + CHECK(lex_file(std::string(REPO_ROOT) + "/examples/line1/main.st", &ts, &err)); + bool saw_fb_call = false; + for (size_t k = 0; k + 3 < ts.size(); ++k) { + if (ts[k].type == Tok::IDENT && ts[k].text == "starter" && + ts[k + 1].type == Tok::LPAREN && ts[k + 2].type == Tok::IDENT) { + saw_fb_call = true; + } + } + CHECK(saw_fb_call); + bool saw_field = false; + for (size_t k = 0; k + 1 < ts.size(); ++k) { + if (ts[k].type == Tok::IDENT && ts[k].text == "starter" && + ts[k + 1].type == Tok::DOT) { + saw_field = true; + } + } + CHECK(saw_field); + return true; +} + +int main() { + if (!test_basic()) return 1; + if (!test_case()) return 1; + if (!test_symbols()) return 1; + if (!test_comments()) return 1; + if (!test_pos()) return 1; + if (!test_negative()) return 1; + if (!test_line1()) return 1; + std::printf("lexer_test: %d checks passed\n", g_checks); + return 0; +}