refactor: 重构为基于有限状态机(FSM)的数字字面量解析器
This commit is contained in:
parent
ce4106743a
commit
f9a65531c1
@ -6,111 +6,183 @@ import org.jcnc.snow.compiler.lexer.token.Token;
|
|||||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数字扫描器:识别整数、小数以及带有 <strong>类型后缀</strong> 的数字字面量。<br>
|
* NumberTokenScanner —— 基于有限状态机(FSM)的数字字面量解析器。
|
||||||
* <p>
|
* <p>
|
||||||
* 支持格式示例:
|
* 该扫描器负责将源码中的数字字符串切分为 NUMBER_LITERAL token,当前支持:
|
||||||
* <ul>
|
* <ol>
|
||||||
* <li>整数:<code>123</code>、<code>0</code>、<code>45678</code></li>
|
* <li>十进制整数(如 0,42,123456)</li>
|
||||||
* <li>小数:<code>3.14</code>、<code>0.5</code>、<code>12.0</code></li>
|
* <li>十进制小数(如 3.14,0.5)</li>
|
||||||
* <li>带后缀:<code>2.0f</code>、<code>42L</code>、<code>7s</code>、<code>255B</code></li>
|
* <li>单字符类型后缀(如 2.0f,255B,合法集合见 SUFFIX_CHARS)</li>
|
||||||
* </ul>
|
* </ol>
|
||||||
* </p>
|
|
||||||
* <p>
|
* <p>
|
||||||
* 单字符类型后缀:
|
* 如果后续需要支持科学计数法、下划线分隔符、不同进制等,只需扩展现有状态机的转移规则。
|
||||||
* <pre>
|
* <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>
|
* </pre>
|
||||||
* </p>
|
* 状态说明:
|
||||||
* <p>
|
|
||||||
* 规则约束:<br>
|
|
||||||
* 若数字主体之后出现以下情况,将在词法阶段抛出 {@link LexicalException}:
|
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>空白 + 字母(如 <code>3 L</code>)</li>
|
* <li>INT_PART :读取整数部分,遇到 '.' 进入 DEC_POINT,否则结束。</li>
|
||||||
* <li>未知字母紧邻(如 <code>3E</code>)</li>
|
* <li>DEC_POINT :已读到小数点,必须下一个字符是数字,否则报错。</li>
|
||||||
* <li><code>'/'</code> 紧邻(如 <code>3/</code>、<code>3/*</code>)</li>
|
* <li>FRAC_PART :读取小数部分,遇非法字符则结束主体。</li>
|
||||||
|
* <li>END :主体扫描结束,进入后缀/尾随字符判定。</li>
|
||||||
* </ul>
|
* </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 {
|
public class NumberTokenScanner extends AbstractTokenScanner {
|
||||||
|
|
||||||
/** 合法类型后缀字符集合(单字符,大小写均可) */
|
/**
|
||||||
|
* 支持的单字符类型后缀集合。
|
||||||
|
* 包含:b, s, l, f, d 及其大写形式。
|
||||||
|
* 对于多字符后缀,可扩展为 Set<String> 并在扫描尾部做贪婪匹配。
|
||||||
|
*/
|
||||||
private static final String SUFFIX_CHARS = "bslfdBSLFD";
|
private static final String SUFFIX_CHARS = "bslfdBSLFD";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否由该扫描器处理。
|
||||||
|
* 仅当首字符为数字时,NumberTokenScanner 介入处理。
|
||||||
|
*
|
||||||
|
* @param c 当前待判断字符
|
||||||
|
* @param ctx 当前 LexerContext(可用于进一步判断)
|
||||||
|
* @return 如果为数字返回 true,否则返回 false
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean canHandle(char c, LexerContext ctx) {
|
public boolean canHandle(char c, LexerContext ctx) {
|
||||||
// 仅当遇到数字时,本扫描器才处理
|
|
||||||
return Character.isDigit(c);
|
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
|
@Override
|
||||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||||
StringBuilder literal = new StringBuilder();
|
StringBuilder literal = new StringBuilder();
|
||||||
boolean hasDot = false; // 标记是否已出现过小数点
|
State state = State.INT_PART;
|
||||||
|
|
||||||
/* 1. 读取数字主体部分(包括整数、小数) */
|
// 1. 主体扫描 —— 整数 / 小数
|
||||||
while (!ctx.isAtEnd()) {
|
mainLoop:
|
||||||
char c = ctx.peek();
|
while (!ctx.isAtEnd() && state != State.END) {
|
||||||
if (c == '.' && !hasDot) {
|
char ch = ctx.peek();
|
||||||
// 遇到第一个小数点
|
switch (state) {
|
||||||
hasDot = true;
|
case INT_PART:
|
||||||
literal.append(ctx.advance());
|
if (Character.isDigit(ch)) {
|
||||||
} else if (Character.isDigit(c)) {
|
literal.append(ctx.advance());
|
||||||
// 吸收数字字符
|
} else if (ch == '.') {
|
||||||
literal.append(ctx.advance());
|
state = State.DEC_POINT;
|
||||||
} else {
|
literal.append(ctx.advance());
|
||||||
// 非数字/非小数点,终止主体读取
|
} else {
|
||||||
break;
|
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()) {
|
if (!ctx.isAtEnd()) {
|
||||||
char next = ctx.peek();
|
char next = ctx.peek();
|
||||||
|
|
||||||
/* 2-A: 合法类型后缀,直接吸收(如 42L、3.0F) */
|
// 2‑A. 合法单字符后缀
|
||||||
if (SUFFIX_CHARS.indexOf(next) >= 0) {
|
if (SUFFIX_CHARS.indexOf(next) >= 0) {
|
||||||
literal.append(ctx.advance());
|
literal.append(ctx.advance());
|
||||||
}
|
}
|
||||||
/* 2-B: 若紧跟未知字母(如 42X),抛出词法异常 */
|
// 2‑B. 紧跟未知字母(如 42X)
|
||||||
else if (Character.isLetter(next)) {
|
else if (Character.isLetter(next)) {
|
||||||
throw new LexicalException(
|
throw new LexicalException("未知的数字类型后缀 '" + next + "'", line, col);
|
||||||
"未知的数字类型后缀 '" + next + "'",
|
|
||||||
line, col
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
/* 2-C: 若数字后有空白,且空白后紧跟字母(如 3 L),也为非法 */
|
// 2‑C. 数字后出现空白 + 字母(如 3 L)
|
||||||
else if (Character.isWhitespace(next) && next != '\n') {
|
else if (Character.isWhitespace(next) && next != '\n') {
|
||||||
int off = 1;
|
int off = 1;
|
||||||
char look;
|
char look;
|
||||||
// 跳过所有空白字符,找到第一个非空白字符
|
while (true) {
|
||||||
do {
|
|
||||||
look = ctx.peekAhead(off);
|
look = ctx.peekAhead(off);
|
||||||
if (look == '\n' || look == '\0') break;
|
if (look == '\n' || look == '\0') break; // 行尾或 EOF
|
||||||
if (!Character.isWhitespace(look)) break;
|
if (!Character.isWhitespace(look)) break;
|
||||||
off++;
|
off++;
|
||||||
} while (true);
|
}
|
||||||
|
|
||||||
if (Character.isLetter(look)) {
|
if (Character.isLetter(look)) {
|
||||||
// 抛出:数字字面量与位宽符号之间不允许有空白符
|
throw new LexicalException("数字字面量与类型后缀之间不允许有空白符", line, col);
|
||||||
throw new LexicalException(
|
|
||||||
"数字字面量与位宽符号之间不允许有空白符",
|
|
||||||
line, col
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* 2-D: 若紧跟 '/',抛出异常防止死循环 */
|
// 2‑D. 紧跟 '/'(如 3/ 或 3/*)
|
||||||
else if (next == '/') {
|
else if (next == '/') {
|
||||||
throw new LexicalException(
|
throw new LexicalException("数字字面量后不允许直接出现 '/'", line, col);
|
||||||
"数字字面量后不允许直接出现 '/'",
|
|
||||||
line, col
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// 其余情况(如分号、括号、运算符),交由其他扫描器处理
|
// 其他字符(分号、运算符、括号等)留给外层扫描流程处理
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3. 返回 NUMBER_LITERAL Token */
|
// 3. 生成并返回 Token
|
||||||
return new Token(TokenType.NUMBER_LITERAL, literal.toString(), line, col);
|
return new Token(TokenType.NUMBER_LITERAL, literal.toString(), line, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FSM 内部状态。
|
||||||
|
* 每次读取一个字符后,根据“当前状态 + 当前字符”决定转移。
|
||||||
|
*/
|
||||||
|
private enum State {
|
||||||
|
/**
|
||||||
|
* 整数部分(尚未读到小数点)
|
||||||
|
*/
|
||||||
|
INT_PART,
|
||||||
|
/**
|
||||||
|
* 已读到小数点,但还未读到第一位小数数字
|
||||||
|
*/
|
||||||
|
DEC_POINT,
|
||||||
|
/**
|
||||||
|
* 小数部分(小数点右侧)
|
||||||
|
*/
|
||||||
|
FRAC_PART,
|
||||||
|
/**
|
||||||
|
* 主体结束,准备处理后缀或交还控制权
|
||||||
|
*/
|
||||||
|
END
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user