diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerContext.java b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerContext.java index dbfe62f..4462fca 100644 --- a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerContext.java +++ b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexerContext.java @@ -3,60 +3,72 @@ package org.jcnc.snow.compiler.lexer.core; import org.jcnc.snow.compiler.lexer.base.TokenScanner; /** - * {@code LexerContext} 是词法分析阶段的上下文状态管理器。 + * {@code LexerContext} —— 词法分析阶段的上下文状态管理器。
*

- * 该类提供对源代码字符流的读取访问,追踪当前行号与列号, - * 并支持字符匹配、回看与指针推进等操作,是 {@link TokenScanner} 实现进行词法识别的重要支撑工具。 + * 提供对源代码字符流的读取访问、行列号追踪、指针推进与字符匹配等操作, + * 是 {@link TokenScanner} 实现进行词法识别的基础设施。 *

*

- * 所有源代码输入在构造时统一将 Windows 风格的换行符(\r\n)转换为 Unix 风格(\n), - * 保证换行行为一致性。 + * 设计要点: + *

*

*/ public class LexerContext { - /** 源代码字符串,换行符已标准化为 \n */ + /* ───────────────────────────────── 私有字段 ───────────────────────────────── */ + + /** 源代码字符串(换行符已标准化为 \n) */ private final String source; - /** 当前扫描位置(从 0 开始的偏移) */ + /** 当前扫描位置(自 0 起算的全局偏移量) */ private int pos = 0; - /** 当前行号,从 1 开始 */ + /** 当前行号(从 1 开始) */ private int line = 1; - /** 当前列号,从 1 开始 */ + /** 当前列号(从 1 开始) */ private int col = 1; - /** 上一个字符对应的列号(用于位置精确记录) */ + /** 上一个字符对应的列号(用于异常定位) */ private int lastCol = 1; + /* ──────────────────────────────── 构造 & 基本信息 ─────────────────────────────── */ + /** - * 构造一个新的 {@code LexerContext} 实例,并标准化换行符。 + * 创建新的 {@code LexerContext},并完成换行符标准化。 * - * @param source 原始源代码字符串 + * @param rawSource 原始源代码文本 */ - public LexerContext(String source) { - this.source = source.replace("\r\n", "\n"); + public LexerContext(String rawSource) { + this.source = rawSource.replace("\r\n", "\n"); } /** - * 判断是否已读取到源代码末尾。 + * 判断是否已到达源代码结尾。 * - * @return 若已结束,返回 {@code true};否则返回 {@code false} + * @return 若游标位于终点之后返回 {@code true} */ public boolean isAtEnd() { return pos >= source.length(); } + /* ──────────────────────────────── 指针推进与查看 ─────────────────────────────── */ + /** - * 消费当前字符并前进一个位置,自动更新行列信息。 + * 消费 当前 字符并前进一个位置,同时更新行列号。 * - * @return 当前字符,若已结束则返回空字符('\0') + * @return 被消费的字符;若已结束则返回空字符 {@code '\0'} */ public char advance() { if (isAtEnd()) return '\0'; + char c = source.charAt(pos++); lastCol = col; + if (c == '\n') { line++; col = 1; @@ -67,9 +79,9 @@ public class LexerContext { } /** - * 查看当前位置的字符,但不前进。 + * 查看当前位置字符,但不移动游标。 * - * @return 当前字符,若结束则返回空字符 + * @return 当前字符;若越界则返回 {@code '\0'} */ public char peek() { return isAtEnd() ? '\0' : source.charAt(pos); @@ -78,17 +90,29 @@ public class LexerContext { /** * 查看下一个字符,但不改变位置。 * - * @return 下一个字符,若结束则返回空字符 + * @return 下一字符;若越界则返回 {@code '\0'} */ public char peekNext() { return pos + 1 >= source.length() ? '\0' : source.charAt(pos + 1); } /** - * 若当前字符与期望字符相同,则前进并返回 {@code true},否则不动并返回 {@code false}。 + * 向前查看 offset 个字符(不移动游标)。offset=1 等价于 {@link #peekNext()}。 * - * @param expected 期待匹配的字符 - * @return 是否匹配成功并消费 + * @param offset 偏移量 (≥ 1) + * @return 指定偏移处的字符;若越界返回 {@code '\0'} + */ + public char peekAhead(int offset) { + if (offset <= 0) return peek(); + int idx = pos + offset; + return idx >= source.length() ? '\0' : source.charAt(idx); + } + + /** + * 若当前位置字符等于 {@code expected},则消费并返回 {@code true};否则保持原位返回 {@code false}。 + * + * @param expected 期望匹配的字符 + * @return 是否匹配并消费 */ public boolean match(char expected) { if (isAtEnd() || source.charAt(pos) != expected) return false; @@ -96,30 +120,17 @@ public class LexerContext { return true; } - /** - * 获取当前位置的行号。 - * - * @return 当前行号(从 1 开始) - */ - public int getLine() { - return line; - } + /* ──────────────────────────────── 坐标查询 ─────────────────────────────── */ - /** - * 获取当前位置的列号。 - * - * @return 当前列号(从 1 开始) - */ - public int getCol() { - return col; - } + /** @return 当前行号 (1-based) */ + public int getLine() { return line; } - /** - * 获取上一个字符所在的列号。 - * - * @return 上一个字符对应的列位置 - */ - public int getLastCol() { - return lastCol; - } + /** @return 当前列号 (1-based) */ + public int getCol() { return col; } + + /** @return 上一个字符的列号 */ + public int getLastCol() { return lastCol; } + + /** @return 当前指针在源文件中的全局偏移 (0-based) */ + public int getPos() { return pos; } } diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java index 517509f..ac598de 100644 --- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java @@ -1,92 +1,110 @@ 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; /** - * 数字扫描器:识别整数、小数以及带有类型后缀的数字字面量。 + * 数字扫描器:识别整数、小数以及带有 类型后缀 的数字字面量。
*

- * 支持的格式示例: + * 支持格式示例: *

+ *

*

- * 语法允许在数字 (整数或小数) 末尾添加以下单字符后缀来显式指定常量类型: - *

b | s | l | f | d   // 分别对应 byte、short、long、float、double
- * B | S | L | F | D   // 同上,大小写皆可
- * 生成的 Token 类型始终为 {@code NUMBER_LITERAL},词法单元将携带完整的文本(含后缀,若存在)。 + * 单字符类型后缀: + *
+ * b | s | l | f | d   // byte, short, long, float, double
+ * B | S | L | F | D   // 同上(大小写均可)
+ * 
+ *

+ *

+ * 规则约束:
+ * 若数字主体之后出现以下情况,将在词法阶段抛出 {@link LexicalException}: + *

+ * 以避免编译器陷入死循环。 + *

*/ public class NumberTokenScanner extends AbstractTokenScanner { - /** - * 可选类型后缀字符集合 (大小写均可)。 - * 与 {@code ExpressionBuilder} 内的后缀解析逻辑保持一致。 - */ + /** 合法类型后缀字符集合 */ private static final String SUFFIX_CHARS = "bslfdBSLFD"; - /** - * 判断是否可以处理当前位置的字符。 - *

当字符为数字时,表示可能是数字字面量的起始。

- * - * @param c 当前字符 - * @param ctx 当前词法上下文 - * @return 如果为数字字符,则返回 true - */ @Override public boolean canHandle(char c, LexerContext ctx) { return Character.isDigit(c); } - /** - * 执行数字扫描逻辑。 - *
    - *
  1. 连续读取数字字符,允许出现一个小数点,用于识别整数或小数。
  2. - *
  3. 读取完主体后,一次性检查下一个字符,若属于合法类型后缀则吸收。
  4. - *
- * 这样可以保证诸如 {@code 2.0f} 被视为一个整体的 {@code NUMBER_LITERAL}, - * 而不是拆分成 "2.0" 与 "f" 两个 Token。 - * - * @param ctx 词法上下文 - * @param line 当前行号 - * @param col 当前列号 - * @return 表示数字字面量的 Token - */ @Override protected Token scanToken(LexerContext ctx, int line, int col) { - StringBuilder sb = new StringBuilder(); - boolean hasDot = false; // 标识是否已经遇到过小数点 + StringBuilder literal = new StringBuilder(); + boolean hasDot = false; // 是否已遇到小数点 - /* - * 1️⃣ 扫描整数或小数主体 - * 允许出现一个小数点,其余必须是数字。 - */ + /* 1. 读取数字主体(整数 / 小数) */ while (!ctx.isAtEnd()) { char c = ctx.peek(); if (c == '.' && !hasDot) { hasDot = true; - sb.append(ctx.advance()); + literal.append(ctx.advance()); } else if (Character.isDigit(c)) { - sb.append(ctx.advance()); + literal.append(ctx.advance()); } else { - break; // 遇到非数字/第二个点 => 主体结束 + break; } } - /* - * 2️⃣ 可选类型后缀 - * 如果下一字符是合法后缀字母,则一起纳入当前 Token。 - */ + /* 2. 处理后缀或非法跟随字符 */ if (!ctx.isAtEnd()) { - char suffix = ctx.peek(); - if (SUFFIX_CHARS.indexOf(suffix) >= 0) { - sb.append(ctx.advance()); + char next = ctx.peek(); + + /* 2-A: 合法类型后缀,直接吸收 */ + if (SUFFIX_CHARS.indexOf(next) >= 0) { + literal.append(ctx.advance()); } + /* 2-B: 未知字母紧邻 → 抛异常 */ + else if (Character.isLetter(next)) { + throw new LexicalException( + "Unknown numeric suffix '" + next + "'", + line, col + ); + } + /* 2-C: 数字后空白(非换行)→ 若空白后跟字母,抛异常 */ + else if (Character.isWhitespace(next) && next != '\n') { + int off = 1; + char look; + do { + look = ctx.peekAhead(off); + if (look == '\n' || look == '\0') break; + if (!Character.isWhitespace(look)) break; + off++; + } while (true); + + if (Character.isLetter(look)) { + throw new LexicalException( + "Whitespace between numeric literal and an alphabetic character is not allowed", + line, col + ); + } + } + /* 2-D: 紧邻字符为 '/' → 抛异常以避免死循环 */ + else if (next == '/') { + throw new LexicalException( + "Unexpected '/' after numeric literal", + line, col + ); + } + /* 其余字符(运算符、分隔符等)留给后续扫描器处理 */ } - // 构造并返回 NUMBER_LITERAL Token,文本内容形如 "123", "3.14f" 等。 - return new Token(TokenType.NUMBER_LITERAL, sb.toString(), line, col); + /* 3. 返回 NUMBER_LITERAL Token */ + return new Token(TokenType.NUMBER_LITERAL, literal.toString(), line, col); } }