feat: LexerEngine 增加后置整体校验

This commit is contained in:
Luke 2025-07-01 17:02:35 +08:00
parent b43245b1f5
commit e83244df61

View File

@ -3,6 +3,7 @@ package org.jcnc.snow.compiler.lexer.core;
import org.jcnc.snow.compiler.lexer.base.TokenScanner;
import org.jcnc.snow.compiler.lexer.scanners.*;
import org.jcnc.snow.compiler.lexer.token.Token;
import org.jcnc.snow.compiler.lexer.token.TokenType;
import org.jcnc.snow.compiler.lexer.utils.TokenPrinter;
import java.io.File;
@ -10,154 +11,144 @@ import java.util.ArrayList;
import java.util.List;
/**
* {@code LexerEngine} 是编译器前端的词法分析器核心实现
* <p>
* 负责将源代码字符串按顺序扫描并转换为一系列 {@link Token} 实例
* 每个 Token 表示语法上可识别的最小单位如标识符关键字常量运算符等
* <p>
* 分析流程通过注册多个 {@link TokenScanner} 扫描器实现类型识别
* 并由 {@link LexerContext} 提供字符流与位置信息支持
* 支持文件名传递遇到非法字符时会以文件名:::错误信息输出简洁诊断
* </p>
* Snow 语言词法分析器核心实现
* <p>采用<b>先扫描 后批量校验 统一报告</b>策略
* <ol>
* <li>{@link #scanAllTokens()} 用扫描器链把字符流拆成 {@link Token}</li>
* <li>{@link #validateTokens()} 基于 token 序列做轻量上下文校验</li>
* <li>{@link #report(List)} 一次性输出所有词法错误</li>
* </ol></p>
*/
public class LexerEngine {
/**
* 扫描生成的 Token 序列包含文件结束符 EOF
* 每个 Token 表示源代码中的一个词法单元
*/
private final List<Token> tokens = new ArrayList<>();
private final List<Token> tokens = new ArrayList<>(); // 扫描结果
private final List<LexicalError> errors = new ArrayList<>();
private final String absPath; // 绝对路径
private final LexerContext context; // 字符流
private final List<TokenScanner> scanners; // 扫描器链
/**
* 当前源文件的绝对路径用于错误信息定位
*/
private final String absPath;
/**
* 词法上下文负责字符流读取与位置信息维护
*/
private final LexerContext context;
/**
* Token 扫描器集合按优先级顺序排列
* 用于识别不同类别的 Token如空白注释数字标识符等
*/
private final List<TokenScanner> scanners;
/**
* 词法分析过程中收集到的全部词法错误
*/
private final List<LexicalError> errors = new ArrayList<>();
/**
* 构造词法分析器并指定源文件名用于诊断信息
* 构造时立即进行全量扫描扫描结束后打印所有 Token 并报告词法错误
*
* 创建并立即执行扫描-校验-报告流程
* @param source 源代码文本
* @param sourceName 文件名或来源描述"Main.snow"
* @param sourceName 文件名诊断用
*/
public LexerEngine(String source, String sourceName) {
this.absPath = new File(sourceName).getAbsolutePath();
this.context = new LexerContext(source);
this.absPath = new File(sourceName).getAbsolutePath();
this.context = new LexerContext(source);
this.scanners = List.of(
new WhitespaceTokenScanner(), // 跳过空格制表符等
new NewlineTokenScanner(), // 处理换行符生成 NEWLINE Token
new CommentTokenScanner(), // 处理单行/多行注释
new NumberTokenScanner(), // 识别整数与浮点数字面量
new IdentifierTokenScanner(), // 识别标识符和关键字
new StringTokenScanner(), // 处理字符串常量
new OperatorTokenScanner(), // 识别运算符
new SymbolTokenScanner(), // 识别括号分号等符号
new UnknownTokenScanner() // 捕捉无法识别的字符最后兜底
new WhitespaceTokenScanner(),
new NewlineTokenScanner(),
new CommentTokenScanner(),
new NumberTokenScanner(),
new IdentifierTokenScanner(),
new StringTokenScanner(),
new OperatorTokenScanner(),
new SymbolTokenScanner(),
new UnknownTokenScanner()
);
// 主扫描流程遇到非法字符立即输出错误并终止进程
try {
scanAllTokens();
} catch (LexicalException le) {
// 输出绝对路径: x, y: 错误信息
System.err.printf(
"%s: 行 %d, 列 %d: %s%n",
absPath,
le.getLine(),
le.getColumn(),
le.getReason()
);
System.exit(65); // 65 = EX_DATAERR
}
TokenPrinter.print(this.tokens);
LexerEngine.report(this.getErrors());
/* 1. 扫描 */
scanAllTokens();
/* 2. 后置整体校验 */
validateTokens();
/* 3. 打印 token */
TokenPrinter.print(tokens);
/* 4. 统一报告错误 */
report(errors);
if (!errors.isEmpty()) {
throw new LexicalException("Lexing failed with " + errors.size() + " error(s).", this.context.getLine(), this.context.getCol());
throw new LexicalException(
"Lexing failed with " + errors.size() + " error(s).",
context.getLine(), context.getCol()
);
}
}
/**
* 静态报告方法
* <p>
* 打印所有词法分析过程中收集到的错误信息
* 如果无错误输出词法分析通过的提示
*
* @param errors 词法错误列表
*/
public static void report(List<LexicalError> errors) {
if (errors != null && !errors.isEmpty()) {
System.err.println("\n词法分析发现 " + errors.size() + " 个错误:");
errors.forEach(err -> System.err.println(" " + err));
} else {
if (errors == null || errors.isEmpty()) {
System.out.println("\n## 词法分析通过,没有发现错误\n");
return;
}
System.err.println("\n词法分析发现 " + errors.size() + " 个错误:");
errors.forEach(e -> System.err.println(" " + e));
}
public List<Token> getAllTokens() { return List.copyOf(tokens); }
public List<LexicalError> getErrors() { return List.copyOf(errors); }
/**
* 主扫描循环将源代码转为 Token 序列
* <p>
* 依次尝试每个扫描器直到找到可处理当前字符的扫描器为止
* 扫描到结尾后补充 EOF Token
* 若遇到词法异常则收集错误并跳过当前字符避免死循环
* 逐字符扫描依次尝试各扫描器扫描器抛出的
* {@link LexicalException} 被捕获并转为 {@link LexicalError}
*/
private void scanAllTokens() {
while (!context.isAtEnd()) {
char currentChar = context.peek();
char ch = context.peek();
boolean handled = false;
for (TokenScanner scanner : scanners) {
if (scanner.canHandle(currentChar, context)) {
try {
scanner.handle(context, tokens);
} catch (LexicalException le) {
// 收集词法错误不直接退出
errors.add(new LexicalError(
absPath, le.getLine(), le.getColumn(), le.getReason()
));
// 跳过当前字符防止死循环
context.advance();
}
handled = true;
break;
for (TokenScanner s : scanners) {
if (!s.canHandle(ch, context)) continue;
try {
s.handle(context, tokens);
} catch (LexicalException le) {
errors.add(new LexicalError(
absPath, le.getLine(), le.getColumn(), le.getReason()
));
context.advance(); // 跳过问题字符
}
handled = true;
break;
}
if (!handled) {
// 没有任何扫描器能处理跳过一个字符防止死循环
context.advance();
}
if (!handled) context.advance(); // 理论不会走到保险
}
tokens.add(Token.eof(context.getLine()));
}
/**
* 获取全部 Token包含 EOF返回只读列表
*
* @return 词法分析结果 Token 列表
* 目前包含三条规则<br>
* 1. Dot-Prefix'.' 不能作标识符前缀<br>
* 2. Declare-Ident declare 后必须紧跟合法标识符并且只能一个<br>
* 3. Double-Ident declare 后若出现第二个 IDENTIFIER 视为多余<br>
* <p>发现问题仅写入 {@link #errors}不抛异常</p>
*/
public List<Token> getAllTokens() {
return List.copyOf(tokens);
private void validateTokens() {
for (int i = 0; i < tokens.size(); i++) {
Token tok = tokens.get(i);
/* ---------- declare 规则 ---------- */
if (tok.getType() == TokenType.KEYWORD
&& "declare".equalsIgnoreCase(tok.getLexeme())) {
// 第一个非 NEWLINE token
Token id1 = findNextNonNewline(i);
if (id1 == null || id1.getType() != TokenType.IDENTIFIER) {
errors.add(err(
(id1 == null ? tok : id1),
"declare 后必须跟合法标识符 (以字母或 '_' 开头)"
));
continue; // 若首标识符就错后续检查可略
}
// 检查是否有第二个 IDENTIFIER
Token id2 = findNextNonNewline(tokens.indexOf(id1));
if (id2 != null && id2.getType() == TokenType.IDENTIFIER) {
errors.add(err(id2, "declare 声明中出现多余的标识符"));
}
}
}
}
/**
* 返回全部词法错误返回只读列表
*
* @return 词法错误列表
*/
public List<LexicalError> getErrors() {
return List.copyOf(errors);
/** index 右侧最近非 NEWLINE token无则 null */
private Token findNextNonNewline(int index) {
for (int j = index + 1; j < tokens.size(); j++) {
Token t = tokens.get(j);
if (t.getType() != TokenType.NEWLINE) return t;
}
return null;
}
/** 构造统一的 LexicalError */
private LexicalError err(Token t, String msg) {
return new LexicalError(absPath, t.getLine(), t.getCol(), "非法的标记序列:" + msg);
}
}