From f9a65531c19b899d328d3b18fd995a4ccdd865d1 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 30 Jun 2025 17:52:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=B8=BA?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=E6=9C=89=E9=99=90=E7=8A=B6=E6=80=81=E6=9C=BA?= =?UTF-8?q?=EF=BC=88FSM=EF=BC=89=E7=9A=84=E6=95=B0=E5=AD=97=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E8=A7=A3=E6=9E=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lexer/scanners/NumberTokenScanner.java | 194 ++++++++++++------ 1 file changed, 133 insertions(+), 61 deletions(-) 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 084c8f7..5e1bd7d 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 @@ -6,111 +6,183 @@ import org.jcnc.snow.compiler.lexer.token.Token; import org.jcnc.snow.compiler.lexer.token.TokenType; /** - * 数字扫描器:识别整数、小数以及带有 类型后缀 的数字字面量。
+ * NumberTokenScanner —— 基于有限状态机(FSM)的数字字面量解析器。 *

- * 支持格式示例: - *

- *

+ * 该扫描器负责将源码中的数字字符串切分为 NUMBER_LITERAL token,当前支持: + *
    + *
  1. 十进制整数(如 0,42,123456)
  2. + *
  3. 十进制小数(如 3.14,0.5)
  4. + *
  5. 单字符类型后缀(如 2.0f,255B,合法集合见 SUFFIX_CHARS)
  6. + *
*

- * 单字符类型后缀: + * 如果后续需要支持科学计数法、下划线分隔符、不同进制等,只需扩展现有状态机的转移规则。 *

- * 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}: + * 状态说明: *

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

+ * 错误处理策略: + *
    + *
  1. 数字后跟未知字母(如 42X)—— 抛出 LexicalException
  2. + *
  3. 数字与合法后缀间有空白(如 3 L)—— 抛出 LexicalException
  4. + *
  5. 数字后直接出现 '/'(如 3/ 或 3/*)—— 抛出 LexicalException,避免死循环
  6. + *
  7. 小数点后缺失数字(如 1.)—— 抛出 LexicalException
  8. + *
+ * 支持的单字符类型后缀包括:b, s, l, f, d 及其大写形式。若需支持多字符后缀,可将该集合扩展为 Set。 */ public class NumberTokenScanner extends AbstractTokenScanner { - /** 合法类型后缀字符集合(单字符,大小写均可) */ + /** + * 支持的单字符类型后缀集合。 + * 包含:b, s, l, f, d 及其大写形式。 + * 对于多字符后缀,可扩展为 Set 并在扫描尾部做贪婪匹配。 + */ private static final String SUFFIX_CHARS = "bslfdBSLFD"; + /** + * 判断是否由该扫描器处理。 + * 仅当首字符为数字时,NumberTokenScanner 介入处理。 + * + * @param c 当前待判断字符 + * @param ctx 当前 LexerContext(可用于进一步判断) + * @return 如果为数字返回 true,否则返回 false + */ @Override public boolean canHandle(char c, LexerContext ctx) { - // 仅当遇到数字时,本扫描器才处理 return Character.isDigit(c); } + /** + * 按照有限状态机读取完整数字字面量,并对尾随字符进行合法性校验。 + *

+ * 主体流程: + * 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 + } }