From e023b576c10899642c7de6969e9864bf17e96e2f Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 16 May 2025 13:12:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:token=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jcnc/snow/compiler/cli/SnowCompiler.java | 14 ++-- .../snow/compiler/lexer/core/LexerEngine.java | 80 ++++++++++++------- .../compiler/lexer/core/LexicalException.java | 54 +++++++++++++ .../lexer/scanners/UnknownTokenScanner.java | 54 +++++++------ test | 2 +- 5 files changed, 146 insertions(+), 58 deletions(-) create mode 100644 src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java diff --git a/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java b/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java index 875933f..397d6d1 100644 --- a/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java +++ b/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java @@ -1,15 +1,19 @@ package org.jcnc.snow.compiler.cli; -import org.jcnc.snow.compiler.backend.*; +import org.jcnc.snow.compiler.backend.RegisterAllocator; +import org.jcnc.snow.compiler.backend.VMCodeGenerator; +import org.jcnc.snow.compiler.backend.VMProgramBuilder; import org.jcnc.snow.compiler.ir.builder.IRProgramBuilder; -import org.jcnc.snow.compiler.ir.core.*; +import org.jcnc.snow.compiler.ir.core.IRFunction; +import org.jcnc.snow.compiler.ir.core.IRProgram; import org.jcnc.snow.compiler.lexer.core.LexerEngine; import org.jcnc.snow.compiler.lexer.token.Token; import org.jcnc.snow.compiler.parser.ast.base.Node; import org.jcnc.snow.compiler.parser.context.ParserContext; import org.jcnc.snow.compiler.parser.core.ParserEngine; import org.jcnc.snow.compiler.semantic.core.SemanticAnalyzerRunner; -import org.jcnc.snow.vm.engine.*; +import org.jcnc.snow.vm.engine.VMMode; +import org.jcnc.snow.vm.engine.VirtualMachineEngine; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -36,12 +40,12 @@ public class SnowCompiler { String source = Files.readString(srcPath, StandardCharsets.UTF_8); /* 1. 词法分析 */ - LexerEngine lexer = new LexerEngine(source); + LexerEngine lexer = new LexerEngine(source, srcPath.toString()); List tokens = lexer.getAllTokens(); /* 2. 语法分析 */ ParserContext ctx = new ParserContext(tokens); - List ast = new ParserEngine(ctx).parse(); + List ast = new ParserEngine(ctx).parse(); /* 3. 语义分析 */ SemanticAnalyzerRunner.runSemanticAnalysis(ast, false); diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerEngine.java b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerEngine.java index b322c73..819fa07 100644 --- a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerEngine.java +++ b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerEngine.java @@ -10,78 +10,102 @@ import java.util.List; /** * {@code LexerEngine} 是编译器前端的词法分析器核心实现。 *

- * 它负责将源代码字符串按顺序扫描并转换为一系列 {@link Token} 实例, + * 负责将源代码字符串按顺序扫描并转换为一系列 {@link Token} 实例, * 每个 Token 表示语法上可识别的最小单位(如标识符、关键字、常量、运算符等)。 *

* 分析流程通过注册多个 {@link TokenScanner} 扫描器实现类型识别, * 并由 {@link LexerContext} 提供字符流与位置信息支持。 + * 支持文件名传递,遇到非法字符时会以“文件名:行:列:错误信息”输出简洁诊断。 *

*/ public class LexerEngine { - - /** 扫描生成的 Token 序列(含 EOF) */ + /** + * 扫描生成的 Token 序列(包含文件结束符 EOF) + */ private final List tokens = new ArrayList<>(); - /** 词法上下文,提供字符流读取与位置信息 */ + /** + * 词法上下文,提供字符流读取与位置信息 + */ private final LexerContext context; - /** Token 扫描器集合,按优先级顺序组织,用于识别不同类别的 Token */ + /** + * Token 扫描器集合,按优先级顺序组织,用于识别不同类别的 Token + */ private final List scanners; /** - * 构造一个 {@code LexerEngine} 实例,并初始化内部扫描器与上下文。 - * 调用构造函数时即开始词法扫描,生成完整 Token 序列。 + * 构造词法分析器(假定输入源自标准输入,文件名默认为 ) * - * @param source 原始源代码文本 + * @param source 源代码文本 */ public LexerEngine(String source) { - this.context = new LexerContext(source); + this(source, ""); + } - // 按优先级注册所有支持的 Token 扫描器 + /** + * 构造词法分析器,并指定源文件名(用于诊断信息)。 + * 构造时立即进行全量扫描。 + * + * @param source 源代码文本 + * @param sourceName 文件名或来源描述(如"main.snow") + */ + public LexerEngine(String source, String sourceName) { + this.context = new LexerContext(source); this.scanners = List.of( new WhitespaceTokenScanner(), // 跳过空格、制表符等 new NewlineTokenScanner(), // 处理换行符,生成 NEWLINE Token new CommentTokenScanner(), // 处理单行/多行注释 - new NumberTokenScanner(), // 识别整数与浮点数 - new IdentifierTokenScanner(), // 识别标识符与关键字 + new NumberTokenScanner(), // 识别整数与浮点数字面量 + new IdentifierTokenScanner(), // 识别标识符和关键字 new StringTokenScanner(), // 处理字符串常量 - new OperatorTokenScanner(), // 处理运算符 - new SymbolTokenScanner(), // 处理括号、分号等符号 - new UnknownTokenScanner() // 捕捉无法识别的字符 + new OperatorTokenScanner(), // 识别运算符 + new SymbolTokenScanner(), // 识别括号、分号等符号 + new UnknownTokenScanner() // 捕捉无法识别的字符,最后兜底 ); - scanAllTokens(); + // 主扫描流程,遇到非法字符立即输出错误并终止进程 + try { + scanAllTokens(); + } catch (LexicalException le) { + // 输出:文件名:行:列: 错误信息,简洁明了 + System.err.printf( + "%s:%d:%d: %s%n", + sourceName, + le.getLine(), // 获取出错行号 + le.getColumn(), // 获取出错列号 + le.getMessage() // 错误描述 + ); + System.exit(65); // 65 = EX_DATAERR,标准数据错误退出码 + } } /** - * 执行主扫描流程,将整个源代码转换为 Token 序列。 - *

- * 每次扫描尝试依次使用各个 {@link TokenScanner},直到某一扫描器能够处理当前字符。 - * 若无匹配扫描器,交由 {@code UnknownTokenScanner} 处理。 - * 扫描结束后自动附加一个 EOF(文件结束)Token。 - *

+ * 主扫描循环,将源代码转为 Token 序列 + * 依次尝试每个扫描器,直到找到可处理当前字符的扫描器为止 + * 扫描到结尾后补充 EOF Token */ private void scanAllTokens() { while (!context.isAtEnd()) { char currentChar = context.peek(); - + // 依次查找能处理当前字符的扫描器 for (TokenScanner scanner : scanners) { if (scanner.canHandle(currentChar, context)) { scanner.handle(context, tokens); - break; + break; // 已处理,跳到下一个字符 } } } - + // 末尾补一个 EOF 标记 tokens.add(Token.eof(context.getLine())); } /** - * 返回词法分析生成的所有 Token(含 EOF)。 + * 获取全部 Token(包含 EOF),返回只读列表 * - * @return Token 的不可变副本列表 + * @return 词法分析结果 Token 列表 */ public List getAllTokens() { return List.copyOf(tokens); } -} \ No newline at end of file +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java new file mode 100644 index 0000000..b509b03 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java @@ -0,0 +1,54 @@ +package org.jcnc.snow.compiler.lexer.core; + +/** + * 词法异常(LexicalException)。 + *

+ * 当 {@link org.jcnc.snow.compiler.lexer.core.LexerEngine} 在扫描过程中遇到 + * 非法或无法识别的字符序列时抛出该异常。 + *

    + *
  • 异常消息仅包含一行简明错误信息(包含行号与列号);
  • + *
  • 完全禁止 Java 堆栈信息输出,使命令行输出保持整洁。
  • + *
+ *
+ * 例:
+ *     main.s:2:19: Lexical error: Illegal character sequence '@' at 2:19
+ * 
+ */ +public class LexicalException extends RuntimeException { + /** 错误发生的行号(从1开始) */ + private final int line; + /** 错误发生的列号(从1开始) */ + private final int column; + + /** + * 构造词法异常 + * @param reason 错误原因(如:非法字符描述) + * @param line 出错行号 + * @param column 出错列号 + */ + public LexicalException(String reason, int line, int column) { + // 构造出错消息,并禁止异常堆栈打印 + super(String.format("Lexical error: %s at %d:%d", reason, line, column), + null, false, false); + this.line = line; + this.column = column; + } + + /** + * 屏蔽异常堆栈填充(始终不打印堆栈信息) + */ + @Override + public synchronized Throwable fillInStackTrace() { return this; } + + /** + * 获取出错的行号 + * @return 行号 + */ + public int getLine() { return line; } + + /** + * 获取出错的列号 + * @return 列号 + */ + public int getColumn() { return column; } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java index 31c4e8a..8f8a940 100644 --- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java @@ -1,25 +1,28 @@ package org.jcnc.snow.compiler.lexer.scanners; import org.jcnc.snow.compiler.lexer.core.LexerContext; +import org.jcnc.snow.compiler.lexer.core.LexicalException; import org.jcnc.snow.compiler.lexer.token.Token; -import org.jcnc.snow.compiler.lexer.token.TokenType; /** - * 未知符号扫描器:兜底处理无法被其他扫描器识别的字符。 + * 未知 Token 扫描器(UnknownTokenScanner)。 *

- * 用于捕捉非法或未定义的符号序列,生成 {@code UNKNOWN} 类型的 Token。 + * 作为所有扫描器的兜底处理器。当前字符若不被任何其他扫描器识别, + * 由本类处理并抛出 {@link LexicalException},终止词法分析流程。 + *

*

- * 它会连续读取一段既不是字母、数字、空白符,也不属于常规符号(如 ;、{、"、:、,、(、)、.、+、-、*)的字符序列。 + * 主要作用:保证所有非法、不可识别的字符(如@、$等)不会被静默跳过或误当作合法 Token, + * 而是在词法阶段立刻定位并报错,有助于尽早发现源代码问题。 + *

*/ public class UnknownTokenScanner extends AbstractTokenScanner { /** - * 始终返回 true,作为所有扫描器中的兜底处理器。 - *

当没有其他扫描器能够处理当前字符时,使用本扫描器。

- * - * @param c 当前字符 - * @param ctx 当前词法上下文 - * @return 总是返回 true + * 判断是否可以处理当前字符。 + * 对于 UnknownTokenScanner,始终返回 true(兜底扫描器,必须排在扫描器链末尾)。 + * @param c 当前待处理字符 + * @param ctx 词法上下文 + * @return 是否处理该字符(始终为 true) */ @Override public boolean canHandle(char c, LexerContext ctx) { @@ -27,23 +30,26 @@ public class UnknownTokenScanner extends AbstractTokenScanner { } /** - * 扫描未知或非法的字符序列。 - *

跳过字母、数字、空白和已知符号,仅捕获无法识别的符号块。

- * - * @param ctx 词法上下文 - * @param line 当前行号 - * @param col 当前列号 - * @return {@code UNKNOWN} 类型的 Token,包含无法识别的字符内容 + * 实际处理非法字符序列的方法。 + * 连续读取所有无法被其他扫描器识别的字符,组成错误片段并抛出异常。 + * @param ctx 词法上下文 + * @param line 错误发生行号 + * @param col 错误发生列号 + * @return 不会返回Token(始终抛异常) + * @throws LexicalException 非法字符导致的词法错误 */ @Override protected Token scanToken(LexerContext ctx, int line, int col) { - String lexeme = readWhile(ctx, c -> - !Character.isLetterOrDigit(c) && - !Character.isWhitespace(c) && - c != ';' && c != '{' && c != '"' && - ":,().+-*".indexOf(c) < 0 + // 读取一段非法字符(既不是字母数字、也不是常见符号) + String lexeme = readWhile(ctx, ch -> + !Character.isLetterOrDigit(ch) && + !Character.isWhitespace(ch) && + ":,().+-*{};\"".indexOf(ch) < 0 ); - - return new Token(TokenType.UNKNOWN, lexeme, line, col); + // 如果没读到任何字符,则把当前字符单独作为非法片段 + if (lexeme.isEmpty()) + lexeme = String.valueOf(ctx.advance()); + // 抛出词法异常,并带上错误片段与具体位置 + throw new LexicalException("Illegal character sequence '" + lexeme + "'", line, col); } } diff --git a/test b/test index 1f3471f..92297d4 100644 --- a/test +++ b/test @@ -1,5 +1,5 @@ module: CommonTasks - function: main + function: main@ parameter: return_type:int