实现 12.4 词法:25 关键字、小写折叠、TIME 字面量、lex error。

- Lexer.h/cpp:关键字表(大小写不敏感折小写)、(* *) 注释不嵌套、整数/T# 段式字面量、最长匹配符号
- token 带 source_file + 行列(起始位置,修了消费后行列的偏差)
- lexer_test:75 断言(line1 三文件 token 流 21/34/27、大小写、符号、注释、行列、5 类负例)
- Doc/compiler/词法.md:设计文档 + 执行计划
This commit is contained in:
2026-08-21 10:51:45 +08:00
parent 9a07f938f9
commit d4e2c1ae7c
6 changed files with 786 additions and 2 deletions
+127
View File
@@ -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#<int><单位>` 段式解析累加毫秒(`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` + 文档
+2
View File
@@ -8,6 +8,7 @@ doc/
初步计划.md 初步计划.md
isa/指令与映像.md isa/指令与映像.md
compiler/编译管线.md compiler/编译管线.md
compiler/词法.md
vm/扫描周期.md vm/扫描周期.md
executor/执行器入口.md executor/执行器入口.md
``` ```
@@ -17,6 +18,7 @@ doc/
| [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 | | [`初步计划.md`](初步计划.md) | 全工程定案:子集 ST、toml、执行模型、第 12 节阶段 |
| [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** | | [`isa/指令与映像.md`](isa/指令与映像.md) | 指令、映像、定宽类型、饱和;**规范以此为准** |
| [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 | | [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 |
| [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 |
| [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 |
| [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 |
+89
View File
@@ -0,0 +1,89 @@
/**
* @file Lexer.h
* @brief ST 子集词法分析
* @author
* @date 2026-08-21
*/
#pragma once
#include <cstdint>
#include <string>
#include <vector>
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<Token>* out, std::string* err);
// 读文件后 lex(文件不存在 / 读取失败也算 lex error
bool lex_file(const std::string& path, std::vector<Token>* out, std::string* err);
}
+347 -2
View File
@@ -1,8 +1,353 @@
/** /**
* @file Lexer.cpp * @file Lexer.cpp
* @brief 词法分析12.4 实现) * @brief ST 子集词法分析
* @date 2026-08-19 * @author
* @date 2026-08-21
*/ */
#include "compiler/Lexer.h"
#include <cctype>
#include <cstdio>
#include <fstream>
#include <string>
namespace compiler { 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<Token>* 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<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;
}
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<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;
}
// 整数
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 + "'");
}
}
// T#<int><unit>... 段式解析,累加毫秒
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_;
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<Token>* 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<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
+11
View File
@@ -23,3 +23,14 @@ target_compile_definitions(compiler_test PRIVATE
add_test(NAME compiler_toml add_test(NAME compiler_toml
COMMAND compiler_test) 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)
+210
View File
@@ -0,0 +1,210 @@
/**
* @file lexer_test.cpp
* @brief 词法测试:line1 token 流 + 负例
* @author
* @date 2026-08-21
*/
#include <cstdio>
#include <string>
#include <vector>
#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<Token> 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<Token> 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<Token> 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<Token> 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<Token> 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<compiler::Token> 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<Token> 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<size_t>(expect_count[i]));
CHECK(ts.back().type == Tok::END);
}
// globals.st 局部抽查
std::vector<Token> 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;
}