fix: 修复数字后空格后接上非法后缀进入死循环的错误

This commit is contained in:
Luke 2025-07-05 14:20:43 +08:00
parent 169523bc33
commit e33f6b0ce2
3 changed files with 54 additions and 66 deletions

View File

@ -128,33 +128,30 @@ public class NumberTokenScanner extends AbstractTokenScanner {
if (!ctx.isAtEnd()) {
char next = ctx.peek();
// 2A. 合法单字符后缀
// 2-A. 合法单字符后缀紧邻不允许空格
if (SUFFIX_CHARS.indexOf(next) >= 0) {
literal.append(ctx.advance());
}
// 2B. 紧跟未知字母 42X
// 未知单字符后缀 直接报错
else if (Character.isLetter(next)) {
throw new LexicalException("未知的数字类型后缀 '" + next + "'", line, col);
}
// 2C. 数字后出现空白 + 类型后缀 3 f 不允许
// 数字 + 空格 + 字母 一律非法
else if (Character.isWhitespace(next) && next != '\n') {
// 允许数字后与普通标识符/关键字间存在空白
// 仅当空白后的首个非空字符是合法的类型后缀时才报错
int off = 1;
char look;
// 跳过任意空白不含换行
// 跳过空白不含换行
while (true) {
look = ctx.peekAhead(off);
if (look == '\n' || look == '\0') break; // 行尾或 EOF
if (look == '\n' || look == '\0') break;
if (!Character.isWhitespace(look)) break;
off++;
}
// 如果紧跟类型后缀字符中间存在空白则视为非法
if (SUFFIX_CHARS.indexOf(look) >= 0) {
throw new LexicalException("数字字面量与类型后缀之间不允许有空白符", line, col);
if (Character.isLetter(look)) {
throw new LexicalException("数字字面量后不允许出现空格再跟标识符/后缀", line, col);
}
}
// 其他字符分号运算符括号等留给外层扫描流程处理
// 其他符号由外层扫描器处理
}
// 3. 生成并返回 Token
@ -165,13 +162,21 @@ public class NumberTokenScanner extends AbstractTokenScanner {
* FSM 内部状态
*/
private enum State {
/** 整数部分(小数点左侧) */
/**
* 整数部分小数点左侧
*/
INT_PART,
/** 已读到小数点,但还未读到第一位小数数字 */
/**
* 已读到小数点但还未读到第一位小数数字
*/
DEC_POINT,
/** 小数部分(小数点右侧) */
/**
* 小数部分小数点右侧
*/
FRAC_PART,
/** 主体结束,准备处理后缀或交还控制权 */
/**
* 主体结束准备处理后缀或交还控制权
*/
END
}
}

View File

@ -14,61 +14,34 @@ import java.util.StringJoiner;
/**
* 语法解析引擎ParserEngine
* <p>
* 负责驱动 Snow 源码的顶层语法结构解析将源码 TokenStream
* 递交给各类 TopLevelParser并收集语法树节点与异常
* 支持容错解析能够批量报告所有语法错误并提供同步恢复功能
* </p>
*
* <p>
* 典型用法
* <pre>
* ParserEngine engine = new ParserEngine(context);
* List&lt;Node&gt; ast = engine.parse();
* </pre>
* </p>
*
* @param ctx 解析器上下文负责持有 TokenStream 及所有全局状态
* <p>驱动顶层解析并在捕获异常后通过同步机制恢复防止死循环</p>
*/
public record ParserEngine(ParserContext ctx) {
/**
* 解析输入 TokenStream生成语法树节点列表
*
* <p>
* 调用各类顶级语句解析器 module, func, import
* 遇到错误时会自动跳过到下一行或已知结构关键字继续后续分析
* 最终汇总所有错误如果解析出现错误将以
* {@link UnexpectedToken} 抛出所有语法错误信息
* </p>
*
* @return AST 节点列表每个节点对应一个顶层语法结构
* @throws UnexpectedToken 如果解析期间发现语法错误
*/
/** 解析整份 TokenStream返回顶层 AST 节点列表。 */
public List<Node> parse() {
List<Node> nodes = new ArrayList<>();
List<String> errs = new ArrayList<>();
TokenStream ts = ctx.getTokens();
// 主循环直到全部 token 处理完毕
// 主循环至 EOF
while (ts.isAtEnd()) {
// 跳过所有空行
// 跳过空行
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
continue;
}
TopLevelParser parser = TopLevelParserFactory.get(ts.peek().getLexeme());
try {
nodes.add(parser.parse(ctx));
} catch (Exception ex) {
errs.add(ex.getMessage());
synchronize(ts); // 错误恢复同步到下一个语句
synchronize(ts); // 出错后同步恢复
}
}
// 批量报告所有解析错误
// 聚合并抛出全部语法错误
if (!errs.isEmpty()) {
StringJoiner sj = new StringJoiner("\n - ", "", "");
errs.forEach(sj::add);
@ -79,27 +52,21 @@ public record ParserEngine(ParserContext ctx) {
}
/**
* 错误同步机制跳过当前 TokenStream直到遇到下一行
* 或下一个可识别的顶级结构关键字以保证后续解析不会被卡住
* <p>
* 同时会跳过连续空行
* </p>
*
* @param ts 当前 TokenStream
* 同步跳过当前行或直到遇到 **显式注册** 的顶层关键字
* 这样可避免因默认脚本解析器导致指针停滞而进入死循环
*/
private void synchronize(TokenStream ts) {
// 跳到下一行或下一个顶层结构关键字
while (ts.isAtEnd()) {
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
break;
}
if (TopLevelParserFactory.get(ts.peek().getLexeme()) != null) {
break;
if (TopLevelParserFactory.isRegistered(ts.peek().getLexeme())) {
break; // 仅在已注册关键字处停下
}
ts.next();
ts.next(); // 继续丢弃 token
}
// 吃掉后续所有空行
// 清掉后续连续空行
while (ts.isAtEnd() && ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
}

View File

@ -8,22 +8,38 @@ import org.jcnc.snow.compiler.parser.top.ScriptTopLevelParser;
import java.util.Map;
import java.util.HashMap;
/**
* {@code TopLevelParserFactory} 用于根据源码中顶层关键字取得对应的解析器
* <p>
* 若关键字未注册则回退到脚本模式解析器 {@link ScriptTopLevelParser}
*/
public class TopLevelParserFactory {
/** 关键字 → 解析器注册表 */
private static final Map<String, TopLevelParser> registry = new HashMap<>();
private static final TopLevelParser DEFAULT = new ScriptTopLevelParser(); // 默认解析器
/** 缺省解析器:脚本模式(单条语句可执行) */
private static final TopLevelParser DEFAULT = new ScriptTopLevelParser();
static {
// 顶层结构解析器
// 在此注册所有受支持的顶层结构关键字
registry.put("module", new ModuleParser());
registry.put("function", new FunctionParser());
// 也可按需继续注册其它关键字
// 若未来新增顶层结构可继续在此处注册
}
/**
* 根据关键字获取解析器若未注册回退到脚本语句解析
* 依据关键字返回解析器若未注册则返回脚本解析器
*/
public static TopLevelParser get(String keyword) {
return registry.getOrDefault(keyword, DEFAULT);
}
/**
* 判断某关键字是否已显式注册为顶层结构
* 供同步恢复逻辑使用避免死循环
*/
public static boolean isRegistered(String keyword) {
return registry.containsKey(keyword);
}
}