From f9a65531c19b899d328d3b18fd995a4ccdd865d1 Mon Sep 17 00:00:00 2001
From: Luke
- * 支持格式示例:
- *
+ * NumberTokenScanner —— 基于有限状态机(FSM)的数字字面量解析器。
*
- *
- * 123、0、456783.14、0.5、12.02.0f、42L、7s、255B
- * 单字符类型后缀: + * 如果后续需要支持科学计数法、下划线分隔符、不同进制等,只需扩展现有状态机的转移规则。 *
- * b | s | l | f | d // byte, short, long, float, double - * B | S | L | F | D // 同上(大小写均可) + * 状态机简述: + * INT_PART --'.'--> DEC_POINT + * | | + * | v + * else------------> END + * | + * v + * DEC_POINT --digit--> FRAC_PART + * | + * v + * else--> END *- * - *
- * 规则约束:
- * 若数字主体之后出现以下情况,将在词法阶段抛出 {@link LexicalException}:
+ * 状态说明:
*
3 L)3E)'/' 紧邻(如 3/、3/*)+ * 主体流程: + * 1. 整数部分、可选小数点和小数部分扫描。 + * 2. 检查合法的类型后缀。 + * 3. 检查非法尾随字符,如未知字母、空白后缀或非法 '/'。 + * 4. 生成并返回 NUMBER_LITERAL Token。 + * + * @param ctx 当前 LexerContext(提供游标、前瞻等功能) + * @param line 源码起始行号(1 基) + * @param col 源码起始列号(1 基) + * @return NUMBER_LITERAL 类型的 Token + * @throws LexicalException 如果遇到非法格式或未受支持的尾随字符 + */ @Override protected Token scanToken(LexerContext ctx, int line, int col) { StringBuilder literal = new StringBuilder(); - boolean hasDot = false; // 标记是否已出现过小数点 + State state = State.INT_PART; - /* 1. 读取数字主体部分(包括整数、小数) */ - while (!ctx.isAtEnd()) { - char c = ctx.peek(); - if (c == '.' && !hasDot) { - // 遇到第一个小数点 - hasDot = true; - literal.append(ctx.advance()); - } else if (Character.isDigit(c)) { - // 吸收数字字符 - literal.append(ctx.advance()); - } else { - // 非数字/非小数点,终止主体读取 - break; + // 1. 主体扫描 —— 整数 / 小数 + mainLoop: + while (!ctx.isAtEnd() && state != State.END) { + char ch = ctx.peek(); + switch (state) { + case INT_PART: + if (Character.isDigit(ch)) { + literal.append(ctx.advance()); + } else if (ch == '.') { + state = State.DEC_POINT; + literal.append(ctx.advance()); + } else { + state = State.END; // 整数已结束 + } + break; + + case DEC_POINT: + if (Character.isDigit(ch)) { + state = State.FRAC_PART; + literal.append(ctx.advance()); + } else { + // 如 "1." —— 语言规范不允许尾点数字 + throw new LexicalException("小数点后必须跟数字", line, col); + } + break; + + case FRAC_PART: + if (Character.isDigit(ch)) { + literal.append(ctx.advance()); + } else { + state = State.END; // 小数字符串结束 + } + break; + + default: + break mainLoop; // 理论不会到达 } } - /* 2. 检查数字字面量后的字符,决定是否继续吸收或抛出异常 */ + // 2. 后缀及非法尾随字符检查 if (!ctx.isAtEnd()) { char next = ctx.peek(); - /* 2-A: 合法类型后缀,直接吸收(如 42L、3.0F) */ + // 2‑A. 合法单字符后缀 if (SUFFIX_CHARS.indexOf(next) >= 0) { literal.append(ctx.advance()); } - /* 2-B: 若紧跟未知字母(如 42X),抛出词法异常 */ + // 2‑B. 紧跟未知字母(如 42X) else if (Character.isLetter(next)) { - throw new LexicalException( - "未知的数字类型后缀 '" + next + "'", - line, col - ); + throw new LexicalException("未知的数字类型后缀 '" + next + "'", line, col); } - /* 2-C: 若数字后有空白,且空白后紧跟字母(如 3 L),也为非法 */ + // 2‑C. 数字后出现空白 + 字母(如 3 L) else if (Character.isWhitespace(next) && next != '\n') { int off = 1; char look; - // 跳过所有空白字符,找到第一个非空白字符 - do { + while (true) { look = ctx.peekAhead(off); - if (look == '\n' || look == '\0') break; + if (look == '\n' || look == '\0') break; // 行尾或 EOF if (!Character.isWhitespace(look)) break; off++; - } while (true); - + } if (Character.isLetter(look)) { - // 抛出:数字字面量与位宽符号之间不允许有空白符 - throw new LexicalException( - "数字字面量与位宽符号之间不允许有空白符", - line, col - ); + throw new LexicalException("数字字面量与类型后缀之间不允许有空白符", line, col); } } - /* 2-D: 若紧跟 '/',抛出异常防止死循环 */ + // 2‑D. 紧跟 '/'(如 3/ 或 3/*) else if (next == '/') { - throw new LexicalException( - "数字字面量后不允许直接出现 '/'", - line, col - ); + throw new LexicalException("数字字面量后不允许直接出现 '/'", line, col); } - // 其余情况(如分号、括号、运算符),交由其他扫描器处理 + // 其他字符(分号、运算符、括号等)留给外层扫描流程处理 } - /* 3. 返回 NUMBER_LITERAL Token */ + // 3. 生成并返回 Token return new Token(TokenType.NUMBER_LITERAL, literal.toString(), line, col); } + + /** + * FSM 内部状态。 + * 每次读取一个字符后,根据“当前状态 + 当前字符”决定转移。 + */ + private enum State { + /** + * 整数部分(尚未读到小数点) + */ + INT_PART, + /** + * 已读到小数点,但还未读到第一位小数数字 + */ + DEC_POINT, + /** + * 小数部分(小数点右侧) + */ + FRAC_PART, + /** + * 主体结束,准备处理后缀或交还控制权 + */ + END + } }