feat: 统一 parser 的异常

This commit is contained in:
Luke 2025-07-03 23:49:26 +08:00
parent b730b53f7b
commit f1086a1ef9
12 changed files with 205 additions and 75 deletions

View File

@ -0,0 +1,11 @@
package org.jcnc.snow.compiler.parser.context;
/**
* 当语法结构缺失必须出现的 Token 时抛出
*/
public final class MissingToken extends ParseException {
public MissingToken(String message) {
super(message);
}
}

View File

@ -1,22 +1,19 @@
package org.jcnc.snow.compiler.parser.context;
/**
* {@code ParseException} 表示语法分析阶段发生的错误
* <p>
* 当语法分析器遇到非法的语法结构或无法继续处理的标记序列时
* 应抛出该异常以中断当前解析流程并向调用方报告错误信息
* </p>
* <p>
* 该异常通常由 {@code ParserContext} 或各类语法规则处理器主动抛出
* 用于提示编译器前端或 IDE 系统进行错误提示与恢复
* </p>
* {@code ParseException}语法分析阶段所有错误的基类
*
* <p>声明为 <em>sealed</em>仅允许 {@link UnexpectedToken}
* {@link MissingToken}{@link UnsupportedFeature} 三个受信子类继承
* 以便调用方根据异常类型进行精确处理</p>
*/
public class ParseException extends RuntimeException {
public sealed class ParseException extends RuntimeException
permits UnexpectedToken, MissingToken, UnsupportedFeature {
/**
* 构造一个带有错误描述信息的解析异常实例
* 构造解析异常并附带错误消息
*
* @param message 错误描述文本用于指明具体的语法错误原因
* @param message 错误描述
*/
public ParseException(String message) {
super(message);

View File

@ -31,7 +31,7 @@ public class TokenStream {
*/
public TokenStream(List<Token> tokens) {
if (tokens == null) {
throw new NullPointerException("Token list cannot be null.");
throw new NullPointerException("Token 列表不能为空");
}
this.tokens = tokens;
}
@ -103,8 +103,8 @@ public class TokenStream {
Token t = peek();
if (!t.getLexeme().equals(lexeme)) {
throw new ParseException(
"Expected lexeme '" + lexeme + "' but got '" + t.getLexeme() +
"' at " + t.getLine() + ":" + t.getCol()
"期望的词素是'" + lexeme + "',但得到的是'" + t.getLexeme() +
"" + t.getLine() + ":" + t.getCol()
);
}
return next();
@ -122,8 +122,8 @@ public class TokenStream {
Token t = peek();
if (t.getType() != type) {
throw new ParseException(
"Expected token type " + type + " but got " + t.getType() +
" ('" + t.getLexeme() + "') at " + t.getLine() + ":" + t.getCol()
"期望的标记类型为 " + type + " 但实际得到的是 " + t.getType() +
" ('" + t.getLexeme() + "') " + t.getLine() + ":" + t.getCol()
);
}
return next();

View File

@ -0,0 +1,11 @@
package org.jcnc.snow.compiler.parser.context;
/**
* 当解析过程中遇到意料之外或无法识别的 Token 时抛出
*/
public final class UnexpectedToken extends ParseException {
public UnexpectedToken(String message) {
super(message);
}
}

View File

@ -0,0 +1,11 @@
package org.jcnc.snow.compiler.parser.context;
/**
* 当源码使用了当前编译器尚未支持的语言特性或语法时抛出
*/
public final class UnsupportedFeature extends ParseException {
public UnsupportedFeature(String message) {
super(message);
}
}

View File

@ -1,24 +1,58 @@
package org.jcnc.snow.compiler.parser.core;
import org.jcnc.snow.compiler.lexer.token.TokenType;
import org.jcnc.snow.compiler.parser.ast.base.Node;
import org.jcnc.snow.compiler.parser.base.TopLevelParser;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.context.TokenStream;
import org.jcnc.snow.compiler.parser.context.UnexpectedToken;
import org.jcnc.snow.compiler.parser.factory.TopLevelParserFactory;
import org.jcnc.snow.compiler.parser.ast.base.Node;
import java.util.ArrayList;
import java.util.List;
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 及所有全局状态
*/
public record ParserEngine(ParserContext ctx) {
/**
* 解析输入 TokenStream生成语法树节点列表
*
* <p>
* 调用各类顶级语句解析器 module, func, import
* 遇到错误时会自动跳过到下一行或已知结构关键字继续后续分析
* 最终汇总所有错误如果解析出现错误将以
* {@link UnexpectedToken} 抛出所有语法错误信息
* </p>
*
* @return AST 节点列表每个节点对应一个顶层语法结构
* @throws UnexpectedToken 如果解析期间发现语法错误
*/
public List<Node> parse() {
List<Node> nodes = new ArrayList<>();
List<String> errs = new ArrayList<>();
TokenStream ts = ctx.getTokens();
// 主循环直到全部 token 处理完毕
while (ts.isAtEnd()) {
// 跳过空行
// 跳过所有空行
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
continue;
@ -30,22 +64,31 @@ public record ParserEngine(ParserContext ctx) {
nodes.add(parser.parse(ctx));
} catch (Exception ex) {
errs.add(ex.getMessage());
synchronize(ts); // 错误恢复
synchronize(ts); // 错误恢复同步到下一个语句
}
}
// 批量报告所有解析错误
if (!errs.isEmpty()) {
throw new IllegalStateException("解析过程中检测到 "
+ errs.size() + " 处错误:\n - "
+ String.join("\n - ", errs));
StringJoiner sj = new StringJoiner("\n - ", "", "");
errs.forEach(sj::add);
throw new UnexpectedToken("解析过程中检测到 "
+ errs.size() + " 处错误:\n - " + sj);
}
return nodes;
}
/**
* 错误同步跳到下一行或下一个已注册顶层关键字
* 错误同步机制跳过当前 TokenStream直到遇到下一行
* 或下一个可识别的顶级结构关键字以保证后续解析不会被卡住
* <p>
* 同时会跳过连续空行
* </p>
*
* @param ts 当前 TokenStream
*/
private void synchronize(TokenStream ts) {
// 跳到下一行或下一个顶层结构关键字
while (ts.isAtEnd()) {
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
@ -56,7 +99,7 @@ public record ParserEngine(ParserContext ctx) {
}
ts.next();
}
// 连续空行全部吃掉
// 吃掉后续所有空行
while (ts.isAtEnd() && ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
}

View File

@ -4,6 +4,7 @@ import org.jcnc.snow.compiler.lexer.token.Token;
import org.jcnc.snow.compiler.lexer.token.TokenType;
import org.jcnc.snow.compiler.parser.ast.base.ExpressionNode;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.context.UnsupportedFeature;
import org.jcnc.snow.compiler.parser.expression.base.ExpressionParser;
import org.jcnc.snow.compiler.parser.expression.base.InfixParselet;
import org.jcnc.snow.compiler.parser.expression.base.PrefixParselet;
@ -87,7 +88,7 @@ public class PrattExpressionParser implements ExpressionParser {
Token token = ctx.getTokens().next();
PrefixParselet prefix = prefixes.get(token.getType().name());
if (prefix == null) {
throw new IllegalStateException("没有为该 Token 类型注册前缀解析器: " + token.getType());
throw new UnsupportedFeature("没有为该 Token 类型注册前缀解析器: " + token.getType());
}
ExpressionNode left = prefix.parse(ctx, token);
@ -96,7 +97,10 @@ public class PrattExpressionParser implements ExpressionParser {
&& prec.ordinal() < nextPrecedence(ctx)) {
String lex = ctx.getTokens().peek().getLexeme();
InfixParselet infix = infixes.get(lex);
if (infix == null) break;
if (infix == null) {
throw new UnsupportedFeature(
"没有为该 Token 类型注册中缀解析器: " + token.getType());
}
left = infix.parse(ctx, left);
}
return left;

View File

@ -7,6 +7,7 @@ import org.jcnc.snow.compiler.parser.context.TokenStream;
import org.jcnc.snow.compiler.parser.ast.ImportNode;
import org.jcnc.snow.compiler.parser.ast.ModuleNode;
import org.jcnc.snow.compiler.parser.ast.FunctionNode;
import org.jcnc.snow.compiler.parser.context.UnexpectedToken;
import org.jcnc.snow.compiler.parser.function.FunctionParser;
import java.util.ArrayList;
@ -33,7 +34,7 @@ public class ModuleParser implements TopLevelParser {
*
* @param ctx 当前解析器上下文包含词法流状态信息等
* @return 返回一个 {@link ModuleNode} 实例表示完整模块的语法结构
* @throws IllegalStateException 当模块体中出现未识别的语句时抛出
* @throws UnexpectedToken 当模块体中出现未识别的语句时抛出
*/
@Override
public ModuleNode parse(ParserContext ctx) {
@ -86,7 +87,7 @@ public class ModuleParser implements TopLevelParser {
functions.add(funcParser.parse(ctx));
} else {
// 遇到无法识别的语句开头抛出异常并提供详细提示
throw new IllegalStateException("Unexpected token in module: " + lex);
throw new UnexpectedToken("Unexpected token in module: " + lex);
}
}

View File

@ -7,6 +7,7 @@ import org.jcnc.snow.compiler.parser.ast.ExpressionStatementNode;
import org.jcnc.snow.compiler.parser.ast.base.StatementNode;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.context.TokenStream;
import org.jcnc.snow.compiler.parser.context.UnexpectedToken;
import org.jcnc.snow.compiler.parser.expression.PrattExpressionParser;
/**
@ -39,7 +40,7 @@ public class ExpressionStatementParser implements StatementParser {
*
* @param ctx 当前解析上下文提供词法流与状态信息
* @return 返回 {@link AssignmentNode} {@link ExpressionStatementNode} 表示的语法节点
* @throws IllegalStateException 若表达式起始为关键字或语法非法
* @throws UnexpectedToken 若表达式起始为关键字或语法非法
*/
@Override
public StatementNode parse(ParserContext ctx) {
@ -47,7 +48,7 @@ public class ExpressionStatementParser implements StatementParser {
// 快速检查若遇空行或关键字开头不可作为表达式语句
if (ts.peek().getType() == TokenType.NEWLINE || ts.peek().getType() == TokenType.KEYWORD) {
throw new IllegalStateException("Cannot parse expression starting with keyword: " + ts.peek().getLexeme());
throw new UnexpectedToken("无法解析以关键字开头的表达式: " + ts.peek().getLexeme());
}
// 获取当前 token 的行号列号和文件名

View File

@ -19,7 +19,6 @@ public class ScriptTopLevelParser implements TopLevelParser {
public Node parse(ParserContext ctx) {
String first = ctx.getTokens().peek().getLexeme();
StatementParser sp = StatementParserFactory.get(first);
StatementNode stmt = sp.parse(ctx);
return stmt; // StatementNode 亦是 Node
return sp.parse(ctx);
}
}

View File

@ -3,6 +3,7 @@ package org.jcnc.snow.compiler.parser.utils;
import org.jcnc.snow.compiler.lexer.token.TokenType;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.context.TokenStream;
import org.jcnc.snow.compiler.parser.context.UnexpectedToken;
import java.util.Map;
import java.util.function.BiConsumer;
@ -45,7 +46,7 @@ public class FlexibleSectionParser {
* @param ctx 当前解析上下文提供语法环境与作用域信息
* @param tokens 当前 token
* @param sectionDefinitions 各个区块的定义映射key 为关键字value 为判断 + 解析逻辑组合
* @throws RuntimeException 若出现无法识别的关键字或未满足的匹配条件
* @throws UnexpectedToken 若出现无法识别的关键字或未满足的匹配条件
*/
public static void parse(ParserContext ctx,
TokenStream tokens,
@ -70,7 +71,7 @@ public class FlexibleSectionParser {
if (definition != null && definition.condition().test(tokens)) {
definition.parser().accept(ctx, tokens); // 执行解析逻辑
} else {
throw new RuntimeException("未识别的关键字或条件不满足: " + keyword);
throw new UnexpectedToken("未识别的关键字或条件不满足: " + keyword);
}
}
}

View File

@ -1,5 +1,7 @@
package org.jcnc.snow.compiler.parser.utils;
import org.jcnc.snow.compiler.parser.context.UnexpectedToken;
import java.util.*;
import java.util.Map.Entry;
@ -10,26 +12,28 @@ import java.util.Map.Entry;
* - 序列化 Java 原生对象转换为符合 JSON 标准的字符串
* <p>
* 设计要点
* 1. 使用静态方法作为唯一入口避免状态共享导致的线程安全问题
* 2. 解析器内部使用 char[] 缓冲区提高访问性能
* 3. 维护行列号信息抛出异常时能精确定位错误位置
* 4. 序列化器基于 StringBuilder预分配容量减少中间字符串创建
* 1. 使用静态方法作为唯一入口避免状态共享导致的线程安全问题
* 2. 解析器内部使用 char[] 缓冲区提高访问性能
* 3. 维护行列号信息抛出异常时能精确定位错误位置
* 4. 序列化器基于 StringBuilder预分配容量减少中间字符串创建
*/
public class JSONParser {
private JSONParser() {}
private JSONParser() {
}
/**
* JSON 文本解析为对应的 Java 对象
*
* @param input JSON 格式字符串
* @return 对应的 Java 原生对象
* - JSON 对象 -> Map<String, Object>
* - JSON 数组 -> List<Object>
* - JSON 字符串 -> String
* - JSON 数值 -> Long Double
* - JSON 布尔 -> Boolean
* - JSON null -> null
* @throws RuntimeException 如果遇到语法错误或多余字符异常消息中包含行列信息
* - JSON 对象 -> Map<String, Object>
* - JSON 数组 -> List<Object>
* - JSON 字符串 -> String
* - JSON 数值 -> Long Double
* - JSON 布尔 -> Boolean
* - JSON null -> null
* @throws UnexpectedToken 如果遇到语法错误或多余字符异常消息中包含行列信息
*/
public static Object parse(String input) {
return new Parser(input).parseInternal();
@ -37,6 +41,7 @@ public class JSONParser {
/**
* Java 原生对象序列化为 JSON 字符串
*
* @param obj 支持的类型MapCollectionStringNumberBoolean null
* @return 符合 JSON 规范的字符串
*/
@ -45,21 +50,31 @@ public class JSONParser {
}
// ======= 内部解析器 =======
/**
* 负责将 char[] 缓冲区中的 JSON 文本解析为 Java 对象
*/
private static class Parser {
/** 输入缓冲区 */
/**
* 输入缓冲区
*/
private final char[] buf;
/** 当前解析到的位置索引 */
/**
* 当前解析到的位置索引
*/
private int pos;
/** 当前字符所在行号,从 1 开始 */
/**
* 当前字符所在行号 1 开始
*/
private int line;
/** 当前字符所在列号,从 1 开始 */
/**
* 当前字符所在列号 1 开始
*/
private int col;
/**
* 构造解析器初始化缓冲区和行列信息
*
* @param input 待解析的 JSON 文本
*/
Parser(String input) {
@ -115,7 +130,9 @@ public class JSONParser {
while (true) {
skipWhitespace();
String key = parseString(); // 解析键
skipWhitespace(); expect(':'); skipWhitespace();
skipWhitespace();
expect(':');
skipWhitespace();
Object val = parseValue(); // 解析值
map.put(key, val);
skipWhitespace();
@ -123,7 +140,8 @@ public class JSONParser {
advance(); // 跳过 '}'
break;
}
expect(','); skipWhitespace();
expect(',');
skipWhitespace();
}
return map;
}
@ -149,7 +167,8 @@ public class JSONParser {
advance();
break;
}
expect(','); skipWhitespace();
expect(',');
skipWhitespace();
}
return list;
}
@ -170,18 +189,35 @@ public class JSONParser {
advance(); // 跳过 '\'
c = currentChar();
switch (c) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case '"':
sb.append('"');
break;
case '\\':
sb.append('\\');
break;
case '/':
sb.append('/');
break;
case 'b':
sb.append('\b');
break;
case 'f':
sb.append('\f');
break;
case 'n':
sb.append('\n');
break;
case 'r':
sb.append('\r');
break;
case 't':
sb.append('\t');
break;
case 'u': // 解析 Unicode 转义
String hex = new String(buf, pos+1, 4);
String hex = new String(buf, pos + 1, 4);
sb.append((char) Integer.parseInt(hex, 16));
pos += 4; col += 4;
pos += 4;
col += 4;
break;
default:
error("无效转义字符 '\\" + c + "'");
@ -250,7 +286,8 @@ public class JSONParser {
private void advance() {
if (pos < buf.length) {
if (buf[pos] == '\n') {
line++; col = 1;
line++;
col = 1;
} else {
col++;
}
@ -292,16 +329,19 @@ public class JSONParser {
* 抛出带行列定位的解析错误
*/
private void error(String msg) {
throw new RuntimeException("Error at line " + line + ", column " + col + ": " + msg);
throw new UnexpectedToken("在第 " + line + " 行,第 " + col + " 列出现错误: " + msg);
}
}
// ======= 内部序列化器 =======
/**
* 负责高效地将 Java 对象写为 JSON 文本
*/
private static class Writer {
/** 默认 StringBuilder 初始容量,避免频繁扩容 */
/**
* 默认 StringBuilder 初始容量避免频繁扩容
*/
private static final int DEFAULT_CAPACITY = 1024;
/**
@ -344,8 +384,8 @@ public class JSONParser {
}
sb.append(']');
} else {
// 其他类型使用 toString 并加引号
quote(obj.toString(), sb);
throw new UnsupportedOperationException(
"不支持的 JSON 字符串化类型: " + obj.getClass());
}
}
@ -356,12 +396,23 @@ public class JSONParser {
sb.append('"');
for (char c : s.toCharArray()) {
switch (c) {
case '\\': sb.append("\\\\"); break;
case '"': sb.append("\\\""); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default: sb.append(c);
case '\\':
sb.append("\\\\");
break;
case '"':
sb.append("\\\"");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
sb.append(c);
}
}
sb.append('"');