refactor: 重构为基于有限状态机(FSM)的数字字面量解析器

This commit is contained in:
Luke 2025-06-30 17:52:37 +08:00
parent ce4106743a
commit f9a65531c1

View File

@ -6,111 +6,183 @@ import org.jcnc.snow.compiler.lexer.token.Token;
import org.jcnc.snow.compiler.lexer.token.TokenType;
/**
* 数字扫描器识别整数小数以及带有 <strong>类型后缀</strong> 的数字字面量<br>
* NumberTokenScanner 基于有限状态机FSM的数字字面量解析器
* <p>
* 支持格式示例
* <ul>
* <li>整数<code>123</code><code>0</code><code>45678</code></li>
* <li>小数<code>3.14</code><code>0.5</code><code>12.0</code></li>
* <li>带后缀<code>2.0f</code><code>42L</code><code>7s</code><code>255B</code></li>
* </ul>
* </p>
* 该扫描器负责将源码中的数字字符串切分为 NUMBER_LITERAL token当前支持
* <ol>
* <li>十进制整数 042123456</li>
* <li>十进制小数 3.140.5</li>
* <li>单字符类型后缀 2.0f255B合法集合见 SUFFIX_CHARS</li>
* </ol>
* <p>
* 单字符类型后缀
* 如果后续需要支持科学计数法下划线分隔符不同进制等只需扩展现有状态机的转移规则
* <pre>
* 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
* </pre>
* </p>
* <p>
* 规则约束<br>
* 若数字主体之后出现以下情况将在词法阶段抛出 {@link LexicalException}
* 状态说明
* <ul>
* <li>空白 + 字母 <code>3&nbsp;L</code></li>
* <li>未知字母紧邻 <code>3E</code></li>
* <li><code>'/'</code> 紧邻 <code>3/</code><code>3/*</code></li>
* <li>INT_PART 读取整数部分遇到 '.' 进入 DEC_POINT否则结束</li>
* <li>DEC_POINT 已读到小数点必须下一个字符是数字否则报错</li>
* <li>FRAC_PART 读取小数部分遇非法字符则结束主体</li>
* <li>END 主体扫描结束进入后缀/尾随字符判定</li>
* </ul>
* 以避免编译器陷入死循环
* </p>
* 错误处理策略
* <ol>
* <li>数字后跟未知字母 42X 抛出 LexicalException</li>
* <li>数字与合法后缀间有空白 3 L 抛出 LexicalException</li>
* <li>数字后直接出现 '/' 3/ 3/* 抛出 LexicalException避免死循环</li>
* <li>小数点后缺失数字 1. 抛出 LexicalException</li>
* </ol>
* 支持的单字符类型后缀包括b, s, l, f, d 及其大写形式若需支持多字符后缀可将该集合扩展为 Set<String>
*/
public class NumberTokenScanner extends AbstractTokenScanner {
/** 合法类型后缀字符集合(单字符,大小写均可) */
/**
* 支持的单字符类型后缀集合
* 包含b, s, l, f, d 及其大写形式
* 对于多字符后缀可扩展为 Set<String> 并在扫描尾部做贪婪匹配
*/
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);
}
/**
* 按照有限状态机读取完整数字字面量并对尾随字符进行合法性校验
* <p>
* 主体流程
* 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 */
// 2A. 合法单字符后缀
if (SUFFIX_CHARS.indexOf(next) >= 0) {
literal.append(ctx.advance());
}
/* 2-B: 若紧跟未知字母(如 42X抛出词法异常 */
// 2B. 紧跟未知字母 42X
else if (Character.isLetter(next)) {
throw new LexicalException(
"未知的数字类型后缀 '" + next + "'",
line, col
);
throw new LexicalException("未知的数字类型后缀 '" + next + "'", line, col);
}
/* 2-C: 若数字后有空白,且空白后紧跟字母(如 3 L也为非法 */
// 2C. 数字后出现空白 + 字母 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: 若紧跟 '/',抛出异常防止死循环 */
// 2D. 紧跟 '/' 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
}
}