diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java index 1a9b8bb..eef90ae 100644 --- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java @@ -128,33 +128,30 @@ public class NumberTokenScanner extends AbstractTokenScanner { if (!ctx.isAtEnd()) { char next = ctx.peek(); - // 2‑A. 合法单字符后缀 + // 2-A. 合法单字符后缀(紧邻,不允许空格) if (SUFFIX_CHARS.indexOf(next) >= 0) { literal.append(ctx.advance()); } - // 2‑B. 紧跟未知字母(如 42X) + // 未知单字符后缀 —— 直接报错 else if (Character.isLetter(next)) { throw new LexicalException("未知的数字类型后缀 '" + next + "'", line, col); } - // 2‑C. 数字后出现空白 + 类型后缀(如 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 } } diff --git a/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java b/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java index c5bfb36..f278b41 100644 --- a/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java +++ b/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java @@ -14,61 +14,34 @@ import java.util.StringJoiner; /** * 语法解析引擎(ParserEngine)。 - *

- * 负责驱动 Snow 源码的顶层语法结构解析,将源码 TokenStream - * 递交给各类 TopLevelParser,并收集语法树节点与异常。 - * 支持容错解析,能够批量报告所有语法错误,并提供同步恢复功能。 - *

- * - *

- * 典型用法: - *

- *     ParserEngine engine = new ParserEngine(context);
- *     List<Node> ast = engine.parse();
- * 
- *

- * - * @param ctx 解析器上下文,负责持有 TokenStream 及所有全局状态。 + *

驱动顶层解析,并在捕获异常后通过同步机制恢复,防止死循环。

*/ public record ParserEngine(ParserContext ctx) { - /** - * 解析输入 TokenStream,生成语法树节点列表。 - * - *

- * 调用各类顶级语句解析器(如 module, func, import), - * 遇到错误时会自动跳过到下一行或已知结构关键字,继续后续分析, - * 最终汇总所有错误。如果解析出现错误,将以 - * {@link UnexpectedToken} 抛出所有语法错误信息。 - *

- * - * @return AST 节点列表,每个节点对应一个顶层语法结构 - * @throws UnexpectedToken 如果解析期间发现语法错误 - */ + /** 解析整份 TokenStream,返回顶层 AST 节点列表。 */ public List parse() { List nodes = new ArrayList<>(); - List errs = new ArrayList<>(); - TokenStream ts = ctx.getTokens(); + List 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,直到遇到下一行 - * 或下一个可识别的顶级结构关键字,以保证后续解析不会被卡住。 - *

- * 同时会跳过连续空行。 - *

- * - * @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(); } diff --git a/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java b/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java index 7fa779c..82c486f 100644 --- a/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java +++ b/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java @@ -8,22 +8,38 @@ import org.jcnc.snow.compiler.parser.top.ScriptTopLevelParser; import java.util.Map; import java.util.HashMap; +/** + * {@code TopLevelParserFactory} 用于根据源码中顶层关键字取得对应的解析器。 + *

+ * 若关键字未注册,则回退到脚本模式解析器 {@link ScriptTopLevelParser}。 + */ public class TopLevelParserFactory { + /** 关键字 → 解析器注册表 */ private static final Map 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); + } }