feat: NumberTokenScanner 增加规则约束

This commit is contained in:
Luke 2025-06-29 23:56:21 +08:00
parent cb4faf0632
commit 6a247f456c
2 changed files with 130 additions and 101 deletions

View File

@ -3,60 +3,72 @@ package org.jcnc.snow.compiler.lexer.core;
import org.jcnc.snow.compiler.lexer.base.TokenScanner; import org.jcnc.snow.compiler.lexer.base.TokenScanner;
/** /**
* {@code LexerContext} 词法分析阶段的上下文状态管理器 * {@code LexerContext} 词法分析阶段的上下文状态管理器<br>
* <p> * <p>
* 该类提供对源代码字符流的读取访问追踪当前行号与列号 * 提供对源代码字符流的读取访问行列号追踪指针推进与字符匹配等操作
* 并支持字符匹配回看与指针推进等操作 {@link TokenScanner} 实现进行词法识别的重要支撑工具 * {@link TokenScanner} 实现进行词法识别的基础设施
* </p> * </p>
* <p> * <p>
* 所有源代码输入在构造时统一将 Windows 风格的换行符\r\n转换为 Unix 风格\n * 设计要点
* 保证换行行为一致性 * <ul>
* <li>构造时统一将 Windows 换行符 (<code>\r\n</code>) 转换为 Unix 风格 (<code>\n</code>)</li>
* <li>所有坐标均以 <strong>1</strong> 为起始行列号更贴合人类直觉</li>
* <li>提供 {@link #peekAhead(int)} 方法以支持向前多字符查看而不移动游标</li>
* </ul>
* </p> * </p>
*/ */
public class LexerContext { public class LexerContext {
/** 源代码字符串,换行符已标准化为 \n */ /* ───────────────────────────────── 私有字段 ───────────────────────────────── */
/** 源代码字符串(换行符已标准化为 \n */
private final String source; private final String source;
/** 当前扫描位置(从 0 开始的偏移 */ /** 当前扫描位置(自 0 起算的全局偏移量 */
private int pos = 0; private int pos = 0;
/** 当前行号,从 1 开始 */ /** 当前行号(从 1 开始) */
private int line = 1; private int line = 1;
/** 当前列号,从 1 开始 */ /** 当前列号(从 1 开始) */
private int col = 1; private int col = 1;
/** 上一个字符对应的列号(用于置精确记录 */ /** 上一个字符对应的列号(用于异常定位) */
private int lastCol = 1; private int lastCol = 1;
/* ──────────────────────────────── 构造 & 基本信息 ─────────────────────────────── */
/** /**
* 构造一个新的 {@code LexerContext} 实例并标准化换行符 * 创建新的 {@code LexerContext}完成换行符标准化
* *
* @param source 原始源代码字符串 * @param rawSource 原始源代码文本
*/ */
public LexerContext(String source) { public LexerContext(String rawSource) {
this.source = source.replace("\r\n", "\n"); this.source = rawSource.replace("\r\n", "\n");
} }
/** /**
* 判断是否已读取到源代码末 * 判断是否已到达源代码结
* *
* @return 已结束返回 {@code true}否则返回 {@code false} * @return 游标位于终点之后返回 {@code true}
*/ */
public boolean isAtEnd() { public boolean isAtEnd() {
return pos >= source.length(); return pos >= source.length();
} }
/* ──────────────────────────────── 指针推进与查看 ─────────────────────────────── */
/** /**
* 消费当前字符并前进一个位置自动更新行列信息 * 消费 <em>当前</em> 字符并前进一个位置同时更新行列号
* *
* @return 当前字符若已结束则返回空字符'\0' * @return 被消费的字符若已结束则返回空字符 {@code '\0'}
*/ */
public char advance() { public char advance() {
if (isAtEnd()) return '\0'; if (isAtEnd()) return '\0';
char c = source.charAt(pos++); char c = source.charAt(pos++);
lastCol = col; lastCol = col;
if (c == '\n') { if (c == '\n') {
line++; line++;
col = 1; col = 1;
@ -67,9 +79,9 @@ public class LexerContext {
} }
/** /**
* 查看当前位置的字符但不前进 * 查看当前位置字符但不移动游标
* *
* @return 当前字符若结束则返回空字符 * @return 当前字符若越界则返回 {@code '\0'}
*/ */
public char peek() { public char peek() {
return isAtEnd() ? '\0' : source.charAt(pos); return isAtEnd() ? '\0' : source.charAt(pos);
@ -78,17 +90,29 @@ public class LexerContext {
/** /**
* 查看下一个字符但不改变位置 * 查看下一个字符但不改变位置
* *
* @return 下一个字符若结束则返回空字符 * @return 下一字符若越界则返回 {@code '\0'}
*/ */
public char peekNext() { public char peekNext() {
return pos + 1 >= source.length() ? '\0' : source.charAt(pos + 1); return pos + 1 >= source.length() ? '\0' : source.charAt(pos + 1);
} }
/** /**
* 若当前字符与期望字符相同则前进并返回 {@code true}否则不动并返回 {@code false} * 向前查看 <em>offset</em> 个字符不移动游标offset=1 等价于 {@link #peekNext()}
* *
* @param expected 期待匹配的字符 * @param offset 偏移量 ( 1)
* @return 是否匹配成功并消费 * @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) { public boolean match(char expected) {
if (isAtEnd() || source.charAt(pos) != expected) return false; if (isAtEnd() || source.charAt(pos) != expected) return false;
@ -96,30 +120,17 @@ public class LexerContext {
return true; return true;
} }
/** /* ──────────────────────────────── 坐标查询 ─────────────────────────────── */
* 获取当前位置的行号
*
* @return 当前行号 1 开始
*/
public int getLine() {
return line;
}
/** /** @return 当前行号 (1-based) */
* 获取当前位置的列号 public int getLine() { return line; }
*
* @return 当前列号 1 开始
*/
public int getCol() {
return col;
}
/** /** @return 当前列号 (1-based) */
* 获取上一个字符所在的列号 public int getCol() { return col; }
*
* @return 上一个字符对应的列位置 /** @return 上一个字符的列号 */
*/ public int getLastCol() { return lastCol; }
public int getLastCol() {
return lastCol; /** @return 当前指针在源文件中的全局偏移 (0-based) */
} public int getPos() { return pos; }
} }

View File

@ -1,92 +1,110 @@
package org.jcnc.snow.compiler.lexer.scanners; package org.jcnc.snow.compiler.lexer.scanners;
import org.jcnc.snow.compiler.lexer.core.LexerContext; 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.Token;
import org.jcnc.snow.compiler.lexer.token.TokenType; import org.jcnc.snow.compiler.lexer.token.TokenType;
/** /**
* 数字扫描器识别整数小数以及带有<strong>类型后缀</strong>的数字字面量 * 数字扫描器识别整数小数以及带有 <strong>类型后缀</strong> 的数字字面量<br>
* <p> * <p>
* 支持格式示例 * 支持格式示例
* <ul> * <ul>
* <li>整数123045678</li> * <li>整数<code>123</code><code>0</code><code>45678</code></li>
* <li>小数3.140.512.0</li> * <li>小数<code>3.14</code><code>0.5</code><code>12.0</code></li>
* <li>类型后缀2.0f42L7s255B</li> * <li>后缀<code>2.0f</code><code>42L</code><code>7s</code><code>255B</code></li>
* </ul> * </ul>
* </p>
* <p> * <p>
* 语法允许在数字 (整数或小数) 末尾添加以下<strong>单字符后缀</strong>来显式指定常量类型 * 单字符类型后缀
* <pre>b | s | l | f | d // 分别对应 byteshortlongfloatdouble * <pre>
* B | S | L | F | D // 同上大小写皆可</pre> * b | s | l | f | d // byte, short, long, float, double
* 生成的 Token 类型始终为 {@code NUMBER_LITERAL}词法单元将携带完整的文本含后缀若存在 * B | S | L | F | D // 同上大小写均可
* </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>
* </ul>
* 以避免编译器陷入死循环
* </p>
*/ */
public class NumberTokenScanner extends AbstractTokenScanner { public class NumberTokenScanner extends AbstractTokenScanner {
/** /** 合法类型后缀字符集合 */
* 可选类型后缀字符集合 (大小写均可)
* {@code ExpressionBuilder} 内的后缀解析逻辑保持一致
*/
private static final String SUFFIX_CHARS = "bslfdBSLFD"; private static final String SUFFIX_CHARS = "bslfdBSLFD";
/**
* 判断是否可以处理当前位置的字符
* <p>当字符为数字时表示可能是数字字面量的起始</p>
*
* @param c 当前字符
* @param ctx 当前词法上下文
* @return 如果为数字字符则返回 true
*/
@Override @Override
public boolean canHandle(char c, LexerContext ctx) { public boolean canHandle(char c, LexerContext ctx) {
return Character.isDigit(c); return Character.isDigit(c);
} }
/**
* 执行数字扫描逻辑
* <ol>
* <li>连续读取数字字符允许出现<strong>一个</strong>小数点用于识别整数或小数</li>
* <li>读取完主体后<strong>一次性</strong>检查下一个字符若属于合法类型后缀则吸收</li>
* </ol>
* 这样可以保证诸如 {@code 2.0f} 被视为一个整体的 {@code NUMBER_LITERAL}
* 而不是拆分成 "2.0" "f" 两个 Token
*
* @param ctx 词法上下文
* @param line 当前行号
* @param col 当前列号
* @return 表示数字字面量的 Token
*/
@Override @Override
protected Token scanToken(LexerContext ctx, int line, int col) { protected Token scanToken(LexerContext ctx, int line, int col) {
StringBuilder sb = new StringBuilder(); StringBuilder literal = new StringBuilder();
boolean hasDot = false; // 标识是否已遇到小数点 boolean hasDot = false; // 是否已遇到小数点
/* /* 1. 读取数字主体(整数 / 小数) */
* 1 扫描整数或小数主体
* 允许出现一个小数点其余必须是数字
*/
while (!ctx.isAtEnd()) { while (!ctx.isAtEnd()) {
char c = ctx.peek(); char c = ctx.peek();
if (c == '.' && !hasDot) { if (c == '.' && !hasDot) {
hasDot = true; hasDot = true;
sb.append(ctx.advance()); literal.append(ctx.advance());
} else if (Character.isDigit(c)) { } else if (Character.isDigit(c)) {
sb.append(ctx.advance()); literal.append(ctx.advance());
} else { } else {
break; // 遇到非数字/第二个点 => 主体结束 break;
} }
} }
/* /* 2. 处理后缀或非法跟随字符 */
* 2 可选类型后缀
* 如果下一字符是合法后缀字母则一起纳入当前 Token
*/
if (!ctx.isAtEnd()) { if (!ctx.isAtEnd()) {
char suffix = ctx.peek(); char next = ctx.peek();
if (SUFFIX_CHARS.indexOf(suffix) >= 0) {
sb.append(ctx.advance()); /* 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" /* 3. 返回 NUMBER_LITERAL Token */
return new Token(TokenType.NUMBER_LITERAL, sb.toString(), line, col); return new Token(TokenType.NUMBER_LITERAL, literal.toString(), line, col);
} }
} }