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

View File

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

View File

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