feat: 支持语义错误定位到具体文件

This commit is contained in:
Luke 2025-06-09 14:39:58 +08:00
parent ed898d1e74
commit f356bcf227
5 changed files with 79 additions and 16 deletions

View File

@ -59,7 +59,7 @@ public class SnowCompiler {
System.out.println(code); System.out.println(code);
LexerEngine lexer = new LexerEngine(code, p.toString()); LexerEngine lexer = new LexerEngine(code, p.toString());
ParserContext ctx = new ParserContext(lexer.getAllTokens()); ParserContext ctx = new ParserContext(lexer.getAllTokens(), p.toString());
allAst.addAll(new ParserEngine(ctx).parse()); allAst.addAll(new ParserEngine(ctx).parse());
} }

View File

@ -19,7 +19,8 @@ public record CallExpressionNode(
ExpressionNode callee, // 被调用的表达式节点表示函数或方法名 ExpressionNode callee, // 被调用的表达式节点表示函数或方法名
List<ExpressionNode> arguments, // 函数调用的参数表达式列表 List<ExpressionNode> arguments, // 函数调用的参数表达式列表
int line, // 当前节点所在的行号 int line, // 当前节点所在的行号
int column // 当前节点所在的列号 int column, // 当前节点所在的列号
String file // 当前节点所在的文件
) implements ExpressionNode { ) implements ExpressionNode {
/** /**
@ -59,4 +60,11 @@ public record CallExpressionNode(
public int column() { public int column() {
return column; return column;
} }
/**
* 获取当前表达式所在的文件名
*
* @return 当前表达式所在的文件名
*/
public String file() { return file; }
} }

View File

@ -16,15 +16,27 @@ public class ParserContext {
/** 当前语法分析所使用的 Token 流 */ /** 当前语法分析所使用的 Token 流 */
private final TokenStream tokens; private final TokenStream tokens;
/** 当前语法分析所使用的资源文件名 */
private final String sourceName;
/** /**
* 使用词法分析得到的 Token 列表构造上下文 * 构造一个新的 {@code ParserContext} 实例用于在语法分析阶段传递上下文信息
* <p>
* 本构造方法接收词法分析得到的 {@link Token} 列表以及当前源文件名<br>
* 并将 Token 列表包装为 {@link TokenStream} 以便后续遍历与分析<br>
* 源文件名通常用于错误定位调试和报错信息中指明具体文件
* </p>
* *
* @param tokens 词法分析器生成的 Token 集合 * @param tokens 词法分析器生成的 Token 集合表示待解析的完整源代码流
* @param sourceName 当前正在解析的源文件名或文件路径用于错误报告和调试定位
*/ */
public ParserContext(List<Token> tokens) { public ParserContext(List<Token> tokens, String sourceName) {
this.tokens = new TokenStream(tokens); this.tokens = new TokenStream(tokens);
this.sourceName = sourceName;
} }
/** /**
* 获取封装的 Token 用于驱动语法分析过程 * 获取封装的 Token 用于驱动语法分析过程
* *
@ -33,4 +45,13 @@ public class ParserContext {
public TokenStream getTokens() { public TokenStream getTokens() {
return tokens; return tokens;
} }
/**
* 获取资源文件名用于发现错误后展示文件名
*
* @return 当前语法分析所使用的资源文件名
*/
public String getSourceName() {
return sourceName;
}
} }

View File

@ -43,8 +43,9 @@ public class CallParselet implements InfixParselet {
ctx.getTokens().expect(")"); // 消费并验证 ")" ctx.getTokens().expect(")"); // 消费并验证 ")"
// 创建 CallExpressionNode 并传递位置信息 // 创建 CallExpressionNode 并传递位置信息,文件名称
return new CallExpressionNode(left, args, line, column); String file = ctx.getSourceName();
return new CallExpressionNode(left, args, line, column, file);
} }
/** /**

View File

@ -1,32 +1,65 @@
package org.jcnc.snow.compiler.semantic.error; package org.jcnc.snow.compiler.semantic.error;
import org.jcnc.snow.compiler.parser.ast.base.Node; import org.jcnc.snow.compiler.parser.ast.base.Node;
/** /**
* 表示一次语义错误<br/> * 表示一次语义错误Semantic Error
* <p>
* 本类用于在语义分析阶段记录出错的 AST 节点及对应的错误信息<br>
* 便于后续错误报告调试和 IDE 集成等多种用途
* </p>
* <ul> * <ul>
* <li>记录对应 {@link Node} 及出错信息</li> * <li>通过关联的 {@link Node} 提供出错的具体位置文件行号列号等信息</li>
* <li>重写 {@link #toString()} <code> X, Y: message</code> 格式输出</li> * <li>支持格式化错误输出友好展示错误发生的上下文</li>
* <li>避免默认的 <code>Node@hash</code> 形式</li> * <li>避免直接输出 AST 节点的默认 <code>toString()</code> 形式</li>
* </ul> * </ul>
*
* <p><b>示例输出</b></p>
* <pre>
* playground\main.snow: 7, 28: 参数类型不匹配 (位置 1): 期望 int, 实际 long
* </pre>
*
* @param node 指向发生语义错误的 AST 节点可用于获取详细的位置信息文件名行号列号等
* @param message 描述该语义错误的详细信息通常为人类可读的解释或修正建议
*
*/ */
public record SemanticError(Node node, String message) { public record SemanticError(Node node, String message) {
/**
* 返回该语义错误的字符串描述格式如下
* <pre>
* [文件名: ] X, Y: [错误信息]
* </pre>
* 若节点未能提供有效位置则输出未知位置
*
* @return 适合用户阅读的语义错误描述字符串
*/
@Override @Override
public String toString() { public String toString() {
// Node 假定提供 line() / column() 方法如无则返回 -1 // Node 假定提供 line() / column() 方法如无则返回 -1
int line = -1; int line = -1;
int col = -1; int col = -1;
String file = null;
if (node != null) { if (node != null) {
try { try {
line = (int) node.getClass().getMethod("line").invoke(node); line = (int) node.getClass().getMethod("line").invoke(node);
} catch (Exception ignored) {
}
try {
col = (int) node.getClass().getMethod("column").invoke(node); col = (int) node.getClass().getMethod("column").invoke(node);
} catch (ReflectiveOperationException ignored) { } catch (Exception ignored) {
// Node 未提供 line/column 方法则保持 -1 }
try {
file = (String) node.getClass().getMethod("file").invoke(node);
} catch (Exception ignored) {
} }
} }
String pos = (line >= 0 && col >= 0) ? ("" + line + ", 列 " + col) : "未知位置";
return pos + ": " + message; StringBuilder sb = new StringBuilder();
if (file != null && !file.isBlank()) sb.append(file).append(": ");
sb.append((line >= 0 && col >= 0) ? "" + line + ", 列 " + col : "未知位置");
sb.append(": ").append(message);
return sb.toString();
} }
} }