Merge branch 'dev' into bugfix/aot-semantic-error-unknown-location
This commit is contained in:
commit
46e913a37b
17
.run/Bug1.run.xml
Normal file
17
.run/Bug1.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Bug1" type="Application" factoryName="Application" folderName="BugFarm">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="graalvm-ce-23" />
|
||||
<option name="MAIN_CLASS_NAME" value="org.jcnc.snow.cli.SnowCLI" />
|
||||
<module name="Snow" />
|
||||
<option name="PROGRAM_PARAMETERS" value="compile run -d playground/BugFarm/Bug1 -o target/Bug1" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="org.jcnc.snow.compiler.parser.preprocessor.lexer.impl.api.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Demo1" type="Application" factoryName="Application" folderName="Demo" activateToolWindowBeforeRun="false">
|
||||
<configuration default="false" name="Demo1" type="Application" factoryName="Application" folderName="Demo">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="graalvm-ce-23" />
|
||||
<option name="MAIN_CLASS_NAME" value="org.jcnc.snow.cli.SnowCLI" />
|
||||
<module name="Snow" />
|
||||
|
||||
17
.run/Demo11.run.xml
Normal file
17
.run/Demo11.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Demo11" type="Application" factoryName="Application" folderName="Demo">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="graalvm-ce-23" />
|
||||
<option name="MAIN_CLASS_NAME" value="org.jcnc.snow.cli.SnowCLI" />
|
||||
<module name="Snow" />
|
||||
<option name="PROGRAM_PARAMETERS" value="compile run -d playground/Demo11 -o target/Demo11" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="org.jcnc.snow.compiler.parser.preprocessor.lexer.impl.api.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
15
.run/测试.run.xml
Normal file
15
.run/测试.run.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="测试" type="CompoundRunConfigurationType">
|
||||
<toRun name="Demo1" type="Application" />
|
||||
<toRun name="Demo10" type="Application" />
|
||||
<toRun name="Demo11" type="Application" />
|
||||
<toRun name="Demo2" type="Application" />
|
||||
<toRun name="Demo3" type="Application" />
|
||||
<toRun name="Demo4" type="Application" />
|
||||
<toRun name="Demo6" type="Application" />
|
||||
<toRun name="Demo7" type="Application" />
|
||||
<toRun name="Demo8" type="Application" />
|
||||
<toRun name="Demo9" type="Application" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
17
playground/BugFarm/Bug1/Main.snow
Normal file
17
playground/BugFarm/Bug1/Main.snow
Normal file
@ -0,0 +1,17 @@
|
||||
module: Main
|
||||
function: main
|
||||
parameter:
|
||||
return_type: int
|
||||
body:
|
||||
declare n1: int =1
|
||||
declare n2: int =2
|
||||
declare n3: int =1
|
||||
if n1 ==1 then
|
||||
if n2 ==2 then
|
||||
n3 =3
|
||||
end if
|
||||
end if
|
||||
return n3
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
12
playground/BugFarm/Bug1/README.md
Normal file
12
playground/BugFarm/Bug1/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
## 编译器输出
|
||||
### Snow 源代码
|
||||
#### Main.snow
|
||||
```snow
|
||||
function: main
|
||||
return_type: int
|
||||
body:
|
||||
3 L
|
||||
return 65537
|
||||
end body
|
||||
end function
|
||||
```
|
||||
6
playground/Demo11/Main.snow
Normal file
6
playground/Demo11/Main.snow
Normal file
@ -0,0 +1,6 @@
|
||||
function: main
|
||||
return_type: int
|
||||
body:
|
||||
return 65537
|
||||
end body
|
||||
end function
|
||||
@ -91,7 +91,7 @@ public class SnowCLI {
|
||||
System.exit(exitCode);
|
||||
} catch (Exception e) {
|
||||
// 捕获命令执行过程中的异常并打印错误消息
|
||||
System.err.println("Error: " + e.getMessage());
|
||||
System.err.println(e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,60 +3,72 @@ package org.jcnc.snow.compiler.lexer.core;
|
||||
import org.jcnc.snow.compiler.lexer.base.TokenScanner;
|
||||
|
||||
/**
|
||||
* {@code LexerContext} 是词法分析阶段的上下文状态管理器。
|
||||
* {@code LexerContext} —— 词法分析阶段的上下文状态管理器。<br>
|
||||
* <p>
|
||||
* 该类提供对源代码字符流的读取访问,追踪当前行号与列号,
|
||||
* 并支持字符匹配、回看与指针推进等操作,是 {@link TokenScanner} 实现进行词法识别的重要支撑工具。
|
||||
* 提供对源代码字符流的读取访问、行列号追踪、指针推进与字符匹配等操作,
|
||||
* 是 {@link TokenScanner} 实现进行词法识别的基础设施。
|
||||
* </p>
|
||||
* <p>
|
||||
* 所有源代码输入在构造时统一将 Windows 风格的换行符(\r\n)转换为 Unix 风格(\n),
|
||||
* 保证换行行为一致性。
|
||||
* 设计要点:
|
||||
* <ul>
|
||||
* <li>构造时统一将 Windows 换行符 (<code>\r\n</code>) 转换为 Unix 风格 (<code>\n</code>)。</li>
|
||||
* <li>所有坐标均以 <strong>1</strong> 为起始行/列号,更贴合人类直觉。</li>
|
||||
* <li>提供 {@link #peekAhead(int)} 方法以支持“向前多字符查看”而不移动游标。</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
public class LexerContext {
|
||||
|
||||
/** 源代码字符串,换行符已标准化为 \n */
|
||||
/* ───────────────────────────────── 私有字段 ───────────────────────────────── */
|
||||
|
||||
/** 源代码字符串(换行符已标准化为 \n) */
|
||||
private final String source;
|
||||
|
||||
/** 当前扫描位置(从 0 开始的偏移) */
|
||||
/** 当前扫描位置(自 0 起算的全局偏移量) */
|
||||
private int pos = 0;
|
||||
|
||||
/** 当前行号,从 1 开始 */
|
||||
/** 当前行号(从 1 开始) */
|
||||
private int line = 1;
|
||||
|
||||
/** 当前列号,从 1 开始 */
|
||||
/** 当前列号(从 1 开始) */
|
||||
private int col = 1;
|
||||
|
||||
/** 上一个字符对应的列号(用于位置精确记录) */
|
||||
/** 上一个字符对应的列号(用于异常定位) */
|
||||
private int lastCol = 1;
|
||||
|
||||
/* ──────────────────────────────── 构造 & 基本信息 ─────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 构造一个新的 {@code LexerContext} 实例,并标准化换行符。
|
||||
* 创建新的 {@code LexerContext},并完成换行符标准化。
|
||||
*
|
||||
* @param source 原始源代码字符串
|
||||
* @param rawSource 原始源代码文本
|
||||
*/
|
||||
public LexerContext(String source) {
|
||||
this.source = source.replace("\r\n", "\n");
|
||||
public LexerContext(String rawSource) {
|
||||
this.source = rawSource.replace("\r\n", "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否已读取到源代码末尾。
|
||||
* 判断是否已到达源代码结尾。
|
||||
*
|
||||
* @return 若已结束,返回 {@code true};否则返回 {@code false}
|
||||
* @return 若游标位于终点之后返回 {@code true}
|
||||
*/
|
||||
public boolean isAtEnd() {
|
||||
return pos >= source.length();
|
||||
}
|
||||
|
||||
/* ──────────────────────────────── 指针推进与查看 ─────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 消费当前字符并前进一个位置,自动更新行列信息。
|
||||
* 消费 <em>当前</em> 字符并前进一个位置,同时更新行列号。
|
||||
*
|
||||
* @return 当前字符,若已结束则返回空字符('\0')
|
||||
* @return 被消费的字符;若已结束则返回空字符 {@code '\0'}
|
||||
*/
|
||||
public char advance() {
|
||||
if (isAtEnd()) return '\0';
|
||||
|
||||
char c = source.charAt(pos++);
|
||||
lastCol = col;
|
||||
|
||||
if (c == '\n') {
|
||||
line++;
|
||||
col = 1;
|
||||
@ -67,9 +79,9 @@ public class LexerContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看当前位置的字符,但不前进。
|
||||
* 查看当前位置字符,但不移动游标。
|
||||
*
|
||||
* @return 当前字符,若结束则返回空字符
|
||||
* @return 当前字符;若越界则返回 {@code '\0'}
|
||||
*/
|
||||
public char peek() {
|
||||
return isAtEnd() ? '\0' : source.charAt(pos);
|
||||
@ -78,17 +90,29 @@ public class LexerContext {
|
||||
/**
|
||||
* 查看下一个字符,但不改变位置。
|
||||
*
|
||||
* @return 下一个字符,若结束则返回空字符
|
||||
* @return 下一字符;若越界则返回 {@code '\0'}
|
||||
*/
|
||||
public char peekNext() {
|
||||
return pos + 1 >= source.length() ? '\0' : source.charAt(pos + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若当前字符与期望字符相同,则前进并返回 {@code true},否则不动并返回 {@code false}。
|
||||
* 向前查看 <em>offset</em> 个字符(不移动游标)。offset=1 等价于 {@link #peekNext()}。
|
||||
*
|
||||
* @param expected 期待匹配的字符
|
||||
* @return 是否匹配成功并消费
|
||||
* @param offset 偏移量 (≥ 1)
|
||||
* @return 指定偏移处的字符;若越界返回 {@code '\0'}
|
||||
*/
|
||||
public char peekAhead(int offset) {
|
||||
if (offset <= 0) return peek();
|
||||
int idx = pos + offset;
|
||||
return idx >= source.length() ? '\0' : source.charAt(idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若当前位置字符等于 {@code expected},则消费并返回 {@code true};否则保持原位返回 {@code false}。
|
||||
*
|
||||
* @param expected 期望匹配的字符
|
||||
* @return 是否匹配并消费
|
||||
*/
|
||||
public boolean match(char expected) {
|
||||
if (isAtEnd() || source.charAt(pos) != expected) return false;
|
||||
@ -96,30 +120,17 @@ public class LexerContext {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前位置的行号。
|
||||
*
|
||||
* @return 当前行号(从 1 开始)
|
||||
*/
|
||||
public int getLine() {
|
||||
return line;
|
||||
}
|
||||
/* ──────────────────────────────── 坐标查询 ─────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 获取当前位置的列号。
|
||||
*
|
||||
* @return 当前列号(从 1 开始)
|
||||
*/
|
||||
public int getCol() {
|
||||
return col;
|
||||
}
|
||||
/** @return 当前行号 (1-based) */
|
||||
public int getLine() { return line; }
|
||||
|
||||
/**
|
||||
* 获取上一个字符所在的列号。
|
||||
*
|
||||
* @return 上一个字符对应的列位置
|
||||
*/
|
||||
public int getLastCol() {
|
||||
return lastCol;
|
||||
}
|
||||
/** @return 当前列号 (1-based) */
|
||||
public int getCol() { return col; }
|
||||
|
||||
/** @return 上一个字符的列号 */
|
||||
public int getLastCol() { return lastCol; }
|
||||
|
||||
/** @return 当前指针在源文件中的全局偏移 (0-based) */
|
||||
public int getPos() { return pos; }
|
||||
}
|
||||
|
||||
@ -3,109 +3,152 @@ package org.jcnc.snow.compiler.lexer.core;
|
||||
import org.jcnc.snow.compiler.lexer.base.TokenScanner;
|
||||
import org.jcnc.snow.compiler.lexer.scanners.*;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
import org.jcnc.snow.compiler.lexer.utils.TokenPrinter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@code LexerEngine} 是编译器前端的词法分析器核心实现。
|
||||
* <p>
|
||||
* 负责将源代码字符串按顺序扫描并转换为一系列 {@link Token} 实例,
|
||||
* 每个 Token 表示语法上可识别的最小单位(如标识符、关键字、常量、运算符等)。
|
||||
* <p>
|
||||
* 分析流程通过注册多个 {@link TokenScanner} 扫描器实现类型识别,
|
||||
* 并由 {@link LexerContext} 提供字符流与位置信息支持。
|
||||
* 支持文件名传递,遇到非法字符时会以“文件名:行:列:错误信息”输出简洁诊断。
|
||||
* </p>
|
||||
* Snow 语言词法分析器核心实现。
|
||||
* <p>采用“<b>先扫描 → 后批量校验 → 统一报告</b>”策略:
|
||||
* <ol>
|
||||
* <li>{@link #scanAllTokens()}— 用扫描器链把字符流拆成 {@link Token}</li>
|
||||
* <li>{@link #validateTokens()}— 基于 token 序列做轻量上下文校验</li>
|
||||
* <li>{@link #report(List)}— 一次性输出所有词法错误</li>
|
||||
* </ol></p>
|
||||
*/
|
||||
public class LexerEngine {
|
||||
/**
|
||||
* 扫描生成的 Token 序列(包含文件结束符 EOF)
|
||||
*/
|
||||
private final List<Token> tokens = new ArrayList<>();
|
||||
|
||||
private final List<Token> tokens = new ArrayList<>(); // 扫描结果
|
||||
private final List<LexicalError> errors = new ArrayList<>();
|
||||
private final String absPath; // 绝对路径
|
||||
private final LexerContext context; // 字符流
|
||||
private final List<TokenScanner> scanners; // 扫描器链
|
||||
|
||||
/**
|
||||
* 词法上下文,提供字符流读取与位置信息
|
||||
*/
|
||||
private final LexerContext context;
|
||||
|
||||
/**
|
||||
* Token 扫描器集合,按优先级顺序组织,用于识别不同类别的 Token
|
||||
*/
|
||||
private final List<TokenScanner> scanners;
|
||||
|
||||
/**
|
||||
* 构造词法分析器(假定输入源自标准输入,文件名默认为 <stdin>)
|
||||
*
|
||||
* 创建并立即执行扫描-校验-报告流程。
|
||||
* @param source 源代码文本
|
||||
*/
|
||||
public LexerEngine(String source) {
|
||||
this(source, "<stdin>");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造词法分析器,并指定源文件名(用于诊断信息)。
|
||||
* 构造时立即进行全量扫描。
|
||||
*
|
||||
* @param source 源代码文本
|
||||
* @param sourceName 文件名或来源描述(如"Main.snow")
|
||||
* @param sourceName 文件名(诊断用)
|
||||
*/
|
||||
public LexerEngine(String source, String sourceName) {
|
||||
this.absPath = new File(sourceName).getAbsolutePath();
|
||||
this.context = new LexerContext(source);
|
||||
this.scanners = List.of(
|
||||
new WhitespaceTokenScanner(), // 跳过空格、制表符等
|
||||
new NewlineTokenScanner(), // 处理换行符,生成 NEWLINE Token
|
||||
new CommentTokenScanner(), // 处理单行/多行注释
|
||||
new NumberTokenScanner(), // 识别整数与浮点数字面量
|
||||
new IdentifierTokenScanner(), // 识别标识符和关键字
|
||||
new StringTokenScanner(), // 处理字符串常量
|
||||
new OperatorTokenScanner(), // 识别运算符
|
||||
new SymbolTokenScanner(), // 识别括号、分号等符号
|
||||
new UnknownTokenScanner() // 捕捉无法识别的字符,最后兜底
|
||||
new WhitespaceTokenScanner(),
|
||||
new NewlineTokenScanner(),
|
||||
new CommentTokenScanner(),
|
||||
new NumberTokenScanner(),
|
||||
new IdentifierTokenScanner(),
|
||||
new StringTokenScanner(),
|
||||
new OperatorTokenScanner(),
|
||||
new SymbolTokenScanner(),
|
||||
new UnknownTokenScanner()
|
||||
);
|
||||
|
||||
// 主扫描流程,遇到非法字符立即输出错误并终止进程
|
||||
try {
|
||||
/* 1. 扫描 */
|
||||
scanAllTokens();
|
||||
} catch (LexicalException le) {
|
||||
// 输出:文件名:行:列: 错误信息,简洁明了
|
||||
System.err.printf(
|
||||
"%s:%d:%d: %s%n",
|
||||
sourceName,
|
||||
le.getLine(), // 获取出错行号
|
||||
le.getColumn(), // 获取出错列号
|
||||
le.getMessage() // 错误描述
|
||||
/* 2. 后置整体校验 */
|
||||
validateTokens();
|
||||
/* 3. 打印 token */
|
||||
TokenPrinter.print(tokens);
|
||||
/* 4. 统一报告错误 */
|
||||
report(errors);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new LexicalException(
|
||||
"Lexing failed with " + errors.size() + " error(s).",
|
||||
context.getLine(), context.getCol()
|
||||
);
|
||||
System.exit(65); // 65 = EX_DATAERR,标准数据错误退出码
|
||||
}
|
||||
}
|
||||
|
||||
public static void report(List<LexicalError> errors) {
|
||||
if (errors == null || errors.isEmpty()) {
|
||||
System.out.println("\n## 词法分析通过,没有发现错误\n");
|
||||
return;
|
||||
}
|
||||
System.err.println("\n词法分析发现 " + errors.size() + " 个错误:");
|
||||
errors.forEach(e -> System.err.println(" " + e));
|
||||
}
|
||||
|
||||
public List<Token> getAllTokens() { return List.copyOf(tokens); }
|
||||
public List<LexicalError> getErrors() { return List.copyOf(errors); }
|
||||
|
||||
/**
|
||||
* 主扫描循环,将源代码转为 Token 序列
|
||||
* 依次尝试每个扫描器,直到找到可处理当前字符的扫描器为止
|
||||
* 扫描到结尾后补充 EOF Token
|
||||
* 逐字符扫描:依次尝试各扫描器;扫描器抛出的
|
||||
* {@link LexicalException} 被捕获并转为 {@link LexicalError}。
|
||||
*/
|
||||
private void scanAllTokens() {
|
||||
while (!context.isAtEnd()) {
|
||||
char currentChar = context.peek();
|
||||
// 依次查找能处理当前字符的扫描器
|
||||
for (TokenScanner scanner : scanners) {
|
||||
if (scanner.canHandle(currentChar, context)) {
|
||||
scanner.handle(context, tokens);
|
||||
break; // 已处理,跳到下一个字符
|
||||
char ch = context.peek();
|
||||
boolean handled = false;
|
||||
|
||||
for (TokenScanner s : scanners) {
|
||||
if (!s.canHandle(ch, context)) continue;
|
||||
|
||||
try {
|
||||
s.handle(context, tokens);
|
||||
} catch (LexicalException le) {
|
||||
errors.add(new LexicalError(
|
||||
absPath, le.getLine(), le.getColumn(), le.getReason()
|
||||
));
|
||||
context.advance(); // 跳过问题字符
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!handled) context.advance(); // 理论不会走到,保险
|
||||
}
|
||||
// 末尾补一个 EOF 标记
|
||||
tokens.add(Token.eof(context.getLine()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部 Token(包含 EOF),返回只读列表
|
||||
*
|
||||
* @return 词法分析结果 Token 列表
|
||||
* 目前包含三条规则:<br>
|
||||
* 1. Dot-Prefix'.' 不能作标识符前缀<br>
|
||||
* 2. Declare-Ident declare 后必须紧跟合法标识符,并且只能一个<br>
|
||||
* 3. Double-Ident declare 后若出现第二个 IDENTIFIER 视为多余<br>
|
||||
* <p>发现问题仅写入 {@link #errors},不抛异常。</p>
|
||||
*/
|
||||
public List<Token> getAllTokens() {
|
||||
return List.copyOf(tokens);
|
||||
private void validateTokens() {
|
||||
for (int i = 0; i < tokens.size(); i++) {
|
||||
Token tok = tokens.get(i);
|
||||
|
||||
/* ---------- declare 规则 ---------- */
|
||||
if (tok.getType() == TokenType.KEYWORD
|
||||
&& "declare".equalsIgnoreCase(tok.getLexeme())) {
|
||||
|
||||
// 第一个非 NEWLINE token
|
||||
Token id1 = findNextNonNewline(i);
|
||||
if (id1 == null || id1.getType() != TokenType.IDENTIFIER) {
|
||||
errors.add(err(
|
||||
(id1 == null ? tok : id1),
|
||||
"declare 后必须跟合法标识符 (以字母或 '_' 开头)"
|
||||
));
|
||||
continue; // 若首标识符就错,后续检查可略
|
||||
}
|
||||
|
||||
// 检查是否有第二个 IDENTIFIER
|
||||
Token id2 = findNextNonNewline(tokens.indexOf(id1));
|
||||
if (id2 != null && id2.getType() == TokenType.IDENTIFIER) {
|
||||
errors.add(err(id2, "declare 声明中出现多余的标识符"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** index 右侧最近非 NEWLINE token;无则 null */
|
||||
private Token findNextNonNewline(int index) {
|
||||
for (int j = index + 1; j < tokens.size(); j++) {
|
||||
Token t = tokens.get(j);
|
||||
if (t.getType() != TokenType.NEWLINE) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 构造统一的 LexicalError */
|
||||
private LexicalError err(Token t, String msg) {
|
||||
return new LexicalError(absPath, t.getLine(), t.getCol(), "非法的标记序列:" + msg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package org.jcnc.snow.compiler.lexer.core;
|
||||
|
||||
public class LexicalError {
|
||||
private final String file;
|
||||
private final int line;
|
||||
private final int column;
|
||||
private final String message;
|
||||
|
||||
public LexicalError(String file, int line, int column, String message) {
|
||||
this.file = file;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return file + ": 行 " + line + ", 列 " + column + ": " + message;
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,8 @@ public class LexicalException extends RuntimeException {
|
||||
private final int line;
|
||||
/** 错误发生的列号(从1开始) */
|
||||
private final int column;
|
||||
/** 错误原因 */
|
||||
private final String reason;
|
||||
|
||||
/**
|
||||
* 构造词法异常
|
||||
@ -27,13 +29,14 @@ public class LexicalException extends RuntimeException {
|
||||
* @param column 出错列号
|
||||
*/
|
||||
public LexicalException(String reason, int line, int column) {
|
||||
// 构造出错消息,并禁止异常堆栈打印
|
||||
super(String.format("Lexical error: %s at %d:%d", reason, line, column),
|
||||
null, false, false);
|
||||
// 错误描述直接为 reason,禁止异常堆栈打印
|
||||
super(reason, null, false, false);
|
||||
this.reason = reason;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 屏蔽异常堆栈填充(始终不打印堆栈信息)
|
||||
*/
|
||||
@ -51,4 +54,10 @@ public class LexicalException extends RuntimeException {
|
||||
* @return 列号
|
||||
*/
|
||||
public int getColumn() { return column; }
|
||||
|
||||
/**
|
||||
* 获取出错的描述
|
||||
* @return 出错描述
|
||||
*/
|
||||
public String getReason() { return reason; }
|
||||
}
|
||||
|
||||
@ -1,29 +1,30 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.core.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.core.LexicalException;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 注释扫描器:处理源代码中的注释部分,包括:
|
||||
* {@code CommentTokenScanner} —— 注释解析器,基于有限状态机(FSM)。
|
||||
*
|
||||
* <p>负责将源码中的两种注释形式切分为 {@link TokenType#COMMENT COMMENT} token:</p>
|
||||
* <ol>
|
||||
* <li>单行注释:以 {@code //} 开头,直至行尾或文件末尾。</li>
|
||||
* <li>多行注释:以 {@code /*} 开头,以 <code>*/</code> 结束,可跨多行。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>本扫描器遵循“发现即捕获”原则:注释文本被完整保留在 Token 中,供后续的文档提取、源映射等分析使用。</p>
|
||||
*
|
||||
* <p>错误处理策略</p>
|
||||
* <ul>
|
||||
* <li>单行注释(以 "//" 开头,直到行尾)</li>
|
||||
* <li>多行注释(以 "/*" 开头,以 "*/" 结尾)</li>
|
||||
* <li>未终止的多行注释:若文件结束时仍未遇到 <code>*/</code>,抛出 {@link LexicalException}。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 本扫描器会识别注释并生成 {@code TokenType.COMMENT} 类型的 Token,
|
||||
* 不会丢弃注释内容,而是将完整注释文本保留在 Token 中,便于后续分析(如文档提取、保留注释等场景)。
|
||||
* </p>
|
||||
*/
|
||||
public class CommentTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当当前位置字符为 '/' 且下一个字符为 '/' 或 '*' 时,表示可能是注释的起始。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果是注释的起始符,则返回 true
|
||||
* 仅当当前字符为 {@code '/'} 且下一个字符为 {@code '/'} 或 {@code '*'} 时,由本扫描器处理。
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
@ -31,44 +32,75 @@ public class CommentTokenScanner extends AbstractTokenScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现注释的扫描逻辑。
|
||||
* <p>支持两种注释格式:</p>
|
||||
* <ul>
|
||||
* <li><b>单行注释:</b> 以 "//" 开头,直到遇到换行符</li>
|
||||
* <li><b>多行注释:</b> 以 "/*" 开头,直到遇到 "*/" 结束</li>
|
||||
* </ul>
|
||||
* 执行注释扫描,生成 {@code COMMENT} Token。
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号(用于 Token 位置信息)
|
||||
* @param col 当前列号(用于 Token 位置信息)
|
||||
* @return 包含完整注释内容的 COMMENT 类型 Token
|
||||
* @param line 起始行号(1 基)
|
||||
* @param col 起始列号(1 基)
|
||||
* @return 包含完整注释文本的 Token
|
||||
* @throws LexicalException 若遇到未终止的多行注释
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
// 消费第一个 '/' 字符
|
||||
ctx.advance();
|
||||
StringBuilder sb = new StringBuilder("/");
|
||||
StringBuilder literal = new StringBuilder();
|
||||
State currentState = State.INITIAL;
|
||||
|
||||
// 读取注释起始符
|
||||
literal.append(ctx.advance()); // 消费首个 '/'
|
||||
|
||||
// 处理单行注释 //
|
||||
if (ctx.match('/')) {
|
||||
sb.append('/');
|
||||
while (!ctx.isAtEnd() && ctx.peek() != '\n') {
|
||||
sb.append(ctx.advance());
|
||||
}
|
||||
}
|
||||
// 处理多行注释 /* ... */
|
||||
else if (ctx.match('*')) {
|
||||
sb.append('*');
|
||||
while (!ctx.isAtEnd()) {
|
||||
char ch = ctx.advance();
|
||||
sb.append(ch);
|
||||
if (ch == '*' && ctx.peek() == '/') {
|
||||
sb.append(ctx.advance()); // 消费 '/'
|
||||
break;
|
||||
switch (currentState) {
|
||||
case INITIAL:
|
||||
if (ctx.match('/')) {
|
||||
literal.append('/');
|
||||
currentState = State.SINGLE_LINE;
|
||||
} else if (ctx.match('*')) {
|
||||
literal.append('*');
|
||||
currentState = State.MULTI_LINE;
|
||||
}
|
||||
break;
|
||||
|
||||
case SINGLE_LINE:
|
||||
// 单行注释处理:读取直到行尾
|
||||
if (ctx.isAtEnd() || ctx.peek() == '\n') {
|
||||
// 如果遇到换行符,停止读取并返回注释内容
|
||||
return new Token(TokenType.COMMENT, literal.toString(), line, col);
|
||||
} else {
|
||||
literal.append(ctx.advance()); // 继续读取注释内容
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case MULTI_LINE:
|
||||
// 多行注释处理
|
||||
char ch = ctx.advance();
|
||||
literal.append(ch);
|
||||
if (ch == '*' && ctx.peek() == '/') {
|
||||
literal.append(ctx.advance()); // 追加 '/'
|
||||
currentState = State.MULTI_LINE_END;
|
||||
}
|
||||
break;
|
||||
|
||||
case MULTI_LINE_END:
|
||||
// 已经读取了闭合的 "*/"
|
||||
return new Token(TokenType.COMMENT, literal.toString(), line, col);
|
||||
}
|
||||
}
|
||||
|
||||
return new Token(TokenType.COMMENT, sb.toString(), line, col);
|
||||
// 如果未终止的多行注释,抛出异常
|
||||
if (currentState == State.MULTI_LINE) {
|
||||
throw new LexicalException("未终止的多行注释", line, col);
|
||||
}
|
||||
|
||||
// 在正常情况下返回生成的注释 Token
|
||||
return new Token(TokenType.COMMENT, literal.toString(), line, col);
|
||||
}
|
||||
|
||||
// 定义状态
|
||||
private enum State {
|
||||
INITIAL, // 初始状态
|
||||
SINGLE_LINE, // 单行注释状态
|
||||
MULTI_LINE, // 多行注释状态
|
||||
MULTI_LINE_END // 多行注释结束状态
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +1,34 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.core.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.core.LexicalException;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenFactory;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 标识符扫描器:处理标识符的识别,如变量名、函数名等。
|
||||
* <p>
|
||||
* 识别规则如下:
|
||||
* {@code IdentifierTokenScanner} —— 标识符扫描器,负责识别源代码中的标识符(如变量名、函数名等)。
|
||||
*
|
||||
* <p>标识符的识别遵循以下规则:</p>
|
||||
* <ul>
|
||||
* <li>必须以字母或下划线(_)开头</li>
|
||||
* <li>后续字符可以是字母、数字或下划线</li>
|
||||
* <li>标识符必须以字母(A-Z,a-z)或下划线(_)开头。</li>
|
||||
* <li>标识符的后续字符可以是字母、数字(0-9)或下划线。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 扫描完成后会调用 {@link TokenFactory} 自动判断是否为关键字,
|
||||
* 并返回对应类型的 {@link Token}。
|
||||
*
|
||||
* <p>在扫描过程中,标识符会被处理为一个 {@link Token} 对象。如果该标识符是一个关键字,
|
||||
* 扫描器会通过 {@link TokenFactory} 自动识别并返回相应的 {@link TokenType}。</p>
|
||||
*
|
||||
* <p>本扫描器实现了一个有限状态机(FSM),它能够在不同状态之间转换,确保标识符的正确识别。</p>
|
||||
*/
|
||||
public class IdentifierTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>如果字符为字母或下划线,则认为是标识符的起始。</p>
|
||||
* 判断当前字符是否可以作为标识符的起始字符。
|
||||
* <p>如果字符为字母或下划线,则认为是标识符的开始。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果是标识符起始字符,则返回 true
|
||||
* @return 如果字符是标识符的起始字符,则返回 {@code true};否则返回 {@code false}。
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
@ -32,17 +36,57 @@ public class IdentifierTokenScanner extends AbstractTokenScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行标识符的扫描逻辑。
|
||||
* <p>连续读取满足标识符规则的字符序列,交由 {@code TokenFactory} 创建对应的 Token。</p>
|
||||
* 执行标识符扫描。
|
||||
* <p>使用状态机模式扫描标识符。首先从初始状态开始,读取标识符的起始字符(字母或下划线)。
|
||||
* 然后,进入标识符状态,继续读取标识符字符(字母、数字或下划线)。一旦遇到不符合标识符规则的字符,
|
||||
* 标识符扫描结束,返回一个 {@link Token}。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 标识符或关键字类型的 Token
|
||||
* @param ctx 词法上下文,用于获取字符流
|
||||
* @param line 当前行号(1 基)
|
||||
* @param col 当前列号(1 基)
|
||||
* @return 返回一个包含标识符或关键字的 {@link Token} 对象。
|
||||
* @throws LexicalException 如果标识符以非法字符(如点号)开头,则抛出异常
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
String lexeme = readWhile(ctx, ch -> Character.isLetterOrDigit(ch) || ch == '_');
|
||||
return TokenFactory.create(lexeme, line, col);
|
||||
StringBuilder lexeme = new StringBuilder(); // 用于构建标识符的字符串
|
||||
State currentState = State.INITIAL; // 初始状态
|
||||
|
||||
// 遍历字符流,直到遇到不合法的字符或流结束
|
||||
while (!ctx.isAtEnd()) {
|
||||
char currentChar = ctx.peek(); // 获取当前字符
|
||||
switch (currentState) {
|
||||
case INITIAL:
|
||||
// 初始状态,标识符开始
|
||||
if (Character.isLetter(currentChar) || currentChar == '_') {
|
||||
lexeme.append(ctx.advance()); // 接受当前字符
|
||||
currentState = State.IDENTIFIER; // 进入标识符状态
|
||||
} else {
|
||||
return null; // 当前字符不符合标识符的规则,返回 null
|
||||
}
|
||||
break;
|
||||
|
||||
case IDENTIFIER:
|
||||
// 标识符状态,继续读取合法标识符字符
|
||||
if (Character.isLetterOrDigit(currentChar) || currentChar == '_') {
|
||||
lexeme.append(ctx.advance()); // 继续接受合法字符
|
||||
} else {
|
||||
// 当前字符不符合标识符的规则,标识符结束,返回 token
|
||||
return TokenFactory.create(lexeme.toString(), line, col);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果字符流结束,返回标识符 token
|
||||
return TokenFactory.create(lexeme.toString(), line, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举类型表示标识符扫描的状态。
|
||||
*/
|
||||
private enum State {
|
||||
INITIAL, // 初始状态,等待标识符的开始
|
||||
IDENTIFIER // 标识符状态,继续读取标识符字符
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,19 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
/**
|
||||
* 换行符扫描器:将源代码中的换行符(\n)识别为 {@code NEWLINE} 类型的 Token。
|
||||
* <p>
|
||||
* 通常用于记录行的分界,辅助语法分析阶段进行行敏感的判断或保持结构清晰。
|
||||
* 用于记录行的分界,辅助语法分析阶段进行行敏感的判断或保持结构清晰。
|
||||
*/
|
||||
public class NewlineTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
// 定义状态枚举
|
||||
private enum State {
|
||||
INITIAL,
|
||||
NEWLINE
|
||||
}
|
||||
|
||||
// 当前状态
|
||||
private State currentState = State.INITIAL;
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当字符为换行符(\n)时返回 true。</p>
|
||||
@ -21,7 +30,8 @@ public class NewlineTokenScanner extends AbstractTokenScanner {
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return c == '\n';
|
||||
// 只有当处于 INITIAL 状态,并且遇到换行符时,才可以处理
|
||||
return currentState == State.INITIAL && c == '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,7 +45,16 @@ public class NewlineTokenScanner extends AbstractTokenScanner {
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
// 状态转换为 NEWLINE
|
||||
currentState = State.NEWLINE;
|
||||
|
||||
// 执行换行符扫描,生成 token
|
||||
ctx.advance();
|
||||
return new Token(TokenType.NEWLINE, "\n", line, col);
|
||||
Token newlineToken = new Token(TokenType.NEWLINE, "\n", line, col);
|
||||
|
||||
// 扫描完成后,恢复状态为 INITIAL
|
||||
currentState = State.INITIAL;
|
||||
|
||||
return newlineToken;
|
||||
}
|
||||
}
|
||||
@ -1,39 +1,65 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.core.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.core.LexicalException;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 数字扫描器:识别整数、小数以及带有<strong>类型后缀</strong>的数字字面量。
|
||||
* NumberTokenScanner —— 基于有限状态机(FSM)的数字字面量解析器。
|
||||
* <p>
|
||||
* 支持的格式示例:
|
||||
* 该扫描器负责将源码中的数字字符串切分为 NUMBER_LITERAL token,当前支持:
|
||||
* <ol>
|
||||
* <li>十进制整数(如 0,42,123456)</li>
|
||||
* <li>十进制小数(如 3.14,0.5)</li>
|
||||
* <li>单字符类型后缀(如 2.0f,255B,合法集合见 SUFFIX_CHARS)</li>
|
||||
* </ol>
|
||||
*
|
||||
* 如果后续需要支持科学计数法、下划线分隔符、不同进制等,只需扩展现有状态机的转移规则。
|
||||
*
|
||||
* <pre>
|
||||
* 状态机简述:
|
||||
* INT_PART --'.'--> DEC_POINT
|
||||
* | |
|
||||
* | v
|
||||
* else------------> END
|
||||
* |
|
||||
* v
|
||||
* DEC_POINT --digit--> FRAC_PART
|
||||
* </pre>
|
||||
* 状态说明:
|
||||
* <ul>
|
||||
* <li>整数:123、0、45678</li>
|
||||
* <li>小数:3.14、0.5、12.0</li>
|
||||
* <li>带类型后缀:2.0f、42L、7s、255B</li>
|
||||
* <li>INT_PART :读取整数部分,遇到 '.' 进入 DEC_POINT,否则结束。</li>
|
||||
* <li>DEC_POINT :已读到小数点,必须下一个字符是数字,否则报错。</li>
|
||||
* <li>FRAC_PART :读取小数部分,遇非法字符则结束主体。</li>
|
||||
* <li>END :主体扫描结束,进入后缀/尾随字符判定。</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 语法允许在数字 (整数或小数) 末尾添加以下<strong>单字符后缀</strong>来显式指定常量类型:
|
||||
* <pre>b | s | l | f | d // 分别对应 byte、short、long、float、double
|
||||
* B | S | L | F | D // 同上,大小写皆可</pre>
|
||||
* 生成的 Token 类型始终为 {@code NUMBER_LITERAL},词法单元将携带完整的文本(含后缀,若存在)。
|
||||
*
|
||||
* 错误处理策略:
|
||||
* <ol>
|
||||
* <li>数字后跟未知字母(如 42X)—— 抛出 LexicalException</li>
|
||||
* <li>数字与合法后缀间有空白(如 3 L)—— 抛出 LexicalException</li>
|
||||
* <li>小数点后缺失数字(如 1.)—— 抛出 LexicalException</li>
|
||||
* </ol>
|
||||
*
|
||||
* 支持的单字符类型后缀包括:b, s, l, f, d 及其大写形式。若需支持多字符后缀,可将该集合扩展为 Set<String>。
|
||||
*/
|
||||
public class NumberTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 可选类型后缀字符集合 (大小写均可)。
|
||||
* 与 {@code ExpressionBuilder} 内的后缀解析逻辑保持一致。
|
||||
* 支持的单字符类型后缀集合。
|
||||
* 包含:b, s, l, f, d 及其大写形式。
|
||||
* 对于多字符后缀,可扩展为 Set<String> 并在扫描尾部做贪婪匹配。
|
||||
*/
|
||||
private static final String SUFFIX_CHARS = "bslfdBSLFD";
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当字符为数字时,表示可能是数字字面量的起始。</p>
|
||||
* 判断是否由该扫描器处理。
|
||||
* 仅当首字符为数字时,NumberTokenScanner 介入处理。
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果为数字字符,则返回 true
|
||||
* @param c 当前待判断字符
|
||||
* @param ctx 当前 LexerContext
|
||||
* @return 如果为数字返回 true,否则返回 false
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
@ -41,52 +67,88 @@ public class NumberTokenScanner extends AbstractTokenScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数字扫描逻辑。
|
||||
* <ol>
|
||||
* <li>连续读取数字字符,允许出现<strong>一个</strong>小数点,用于识别整数或小数。</li>
|
||||
* <li>读取完主体后,<strong>一次性</strong>检查下一个字符,若属于合法类型后缀则吸收。</li>
|
||||
* </ol>
|
||||
* 这样可以保证诸如 {@code 2.0f} 被视为一个整体的 {@code NUMBER_LITERAL},
|
||||
* 而不是拆分成 "2.0" 与 "f" 两个 Token。
|
||||
* 按照有限状态机读取完整数字字面量,并对尾随字符进行合法性校验。
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 表示数字字面量的 Token
|
||||
* @param ctx 当前 LexerContext
|
||||
* @param line 源码起始行号(1 基)
|
||||
* @param col 源码起始列号(1 基)
|
||||
* @return NUMBER_LITERAL 类型的 Token
|
||||
* @throws LexicalException 如果遇到非法格式或未受支持的尾随字符
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean hasDot = false; // 标识是否已经遇到过小数点
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) throws LexicalException {
|
||||
StringBuilder literal = new StringBuilder();
|
||||
State state = State.INT_PART;
|
||||
|
||||
/*
|
||||
* 1️⃣ 扫描整数或小数主体
|
||||
* 允许出现一个小数点,其余必须是数字。
|
||||
*/
|
||||
while (!ctx.isAtEnd()) {
|
||||
char c = ctx.peek();
|
||||
if (c == '.' && !hasDot) {
|
||||
hasDot = true;
|
||||
sb.append(ctx.advance());
|
||||
} else if (Character.isDigit(c)) {
|
||||
sb.append(ctx.advance());
|
||||
/* ───── 1. 主体扫描 —— 整数 / 小数 ───── */
|
||||
mainLoop:
|
||||
while (!ctx.isAtEnd() && state != State.END) {
|
||||
char ch = ctx.peek();
|
||||
switch (state) {
|
||||
/* 整数部分 */
|
||||
case INT_PART:
|
||||
if (Character.isDigit(ch)) {
|
||||
literal.append(ctx.advance());
|
||||
} else if (ch == '.') {
|
||||
state = State.DEC_POINT;
|
||||
literal.append(ctx.advance());
|
||||
} else {
|
||||
break; // 遇到非数字/第二个点 => 主体结束
|
||||
state = State.END;
|
||||
}
|
||||
break;
|
||||
|
||||
/* 已读到小数点,下一字符必须是数字 */
|
||||
case DEC_POINT:
|
||||
if (Character.isDigit(ch)) {
|
||||
state = State.FRAC_PART;
|
||||
literal.append(ctx.advance());
|
||||
} else {
|
||||
throw new LexicalException("小数点后必须跟数字", line, col);
|
||||
}
|
||||
break;
|
||||
|
||||
/* 小数部分 */
|
||||
case FRAC_PART:
|
||||
if (Character.isDigit(ch)) {
|
||||
literal.append(ctx.advance());
|
||||
} else {
|
||||
state = State.END;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break mainLoop;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 2️⃣ 可选类型后缀
|
||||
* 如果下一字符是合法后缀字母,则一起纳入当前 Token。
|
||||
*/
|
||||
/* ───── 2. 后缀及非法尾随字符检查 ───── */
|
||||
if (!ctx.isAtEnd()) {
|
||||
char suffix = ctx.peek();
|
||||
if (SUFFIX_CHARS.indexOf(suffix) >= 0) {
|
||||
sb.append(ctx.advance());
|
||||
char next = ctx.peek();
|
||||
|
||||
/* 2-A. 合法单字符后缀(紧邻数字,不允许空格) */
|
||||
if (SUFFIX_CHARS.indexOf(next) >= 0) {
|
||||
literal.append(ctx.advance());
|
||||
}
|
||||
/* 2-B. 未知紧邻字母后缀 —— 报错 */
|
||||
else if (Character.isLetter(next)) {
|
||||
throw new LexicalException("未知的数字类型后缀 '" + next + "'", line, col);
|
||||
}
|
||||
/* 其余情况交由外层扫描器处理(包括空白及其它符号) */
|
||||
}
|
||||
|
||||
// 构造并返回 NUMBER_LITERAL Token,文本内容形如 "123", "3.14f" 等。
|
||||
return new Token(TokenType.NUMBER_LITERAL, sb.toString(), line, col);
|
||||
/* ───── 3. 生成并返回 Token ───── */
|
||||
return new Token(TokenType.NUMBER_LITERAL, literal.toString(), line, col);
|
||||
}
|
||||
|
||||
/** FSM 内部状态定义 */
|
||||
private enum State {
|
||||
/** 整数部分 */
|
||||
INT_PART,
|
||||
/** 已读到小数点,但还未读到第一位小数数字 */
|
||||
DEC_POINT,
|
||||
/** 小数部分 */
|
||||
FRAC_PART,
|
||||
/** 主体结束,准备处理后缀或交还控制权 */
|
||||
END
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,8 +45,11 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
char c = ctx.advance();
|
||||
String lexeme;
|
||||
TokenType type;
|
||||
String lexeme = String.valueOf(c);
|
||||
TokenType type = TokenType.UNKNOWN;
|
||||
|
||||
// 当前状态
|
||||
State currentState = State.OPERATOR;
|
||||
|
||||
switch (c) {
|
||||
case '=':
|
||||
@ -54,7 +57,6 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
lexeme = "==";
|
||||
type = TokenType.DOUBLE_EQUALS;
|
||||
} else {
|
||||
lexeme = "=";
|
||||
type = TokenType.EQUALS;
|
||||
}
|
||||
break;
|
||||
@ -64,7 +66,6 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
lexeme = "!=";
|
||||
type = TokenType.NOT_EQUALS;
|
||||
} else {
|
||||
lexeme = "!";
|
||||
type = TokenType.NOT;
|
||||
}
|
||||
break;
|
||||
@ -74,7 +75,6 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
lexeme = ">=";
|
||||
type = TokenType.GREATER_EQUAL;
|
||||
} else {
|
||||
lexeme = ">";
|
||||
type = TokenType.GREATER_THAN;
|
||||
}
|
||||
break;
|
||||
@ -84,13 +84,11 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
lexeme = "<=";
|
||||
type = TokenType.LESS_EQUAL;
|
||||
} else {
|
||||
lexeme = "<";
|
||||
type = TokenType.LESS_THAN;
|
||||
}
|
||||
break;
|
||||
|
||||
case '%':
|
||||
lexeme = "%";
|
||||
type = TokenType.MODULO;
|
||||
break;
|
||||
|
||||
@ -98,9 +96,6 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
if (ctx.match('&')) {
|
||||
lexeme = "&&";
|
||||
type = TokenType.AND;
|
||||
} else {
|
||||
lexeme = "&";
|
||||
type = TokenType.UNKNOWN;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -108,17 +103,26 @@ public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
if (ctx.match('|')) {
|
||||
lexeme = "||";
|
||||
type = TokenType.OR;
|
||||
} else {
|
||||
lexeme = "|";
|
||||
type = TokenType.UNKNOWN;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
lexeme = String.valueOf(c);
|
||||
type = TokenType.UNKNOWN;
|
||||
currentState = State.UNKNOWN;
|
||||
break;
|
||||
}
|
||||
|
||||
// 执行完扫描后,重置状态为初始状态
|
||||
if (currentState != State.UNKNOWN) {
|
||||
currentState = State.START;
|
||||
}
|
||||
|
||||
return new Token(type, lexeme, line, col);
|
||||
}
|
||||
|
||||
// 定义状态枚举
|
||||
private enum State {
|
||||
START, // 初始状态
|
||||
OPERATOR, // 当前字符是运算符的一部分
|
||||
UNKNOWN // 无法识别的状态
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public class StringTokenScanner extends AbstractTokenScanner {
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return c == '"';
|
||||
return c == '"'; // 只处理字符串开始符号
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,19 +45,51 @@ public class StringTokenScanner extends AbstractTokenScanner {
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(ctx.advance()); // 起始双引号
|
||||
// 当前状态
|
||||
State currentState = State.START; // 初始状态为开始扫描字符串
|
||||
|
||||
// 开始扫描字符串
|
||||
while (!ctx.isAtEnd()) {
|
||||
char c = ctx.advance();
|
||||
sb.append(c);
|
||||
|
||||
if (c == '\\') {
|
||||
sb.append(ctx.advance()); // 添加转义字符后的实际字符
|
||||
} else if (c == '"') {
|
||||
switch (currentState) {
|
||||
case START:
|
||||
// 开始状态,遇到第一个双引号
|
||||
currentState = State.STRING;
|
||||
break;
|
||||
|
||||
case STRING:
|
||||
if (c == '\\') {
|
||||
// 遇到转义字符,进入 ESCAPE 状态
|
||||
currentState = State.ESCAPE;
|
||||
} else if (c == '"') {
|
||||
// 遇到结束的双引号,结束扫描
|
||||
currentState = State.END;
|
||||
}
|
||||
break;
|
||||
|
||||
case ESCAPE:
|
||||
// 在转义状态下,处理转义字符
|
||||
sb.append(ctx.advance()); // 加入转义字符后的字符
|
||||
currentState = State.STRING; // 返回字符串状态
|
||||
break;
|
||||
|
||||
case END:
|
||||
// 结束状态,字符串扫描完成
|
||||
return new Token(TokenType.STRING_LITERAL, sb.toString(), line, col);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有结束的双引号,则表示错误,或者未正确处理
|
||||
return new Token(TokenType.STRING_LITERAL, sb.toString(), line, col);
|
||||
}
|
||||
|
||||
// 定义状态枚举
|
||||
private enum State {
|
||||
START, // 开始状态,寻找字符串的开始双引号
|
||||
STRING, // 字符串扫描状态,处理字符串中的字符
|
||||
ESCAPE, // 处理转义字符状态
|
||||
END // 字符串结束状态
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,5 +67,7 @@ public record CallExpressionNode(
|
||||
*
|
||||
* @return 当前表达式所在的文件名。
|
||||
*/
|
||||
public String file() { return file; }
|
||||
public String file() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package org.jcnc.snow.compiler.parser.context;
|
||||
|
||||
/**
|
||||
* 表示在语法分析过程中,必须出现的 Token 缺失时抛出的异常。
|
||||
* <p>
|
||||
* 当分析器检测到输入流中缺少某个预期 Token 时,会抛出此异常,以便准确地指明语法错误位置。
|
||||
* 该异常包含了缺失 Token 的名称以及发生缺失的位置(行号和列号),便于错误定位和后续处理。
|
||||
* </p>
|
||||
*/
|
||||
public final class MissingToken extends ParseException {
|
||||
|
||||
/**
|
||||
* 构造一个表示缺失 Token 的异常。
|
||||
*
|
||||
* @param expected 预期但未出现的 Token 名称
|
||||
* @param line 发生异常的行号
|
||||
* @param column 发生异常的列号
|
||||
*/
|
||||
public MissingToken(String expected, int line, int column) {
|
||||
super("缺失 Token: " + expected, line, column);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package org.jcnc.snow.compiler.parser.context;
|
||||
|
||||
/**
|
||||
* 语法错误的数据传输对象(DTO)。
|
||||
* <p>
|
||||
* 用于收集和展示语法分析过程中检测到的错误信息,便于错误定位和报告。
|
||||
* 包含出错文件、行号、列号和具体错误信息等字段。
|
||||
* </p>
|
||||
*/
|
||||
public class ParseError {
|
||||
|
||||
/** 出错的文件名 */
|
||||
private final String file;
|
||||
/** 出错的行号 */
|
||||
private final int line;
|
||||
/** 出错的列号 */
|
||||
private final int column;
|
||||
/** 错误信息描述 */
|
||||
private final String message;
|
||||
|
||||
/**
|
||||
* 构造一个语法错误数据对象。
|
||||
*
|
||||
* @param file 出错文件名
|
||||
* @param line 出错行号
|
||||
* @param column 出错列号
|
||||
* @param message 错误信息描述
|
||||
*/
|
||||
public ParseError(String file, int line, int column, String message) {
|
||||
this.file = file;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回该错误对象的字符串表示。
|
||||
*
|
||||
* @return 格式化后的错误描述字符串
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return file + ": 行 " + line + ", 列 " + column + ": " + message;
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,75 @@
|
||||
package org.jcnc.snow.compiler.parser.context;
|
||||
|
||||
/**
|
||||
* {@code ParseException} 表示语法分析阶段发生的错误。
|
||||
* 语法分析阶段所有错误的基类。
|
||||
* <p>
|
||||
* 当语法分析器遇到非法的语法结构或无法继续处理的标记序列时,
|
||||
* 应抛出该异常以中断当前解析流程,并向调用方报告错误信息。
|
||||
* 本异常作为语法分析相关错误的统一父类,屏蔽了堆栈信息,确保在命令行界面(CLI)输出时只占用一行,方便用户快速定位问题。
|
||||
* 通过 {@code permits} 关键字,限定了可被继承的异常类型,增强类型安全性。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 该异常通常由 {@code ParserContext} 或各类语法规则处理器主动抛出,
|
||||
* 用于提示编译器前端或 IDE 系统进行错误提示与恢复。
|
||||
* 该异常携带错误发生的行号、列号和具体原因信息,用于语法错误的精确报告和输出展示。
|
||||
* </p>
|
||||
*/
|
||||
public class ParseException extends RuntimeException {
|
||||
public sealed class ParseException extends RuntimeException
|
||||
permits MissingToken, UnexpectedToken, UnsupportedFeature {
|
||||
|
||||
/** 出错行号(从 1 开始) */
|
||||
private final int line;
|
||||
/** 出错列号(从 1 开始) */
|
||||
private final int column;
|
||||
/** 错误原因描述 */
|
||||
private final String reason;
|
||||
|
||||
/**
|
||||
* 构造一个带有错误描述信息的解析异常实例。
|
||||
* 构造语法分析异常。
|
||||
*
|
||||
* @param message 错误描述文本,用于指明具体的语法错误原因
|
||||
* @param reason 错误原因描述
|
||||
* @param line 出错行号(从 1 开始)
|
||||
* @param column 出错列号(从 1 开始)
|
||||
*/
|
||||
public ParseException(String message) {
|
||||
super(message);
|
||||
public ParseException(String reason, int line, int column) {
|
||||
// 禁用 cause / suppression / stackTrace,确保 CLI 输出简洁
|
||||
super(reason, null, false, false);
|
||||
this.reason = reason;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用堆栈信息的生成,保证异常始终为单行输出。
|
||||
*
|
||||
* @return 当前异常对象自身
|
||||
*/
|
||||
@Override
|
||||
public synchronized Throwable fillInStackTrace() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取出错行号(从 1 开始)。
|
||||
*
|
||||
* @return 行号
|
||||
*/
|
||||
public int getLine() {
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取出错列号(从 1 开始)。
|
||||
*
|
||||
* @return 列号
|
||||
*/
|
||||
public int getColumn() {
|
||||
return column;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误原因描述。
|
||||
*
|
||||
* @return 错误原因
|
||||
*/
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,10 +15,14 @@ import java.util.List;
|
||||
*/
|
||||
public class ParserContext {
|
||||
|
||||
/** 当前语法分析所使用的 Token 流 */
|
||||
/**
|
||||
* 当前语法分析所使用的 Token 流
|
||||
*/
|
||||
private final TokenStream tokens;
|
||||
|
||||
/** 当前语法分析所使用的资源文件名 */
|
||||
/**
|
||||
* 当前语法分析所使用的资源文件名
|
||||
*/
|
||||
private final String sourceName;
|
||||
|
||||
|
||||
|
||||
@ -6,37 +6,49 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@code TokenStream} 封装了一个 Token 列表并维护当前解析位置,
|
||||
* 是语法分析器读取词法单元的核心工具类。
|
||||
* {@code TokenStream} 封装了 Token 序列并维护当前解析位置,是语法分析器读取词法单元的核心工具类。
|
||||
* <p>
|
||||
* 提供前瞻(peek)、消费(next)、匹配(match)、断言(expect)等常用操作,
|
||||
* 支持前向查看和异常处理,适用于递归下降解析等常见语法构建策略。
|
||||
* 该类提供前瞻(peek)、消费(next)、匹配(match)、断言(expect)等常用操作,
|
||||
* 支持前向查看和异常处理,适用于递归下降等常见语法解析策略。
|
||||
* 设计上自动跳过注释(COMMENT)token,并对越界情况提供自动构造的 EOF(文件结束)token,
|
||||
* 有效提升语法处理的健壮性与易用性。
|
||||
* </p>
|
||||
*/
|
||||
public class TokenStream {
|
||||
|
||||
/** 源 Token 列表 */
|
||||
/**
|
||||
* 源 Token 列表
|
||||
*/
|
||||
private final List<Token> tokens;
|
||||
|
||||
/** 当前解析位置索引 */
|
||||
/**
|
||||
* 当前解析位置索引
|
||||
*/
|
||||
private int pos = 0;
|
||||
|
||||
/**
|
||||
* 使用 Token 列表构造 TokenStream。
|
||||
*
|
||||
* @param tokens 由词法分析器产生的 Token 集合
|
||||
* @param tokens 词法分析器输出的 Token 集合
|
||||
* @throws NullPointerException 如果 tokens 为 null
|
||||
*/
|
||||
public TokenStream(List<Token> tokens) {
|
||||
if (tokens == null) {
|
||||
throw new NullPointerException("Token 列表不能为空");
|
||||
}
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向前查看指定偏移量处的 Token(不移动位置)。
|
||||
* 向前查看指定偏移量处的 Token(不移动当前位置)。
|
||||
* 在 {@code offset == 0} 时自动跳过所有连续的注释(COMMENT)token。
|
||||
*
|
||||
* @param offset 相对当前位置的偏移量(0 表示当前)
|
||||
* @param offset 相对当前位置的偏移量(0 表示当前位置 token)
|
||||
* @return 指定位置的 Token;若越界则返回自动构造的 EOF Token
|
||||
*/
|
||||
public Token peek(int offset) {
|
||||
if (offset == 0) {
|
||||
skipTrivia();
|
||||
}
|
||||
int idx = pos + offset;
|
||||
if (idx >= tokens.size()) {
|
||||
return Token.eof(tokens.size() + 1);
|
||||
@ -45,30 +57,32 @@ public class TokenStream {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看当前位置的 Token,等效于 {@code peek(0)}。
|
||||
* 查看当前位置的有效 Token(已跳过注释)。
|
||||
*
|
||||
* @return 当前 Token
|
||||
* @return 当前 Token,等效于 {@code peek(0)}
|
||||
*/
|
||||
public Token peek() {
|
||||
skipTrivia();
|
||||
return peek(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 消费当前位置的 Token 并返回,位置前移。
|
||||
* 消费当前位置的有效 Token 并前移指针,自动跳过注释 token。
|
||||
*
|
||||
* @return 当前 Token
|
||||
* @return 被消费的有效 Token
|
||||
*/
|
||||
public Token next() {
|
||||
Token t = peek();
|
||||
pos++;
|
||||
skipTrivia();
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配当前 Token 的词素与指定字符串,若匹配则消费。
|
||||
* 若当前 Token 的词素等于指定字符串,则消费该 Token 并前移,否则不变。
|
||||
*
|
||||
* @param lexeme 待匹配词素
|
||||
* @return 若成功匹配则返回 true
|
||||
* @param lexeme 目标词素字符串
|
||||
* @return 匹配成功返回 true,否则返回 false
|
||||
*/
|
||||
public boolean match(String lexeme) {
|
||||
if (peek().getLexeme().equals(lexeme)) {
|
||||
@ -79,49 +93,60 @@ public class TokenStream {
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言当前 Token 的词素与指定值相符,否则抛出 {@link ParseException}。
|
||||
* 断言当前位置 Token 的词素等于指定值,否则抛出 {@link ParseException}。
|
||||
* 匹配成功时消费该 Token 并前移。
|
||||
*
|
||||
* @param lexeme 期望的词素值
|
||||
* @param lexeme 期望的词素字符串
|
||||
* @return 匹配成功的 Token
|
||||
* @throws ParseException 若词素不符
|
||||
* @throws ParseException 若词素不匹配
|
||||
*/
|
||||
public Token expect(String lexeme) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言当前 Token 类型为指定类型,否则抛出 {@link ParseException}。
|
||||
* 断言当前位置 Token 类型为指定类型,否则抛出 {@link ParseException}。
|
||||
* 匹配成功时消费该 Token 并前移。
|
||||
*
|
||||
* @param type 期望的 Token 类型
|
||||
* @return 匹配成功的 Token
|
||||
* @throws ParseException 若类型不匹配
|
||||
* @throws ParseException 若类型不符
|
||||
*/
|
||||
public Token expectType(TokenType type) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否“已经”到达 EOF。
|
||||
* 判断是否已到达文件末尾(EOF)。
|
||||
*
|
||||
* @return 若当前位置 Token 为 EOF,则返回 true,否则 false
|
||||
* @return 若当前位置 Token 为 EOF,则返回 true;否则返回 false
|
||||
*/
|
||||
public boolean isAtEnd() {
|
||||
return peek().getType() != TokenType.EOF;
|
||||
return peek().getType() == TokenType.EOF;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 跳过所有连续的注释(COMMENT)token,使解析器总是定位在第一个有效 Token 上。
|
||||
*/
|
||||
private void skipTrivia() {
|
||||
while (pos < tokens.size()
|
||||
&& tokens.get(pos).getType() == TokenType.COMMENT) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jcnc.snow.compiler.parser.context;
|
||||
|
||||
/**
|
||||
* 表示在语法分析过程中遇到意料之外或无法识别的 Token 时抛出的异常。
|
||||
* <p>
|
||||
* 当分析器检测到实际遇到的 Token 不符合语法规则,或与预期类型不符时会抛出本异常,便于错误定位和报告。
|
||||
* </p>
|
||||
*/
|
||||
public final class UnexpectedToken extends ParseException {
|
||||
|
||||
/**
|
||||
* 构造一个“意外的 Token”异常。
|
||||
*
|
||||
* @param actual 实际遇到的 Token 描述
|
||||
* @param line 发生异常的行号
|
||||
* @param column 发生异常的列号
|
||||
*/
|
||||
public UnexpectedToken(String actual, int line, int column) {
|
||||
super("意外的 Token: " + actual, line, column);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jcnc.snow.compiler.parser.context;
|
||||
|
||||
/**
|
||||
* 表示在语法分析过程中使用了尚未支持的语法或语言特性时抛出的异常。
|
||||
* <p>
|
||||
* 当用户使用了当前编译器实现尚不支持的语法、关键字或特性时,语法分析器将抛出此异常,用于清晰提示和错误报告。
|
||||
* </p>
|
||||
*/
|
||||
public final class UnsupportedFeature extends ParseException {
|
||||
|
||||
/**
|
||||
* 构造一个“暂未支持的语法/特性”异常。
|
||||
*
|
||||
* @param feature 未被支持的语法或特性描述
|
||||
* @param line 发生异常的行号
|
||||
* @param column 发生异常的列号
|
||||
*/
|
||||
public UnsupportedFeature(String feature, int line, int column) {
|
||||
super("暂未支持的语法/特性: " + feature, line, column);
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,47 @@
|
||||
package org.jcnc.snow.compiler.parser.core;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
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.factory.TopLevelParserFactory;
|
||||
import org.jcnc.snow.compiler.parser.ast.base.Node;
|
||||
import org.jcnc.snow.compiler.parser.base.TopLevelParser;
|
||||
import org.jcnc.snow.compiler.parser.context.*;
|
||||
import org.jcnc.snow.compiler.parser.factory.TopLevelParserFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* 语法解析引擎(ParserEngine)。
|
||||
* <p>
|
||||
* 负责驱动顶层语法解析,并统一处理、收集所有语法异常,防止死循环,确保整体解析流程的健壮性与鲁棒性。
|
||||
* 支持基于同步点的错误恢复,适用于命令式和脚本式语法环境。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 本引擎以异常收集为核心设计,所有捕获到的 {@link ParseException} 会被聚合,在分析结束后一次性统一抛出。
|
||||
* 同时,在解析出错时会通过同步(synchronize)机制,跳过错误片段以恢复到有效解析点,避免因指针停滞导致的死循环。
|
||||
* </p>
|
||||
*/
|
||||
public record ParserEngine(ParserContext ctx) {
|
||||
|
||||
/**
|
||||
* 解析整个 TokenStream,返回顶层 AST 节点列表。
|
||||
* <p>
|
||||
* 过程中如遇语法异常,均会被收集并在最后聚合抛出,避免单点失败导致整个解析中断。
|
||||
* </p>
|
||||
*
|
||||
* @return 解析所得的顶层 AST 节点列表
|
||||
* @throws UnexpectedToken 当存在语法错误时,统一抛出聚合异常
|
||||
*/
|
||||
public List<Node> parse() {
|
||||
List<Node> nodes = new ArrayList<>();
|
||||
List<String> errs = new ArrayList<>();
|
||||
TokenStream ts = ctx.getTokens();
|
||||
List<ParseError> errs = new ArrayList<>();
|
||||
|
||||
while (ts.isAtEnd()) {
|
||||
TokenStream ts = ctx.getTokens();
|
||||
String file = ctx.getSourceName();
|
||||
|
||||
// 主循环至 EOF
|
||||
while (!ts.isAtEnd()) {
|
||||
// 跳过空行
|
||||
if (ts.peek().getType() == TokenType.NEWLINE) {
|
||||
ts.next();
|
||||
@ -25,39 +49,48 @@ public record ParserEngine(ParserContext ctx) {
|
||||
}
|
||||
|
||||
TopLevelParser parser = TopLevelParserFactory.get(ts.peek().getLexeme());
|
||||
|
||||
try {
|
||||
nodes.add(parser.parse(ctx));
|
||||
} catch (Exception ex) {
|
||||
errs.add(ex.getMessage());
|
||||
synchronize(ts); // 错误恢复
|
||||
} catch (ParseException ex) {
|
||||
// 收集错误并尝试同步
|
||||
errs.add(new ParseError(file, ex.getLine(), ex.getColumn(), ex.getReason()));
|
||||
synchronize(ts);
|
||||
}
|
||||
}
|
||||
|
||||
/* ───── 统一抛出聚合异常 ───── */
|
||||
if (!errs.isEmpty()) {
|
||||
throw new IllegalStateException("解析过程中检测到 "
|
||||
+ errs.size() + " 处错误:\n - "
|
||||
+ String.join("\n - ", errs));
|
||||
StringJoiner sj = new StringJoiner("\n - ", "", "");
|
||||
errs.forEach(e -> sj.add(e.toString()));
|
||||
|
||||
String msg = "解析过程中检测到 " + errs.size() + " 处错误:\n - " + sj;
|
||||
throw new UnexpectedToken(msg, 0, 0);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误同步:跳到下一行或下一个已注册顶层关键字
|
||||
* 同步:跳过当前行或直到遇到显式注册的顶层关键字。
|
||||
* <p>
|
||||
* 该机制用于语法出错后恢复到下一个可能的有效解析点,防止指针停滞导致死循环或重复抛错。
|
||||
* 同步过程中会优先跳过本行所有未识别 token,并在遇到换行或注册关键字时停止,随后跳过连续空行。
|
||||
* </p>
|
||||
*
|
||||
* @param ts 词法 token 流
|
||||
*/
|
||||
private void synchronize(TokenStream ts) {
|
||||
while (ts.isAtEnd()) {
|
||||
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) {
|
||||
// 清理后续连续空行
|
||||
while (!ts.isAtEnd() && ts.peek().getType() == TokenType.NEWLINE) {
|
||||
ts.next();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -12,44 +13,36 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@code PrattExpressionParser} 是基于 Pratt 算法实现的表达式解析器。
|
||||
* {@code PrattExpressionParser} 基于 Pratt 算法的表达式解析器实现。
|
||||
* <p>
|
||||
* 它支持灵活的运算符优先级控制,结合前缀(PrefixParselet)和中缀(InfixParselet)解析器,
|
||||
* 可高效解析复杂表达式结构,包括:
|
||||
* <ul>
|
||||
* <li>字面量(数字、字符串)</li>
|
||||
* <li>标识符</li>
|
||||
* <li>函数调用、成员访问</li>
|
||||
* <li>带括号的表达式、二元运算符</li>
|
||||
* </ul>
|
||||
* 本类提供统一注册机制和递归表达式解析入口。
|
||||
* 该类通过前缀(PrefixParselet)和中缀(InfixParselet)解析器注册表,
|
||||
* 支持灵活扩展的表达式语法,包括字面量、变量、函数调用、成员访问和各种运算符表达式。
|
||||
* </p>
|
||||
* <p>
|
||||
* 运算符优先级通过枚举控制,结合递归解析实现高效的优先级处理和语法结构解析。
|
||||
* 未注册的语法类型或运算符会统一抛出 {@link UnsupportedFeature} 异常。
|
||||
* </p>
|
||||
*/
|
||||
public class PrattExpressionParser implements ExpressionParser {
|
||||
|
||||
/**
|
||||
* 前缀解析器注册表:按 Token 类型映射
|
||||
*/
|
||||
/** 前缀解析器注册表(按 Token 类型名索引) */
|
||||
private static final Map<String, PrefixParselet> prefixes = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 中缀解析器注册表:按运算符词素映射
|
||||
*/
|
||||
/** 中缀解析器注册表(按运算符词素索引) */
|
||||
private static final Map<String, InfixParselet> infixes = new HashMap<>();
|
||||
|
||||
static {
|
||||
// 注册前缀解析器
|
||||
// 前缀解析器注册
|
||||
prefixes.put(TokenType.NUMBER_LITERAL.name(), new NumberLiteralParselet());
|
||||
prefixes.put(TokenType.IDENTIFIER.name(), new IdentifierParselet());
|
||||
prefixes.put(TokenType.LPAREN.name(), new GroupingParselet());
|
||||
prefixes.put(TokenType.STRING_LITERAL.name(), new StringLiteralParselet());
|
||||
prefixes.put(TokenType.BOOL_LITERAL.name(), new BoolLiteralParselet());
|
||||
|
||||
// 注册一元前缀运算
|
||||
// 一元前缀运算符
|
||||
prefixes.put(TokenType.MINUS.name(), new UnaryOperatorParselet());
|
||||
prefixes.put(TokenType.NOT.name(), new UnaryOperatorParselet());
|
||||
|
||||
// 注册中缀解析器
|
||||
// 中缀解析器注册
|
||||
infixes.put("+", new BinaryOperatorParselet(Precedence.SUM, true));
|
||||
infixes.put("-", new BinaryOperatorParselet(Precedence.SUM, true));
|
||||
infixes.put("*", new BinaryOperatorParselet(Precedence.PRODUCT, true));
|
||||
@ -66,10 +59,11 @@ public class PrattExpressionParser implements ExpressionParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 表达式解析入口,使用最低优先级启动递归解析。
|
||||
* 表达式解析统一入口。
|
||||
* 以最低优先级启动递归下降,适配任意表达式复杂度。
|
||||
*
|
||||
* @param ctx 当前语法解析上下文
|
||||
* @return 表达式抽象语法树节点
|
||||
* @param ctx 当前解析上下文
|
||||
* @return 解析后的表达式 AST 节点
|
||||
*/
|
||||
@Override
|
||||
public ExpressionNode parse(ParserContext ctx) {
|
||||
@ -77,36 +71,52 @@ public class PrattExpressionParser implements ExpressionParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定优先级解析表达式。
|
||||
* 按指定优先级解析表达式。Pratt 算法主循环。
|
||||
* <p>
|
||||
* 先根据当前 Token 类型查找前缀解析器进行初始解析,
|
||||
* 然后根据优先级不断递归处理中缀运算符和右侧表达式。
|
||||
* </p>
|
||||
*
|
||||
* @param ctx 当前上下文
|
||||
* @param prec 当前优先级阈值
|
||||
* @param ctx 解析上下文
|
||||
* @param prec 当前运算符优先级阈值
|
||||
* @return 构建完成的表达式节点
|
||||
* @throws UnsupportedFeature 若遇到未注册的前缀或中缀解析器
|
||||
*/
|
||||
ExpressionNode parseExpression(ParserContext ctx, Precedence prec) {
|
||||
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(),
|
||||
token.getLine(),
|
||||
token.getCol()
|
||||
);
|
||||
}
|
||||
|
||||
ExpressionNode left = prefix.parse(ctx, token);
|
||||
|
||||
while (ctx.getTokens().isAtEnd()
|
||||
while (!ctx.getTokens().isAtEnd()
|
||||
&& prec.ordinal() < nextPrecedence(ctx)) {
|
||||
String lex = ctx.getTokens().peek().getLexeme();
|
||||
InfixParselet infix = infixes.get(lex);
|
||||
if (infix == null) break;
|
||||
if (infix == null) {
|
||||
Token t = ctx.getTokens().peek();
|
||||
throw new UnsupportedFeature(
|
||||
"没有为该运算符注册中缀解析器: '" + lex + "'",
|
||||
t.getLine(),
|
||||
t.getCol()
|
||||
);
|
||||
}
|
||||
left = infix.parse(ctx, left);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个中缀解析器的优先级,用于判断是否继续解析。
|
||||
* 获取下一个中缀解析器的优先级(Pratt 算法核心)。
|
||||
*
|
||||
* @param ctx 当前上下文
|
||||
* @return 优先级枚举 ordinal 值;若无解析器则为 -1
|
||||
* @param ctx 当前解析上下文
|
||||
* @return 下一个中缀运算符的优先级序号;若无解析器则为 -1
|
||||
*/
|
||||
private int nextPrecedence(ParserContext ctx) {
|
||||
InfixParselet infix = infixes.get(ctx.getTokens().peek().getLexeme());
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,100 +1,98 @@
|
||||
package org.jcnc.snow.compiler.parser.module;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
import org.jcnc.snow.compiler.parser.ast.FunctionNode;
|
||||
import org.jcnc.snow.compiler.parser.ast.ImportNode;
|
||||
import org.jcnc.snow.compiler.parser.ast.ModuleNode;
|
||||
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.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;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@code ModuleParser} 类负责解析源码中的模块定义结构,属于顶层结构解析器的一种。
|
||||
* {@code ModuleParser} 负责解析源码中的模块结构,是顶层结构解析器实现之一。
|
||||
* <p>
|
||||
* 模块中可包含多个导入语句和函数定义,导入语句可在模块中任意位置出现,
|
||||
* 同时支持空行,空行将被自动忽略,不影响语法结构的正确性。
|
||||
* 模块定义可包含多个导入(import)语句和函数定义(function),
|
||||
* 导入语句可在模块中任意位置出现,且允许模块体中穿插任意数量的空行(空行会被自动忽略,不影响语法结构)。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 典型模块语法结构:
|
||||
* <pre>
|
||||
* module: mymod
|
||||
* import ...
|
||||
* function ...
|
||||
* ...
|
||||
* end module
|
||||
* </pre>
|
||||
* </p>
|
||||
*/
|
||||
public class ModuleParser implements TopLevelParser {
|
||||
|
||||
/**
|
||||
* 解析一个模块定义块,返回构建好的 {@link ModuleNode} 对象。
|
||||
* 解析一个模块定义块,返回完整的 {@link ModuleNode} 语法树节点。
|
||||
* <p>
|
||||
* 本方法的语法流程包括:
|
||||
* 解析过程包括:
|
||||
* <ol>
|
||||
* <li>匹配模块声明开头 {@code module: IDENTIFIER}。</li>
|
||||
* <li>收集模块体中的 import 语句与 function 定义,允许穿插空行。</li>
|
||||
* <li>模块结尾必须为 {@code end module},且后接换行符。</li>
|
||||
* <li>匹配模块声明起始 {@code module: IDENTIFIER}。</li>
|
||||
* <li>收集模块体内所有 import 和 function 语句,允许穿插空行。</li>
|
||||
* <li>匹配模块结束 {@code end module}。</li>
|
||||
* </ol>
|
||||
* 所有语法错误将在解析过程中抛出异常,以便准确反馈问题位置和原因。
|
||||
* 若遇到未识别的语句,将抛出 {@link UnexpectedToken} 异常,定位错误位置和原因。
|
||||
* </p>
|
||||
*
|
||||
* @param ctx 当前解析器上下文,包含词法流、状态信息等。
|
||||
* @return 返回一个 {@link ModuleNode} 实例,表示完整模块的语法结构。
|
||||
* @throws IllegalStateException 当模块体中出现未识别的语句时抛出。
|
||||
* @param ctx 当前解析上下文(包含词法流等状态)
|
||||
* @return 解析得到的 {@link ModuleNode} 实例
|
||||
* @throws UnexpectedToken 当模块体中出现未识别的顶层语句时抛出
|
||||
*/
|
||||
@Override
|
||||
public ModuleNode parse(ParserContext ctx) {
|
||||
// 获取当前上下文中提供的词法流
|
||||
TokenStream ts = ctx.getTokens();
|
||||
|
||||
// 获取当前 token 的行号、列号和文件名
|
||||
int line = ctx.getTokens().peek().getLine();
|
||||
int column = ctx.getTokens().peek().getCol();
|
||||
int line = ts.peek().getLine();
|
||||
int column = ts.peek().getCol();
|
||||
String file = ctx.getSourceName();
|
||||
|
||||
// 期望模块声明以关键字 "module:" 开始
|
||||
ts.expect("module");
|
||||
ts.expect(":");
|
||||
|
||||
// 读取模块名称(要求为标识符类型的词法单元)
|
||||
String name = ts.expectType(TokenType.IDENTIFIER).getLexeme();
|
||||
|
||||
// 模块声明必须以换行符结束
|
||||
ts.expectType(TokenType.NEWLINE);
|
||||
|
||||
// 初始化模块的导入节点列表与函数节点列表
|
||||
List<ImportNode> imports = new ArrayList<>();
|
||||
List<FunctionNode> functions = new ArrayList<>();
|
||||
|
||||
// 创建 import 与 function 的子解析器
|
||||
ImportParser importParser = new ImportParser();
|
||||
FunctionParser funcParser = new FunctionParser();
|
||||
|
||||
// 进入模块主体内容解析循环
|
||||
while (true) {
|
||||
// 跳过所有空行(即连续的 NEWLINE)
|
||||
if (ts.peek().getType() == TokenType.NEWLINE) {
|
||||
ts.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 若遇到 "end",则表明模块定义结束
|
||||
if ("end".equals(ts.peek().getLexeme())) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 根据当前行首关键字决定解析器的选择
|
||||
String lex = ts.peek().getLexeme();
|
||||
if ("import".equals(lex)) {
|
||||
// 调用导入语句解析器,解析多个模块导入节点
|
||||
imports.addAll(importParser.parse(ctx));
|
||||
} else if ("function".equals(lex)) {
|
||||
// 调用函数定义解析器,解析单个函数结构
|
||||
functions.add(funcParser.parse(ctx));
|
||||
} else {
|
||||
// 遇到无法识别的语句开头,抛出异常并提供详细提示
|
||||
throw new IllegalStateException("Unexpected token in module: " + lex);
|
||||
throw new UnexpectedToken(
|
||||
"Unexpected token in module: " + lex,
|
||||
ts.peek().getLine(),
|
||||
ts.peek().getCol()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保模块体以 "end module" 结束
|
||||
ts.expect("end");
|
||||
ts.expect("module");
|
||||
|
||||
// 构建并返回完整的模块语法树节点
|
||||
return new ModuleNode(name, imports, functions, line, column, file);
|
||||
}
|
||||
}
|
||||
@ -2,74 +2,66 @@ package org.jcnc.snow.compiler.parser.statement;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
import org.jcnc.snow.compiler.parser.ast.AssignmentNode;
|
||||
import org.jcnc.snow.compiler.parser.ast.base.ExpressionNode;
|
||||
import org.jcnc.snow.compiler.parser.ast.ExpressionStatementNode;
|
||||
import org.jcnc.snow.compiler.parser.ast.base.ExpressionNode;
|
||||
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;
|
||||
|
||||
/**
|
||||
* {@code ExpressionStatementParser} 负责解析通用表达式语句,包括赋值语句和单一表达式语句。
|
||||
* {@code ExpressionStatementParser} 用于解析通用表达式语句(赋值或普通表达式)。
|
||||
* <p>
|
||||
* 支持的语法结构如下:
|
||||
* 支持以下两种语法结构:
|
||||
* <pre>{@code
|
||||
* x = 1 + 2 // 赋值语句
|
||||
* doSomething() // 函数调用等普通表达式语句
|
||||
* doSomething() // 一般表达式语句
|
||||
* }</pre>
|
||||
* <ul>
|
||||
* <li>若以标识符开头,且后接等号 {@code =},则视为赋值语句,解析为 {@link AssignmentNode}。</li>
|
||||
* <li>以标识符开头且后接 {@code =} 时,解析为 {@link AssignmentNode}。</li>
|
||||
* <li>否则视为普通表达式,解析为 {@link ExpressionStatementNode}。</li>
|
||||
* <li>所有表达式语句必须以换行符 {@code NEWLINE} 结束。</li>
|
||||
* <li>所有表达式语句必须以换行符({@code NEWLINE})结尾。</li>
|
||||
* </ul>
|
||||
* 不允许以关键字或空行作为表达式的起始,若遇到非法开头,将抛出解析异常。
|
||||
* 若语句起始为关键字或空行,将直接抛出异常,防止非法语法进入表达式解析流程。
|
||||
*/
|
||||
public class ExpressionStatementParser implements StatementParser {
|
||||
|
||||
/**
|
||||
* 解析一个表达式语句,根据上下文决定其为赋值或一般表达式。
|
||||
* <p>
|
||||
* 具体逻辑如下:
|
||||
* <ol>
|
||||
* <li>若当前行为标识符后接等号,则作为赋值处理。</li>
|
||||
* <li>否则解析整个表达式作为单独语句。</li>
|
||||
* <li>所有语句都必须以换行符结束。</li>
|
||||
* <li>若表达式以关键字或空行开头,将立即抛出异常,避免非法解析。</li>
|
||||
* </ol>
|
||||
* 解析单行表达式语句,根据上下文判断其为赋值语句或普通表达式语句。
|
||||
*
|
||||
* @param ctx 当前解析上下文,提供词法流与状态信息。
|
||||
* @return 返回 {@link AssignmentNode} 或 {@link ExpressionStatementNode} 表示的语法节点。
|
||||
* @throws IllegalStateException 若表达式起始为关键字或语法非法。
|
||||
* @param ctx 当前解析上下文,提供词法流与环境信息
|
||||
* @return {@link AssignmentNode} 或 {@link ExpressionStatementNode} 语法节点
|
||||
* @throws UnexpectedToken 若遇到非法起始(关键字、空行等)
|
||||
*/
|
||||
@Override
|
||||
public StatementNode parse(ParserContext ctx) {
|
||||
TokenStream ts = ctx.getTokens();
|
||||
|
||||
// 快速检查:若遇空行或关键字开头,不可作为表达式语句
|
||||
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(),
|
||||
ts.peek().getLine(),
|
||||
ts.peek().getCol()
|
||||
);
|
||||
}
|
||||
|
||||
// 获取当前 token 的行号、列号和文件名
|
||||
int line = ctx.getTokens().peek().getLine();
|
||||
int column = ctx.getTokens().peek().getCol();
|
||||
int line = ts.peek().getLine();
|
||||
int column = ts.peek().getCol();
|
||||
String file = ctx.getSourceName();
|
||||
|
||||
// 处理赋值语句:格式为 identifier = expression
|
||||
if (ts.peek().getType() == TokenType.IDENTIFIER
|
||||
&& ts.peek(1).getLexeme().equals("=")) {
|
||||
|
||||
String varName = ts.next().getLexeme(); // 消耗标识符
|
||||
ts.expect("="); // 消耗等号
|
||||
ExpressionNode value = new PrattExpressionParser().parse(ctx); // 解析表达式
|
||||
ts.expectType(TokenType.NEWLINE); // 语句必须以换行符结束
|
||||
return new AssignmentNode(varName, value, line, column, file); // 返回赋值节点
|
||||
// 赋值语句:IDENTIFIER = expr
|
||||
if (ts.peek().getType() == TokenType.IDENTIFIER && "=".equals(ts.peek(1).getLexeme())) {
|
||||
String varName = ts.next().getLexeme();
|
||||
ts.expect("=");
|
||||
ExpressionNode value = new PrattExpressionParser().parse(ctx);
|
||||
ts.expectType(TokenType.NEWLINE);
|
||||
return new AssignmentNode(varName, value, line, column, file);
|
||||
}
|
||||
|
||||
// 处理普通表达式语句,如函数调用、字面量、运算表达式等
|
||||
// 普通表达式语句
|
||||
ExpressionNode expr = new PrattExpressionParser().parse(ctx);
|
||||
ts.expectType(TokenType.NEWLINE); // 语句必须以换行符结束
|
||||
return new ExpressionStatementNode(expr, line, column, file); // 返回表达式语句节点
|
||||
ts.expectType(TokenType.NEWLINE);
|
||||
return new ExpressionStatementNode(expr, line, column, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,90 +3,85 @@ 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;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* {@code FlexibleSectionParser} 是一个通用的语法块解析工具。
|
||||
* {@code FlexibleSectionParser} 是一个通用的区块(Section)解析工具。
|
||||
* <p>
|
||||
* 该工具支持解析由关键字标识的多段结构化区块内容,常用于解析函数、类、模块、循环等语法单元中的命名子结构。
|
||||
* 相比传统硬编码方式,提供更灵活、可组合的解析能力,允许解析器模块动态注册处理逻辑,而非将所有逻辑写死在主流程中。
|
||||
* 支持通过注册表驱动的方式解析具有区块关键字标识的多段结构内容,
|
||||
* 常用于函数、类、模块、循环等语法单元中的命名子结构。
|
||||
* 通过外部注册解析逻辑,支持高度可扩展与复用。
|
||||
* </p>
|
||||
*
|
||||
* <p>典型应用包括:
|
||||
* <p>
|
||||
* 典型用途包括:
|
||||
* <ul>
|
||||
* <li>函数体解析中的 {@code params}、{@code returns}、{@code body} 等部分</li>
|
||||
* <li>模块定义中的 {@code imports}、{@code functions} 等部分</li>
|
||||
* <li>用户自定义 DSL 的可扩展语法结构</li>
|
||||
* <li>可扩展 DSL 的结构化语法区块</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <p>该工具具备以下能力:
|
||||
* <p>主要特性:</p>
|
||||
* <ul>
|
||||
* <li>自动跳过注释与空行</li>
|
||||
* <li>根据区块名称调用外部提供的解析器</li>
|
||||
* <li>支持终止标志(如 {@code end})来退出解析流程</li>
|
||||
* <li>区块入口通过关键字匹配和可选条件判断</li>
|
||||
* <li>解析逻辑由外部以函数式接口方式注册</li>
|
||||
* <li>支持遇到终止关键字(如 {@code end})时自动停止</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class FlexibleSectionParser {
|
||||
|
||||
/**
|
||||
* 启动结构化区块的统一解析流程。
|
||||
* <p>
|
||||
* 每次调用会:
|
||||
* <ol>
|
||||
* <li>从 token 流中跳过空行与注释</li>
|
||||
* <li>依照当前 token 判断是否匹配某个区块</li>
|
||||
* <li>调用对应 {@link SectionDefinition} 执行区块解析逻辑</li>
|
||||
* <li>若遇到 {@code end} 关键字,则终止解析过程</li>
|
||||
* <li>若当前 token 不匹配任何已注册区块,抛出异常</li>
|
||||
* </ol>
|
||||
* 解析并分派处理多区块结构。
|
||||
*
|
||||
* @param ctx 当前解析上下文,提供语法环境与作用域信息
|
||||
* @param tokens 当前 token 流
|
||||
* @param sectionDefinitions 各个区块的定义映射(key 为关键字,value 为判断 + 解析逻辑组合)
|
||||
* @throws RuntimeException 若出现无法识别的关键字或未满足的匹配条件
|
||||
* @param ctx 解析上下文
|
||||
* @param tokens 词法流
|
||||
* @param sectionDefinitions 区块处理注册表,key 为区块关键字,value 为对应的处理定义
|
||||
* @throws UnexpectedToken 遇到未注册或条件不符的关键字时抛出
|
||||
*/
|
||||
public static void parse(ParserContext ctx,
|
||||
TokenStream tokens,
|
||||
Map<String, SectionDefinition> sectionDefinitions) {
|
||||
|
||||
// 跳过开头的注释或空行
|
||||
skipCommentsAndNewlines(tokens);
|
||||
|
||||
while (true) {
|
||||
// 跳过当前区块之间的空白与注释
|
||||
skipCommentsAndNewlines(tokens);
|
||||
|
||||
String keyword = tokens.peek().getLexeme();
|
||||
|
||||
// 结束关键字表示解析流程终止
|
||||
if ("end".equals(keyword)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 查找匹配的区块定义
|
||||
SectionDefinition definition = sectionDefinitions.get(keyword);
|
||||
if (definition != null && definition.condition().test(tokens)) {
|
||||
definition.parser().accept(ctx, tokens); // 执行解析逻辑
|
||||
definition.parser().accept(ctx, tokens);
|
||||
} else {
|
||||
throw new RuntimeException("未识别的关键字或条件不满足: " + keyword);
|
||||
throw new UnexpectedToken(
|
||||
"未识别的关键字或条件不满足: " + keyword,
|
||||
tokens.peek().getLine(),
|
||||
tokens.peek().getCol()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过连续出现的注释行或空行(NEWLINE)。
|
||||
* <p>
|
||||
* 该方法用于在区块之间清理无效 token,避免影响结构判断。
|
||||
* 跳过所有连续的注释(COMMENT)和空行(NEWLINE)token。
|
||||
*
|
||||
* @param tokens 当前 token 流
|
||||
* @param tokens 当前词法流
|
||||
*/
|
||||
private static void skipCommentsAndNewlines(TokenStream tokens) {
|
||||
while (true) {
|
||||
TokenType type = tokens.peek().getType();
|
||||
if (type == TokenType.COMMENT || type == TokenType.NEWLINE) {
|
||||
tokens.next(); // 跳过注释或换行
|
||||
tokens.next();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
@ -94,17 +89,10 @@ public class FlexibleSectionParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 表示一个结构区块的定义,包含匹配条件与解析器。
|
||||
* <p>
|
||||
* 每个区块由两部分组成:
|
||||
* <ul>
|
||||
* <li>{@code condition}:用于判断当前 token 是否应进入该区块</li>
|
||||
* <li>{@code parser}:该区块对应的实际解析逻辑</li>
|
||||
* </ul>
|
||||
* 可实现懒加载、多语言支持或 DSL 的结构化扩展。
|
||||
* 区块定义,包含进入区块的判断条件与具体解析逻辑。
|
||||
*
|
||||
* @param condition 判断是否触发该区块的谓词函数
|
||||
* @param parser 区块解析逻辑(消费语法上下文与 token 流)
|
||||
* @param condition 匹配区块的前置条件
|
||||
* @param parser 区块内容的具体解析操作
|
||||
*/
|
||||
public record SectionDefinition(Predicate<TokenStream> condition,
|
||||
BiConsumer<ParserContext, TokenStream> parser) {
|
||||
|
||||
@ -1,67 +1,66 @@
|
||||
package org.jcnc.snow.compiler.parser.utils;
|
||||
|
||||
import org.jcnc.snow.compiler.parser.context.UnexpectedToken;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* JSON 工具类,提供线程安全、可重用的解析与序列化功能
|
||||
* JSON 工具类,提供线程安全、可重用的 JSON 解析与序列化能力。
|
||||
* <p>
|
||||
* - 解析:将合法的 JSON 文本转换为 Java 原生对象(Map、List、String、Number、Boolean 或 null)
|
||||
* - 序列化:将 Java 原生对象转换为符合 JSON 标准的字符串
|
||||
* <p>
|
||||
* 设计要点:
|
||||
* 1. 使用静态方法作为唯一入口,避免状态共享导致的线程安全问题
|
||||
* 2. 解析器内部使用 char[] 缓冲区,提高访问性能
|
||||
* 3. 维护行列号信息,抛出异常时能精确定位错误位置
|
||||
* 4. 序列化器基于 StringBuilder,预分配容量,减少中间字符串创建
|
||||
* <b>主要功能:</b>
|
||||
* <ul>
|
||||
* <li>解析:将合法的 JSON 文本转换为 Java 原生对象(Map、List、String、Number、Boolean 或 null)</li>
|
||||
* <li>序列化:将 Java 原生对象转换为符合 JSON 标准的字符串</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <b>设计要点:</b>
|
||||
* <ol>
|
||||
* <li>仅提供静态方法入口,无状态,线程安全</li>
|
||||
* <li>解析器内部采用 char[] 缓冲区,支持高性能处理</li>
|
||||
* <li>精确维护行列号信息,异常可定位错误文本位置</li>
|
||||
* <li>序列化器使用 StringBuilder,默认预分配容量</li>
|
||||
* </ol>
|
||||
*/
|
||||
public class JSONParser {
|
||||
|
||||
private JSONParser() {}
|
||||
|
||||
/**
|
||||
* 将 JSON 文本解析为对应的 Java 对象
|
||||
* 解析 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 如果遇到语法错误或多余字符,异常消息中包含行列信息
|
||||
* @return 解析得到的 Java 对象(Map、List、String、Number、Boolean 或 null)
|
||||
* @throws UnexpectedToken 语法错误或多余字符,异常消息带行列定位
|
||||
*/
|
||||
public static Object parse(String input) {
|
||||
return new Parser(input).parseInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Java 原生对象序列化为 JSON 字符串
|
||||
* @param obj 支持的类型:Map、Collection、String、Number、Boolean 或 null
|
||||
* 将 Java 原生对象序列化为 JSON 字符串。
|
||||
*
|
||||
* @param obj 支持 Map、Collection、String、Number、Boolean 或 null
|
||||
* @return 符合 JSON 规范的字符串
|
||||
* @throws UnsupportedOperationException 遇到不支持的类型时抛出
|
||||
*/
|
||||
public static String toJson(Object obj) {
|
||||
return Writer.write(obj);
|
||||
}
|
||||
|
||||
// ======= 内部解析器 =======
|
||||
/**
|
||||
* 负责将 char[] 缓冲区中的 JSON 文本解析为 Java 对象
|
||||
*/
|
||||
private static class Parser {
|
||||
/** 输入缓冲区 */
|
||||
private final char[] buf;
|
||||
/** 当前解析到的位置索引 */
|
||||
private int pos;
|
||||
/** 当前字符所在行号,从 1 开始 */
|
||||
private int line;
|
||||
/** 当前字符所在列号,从 1 开始 */
|
||||
private int col;
|
||||
// ======= 内部解析器实现 =======
|
||||
|
||||
/**
|
||||
* 构造解析器,初始化缓冲区和行列信息
|
||||
* @param input 待解析的 JSON 文本
|
||||
* 负责将 char[] 缓冲区中的 JSON 文本解析为 Java 对象。
|
||||
* 维护行列号,所有异常均带精确位置。
|
||||
*/
|
||||
private static class Parser {
|
||||
private final char[] buf;
|
||||
private int pos;
|
||||
private int line;
|
||||
private int col;
|
||||
|
||||
Parser(String input) {
|
||||
this.buf = input.toCharArray();
|
||||
this.pos = 0;
|
||||
@ -70,7 +69,7 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口方法,跳过空白后调用 parseValue,再校验尾部无多余字符
|
||||
* 解析主入口,校验无多余字符。
|
||||
*/
|
||||
Object parseInternal() {
|
||||
skipWhitespace();
|
||||
@ -83,7 +82,7 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据下一个字符决定解析哪种 JSON 值
|
||||
* 解析 JSON 值(null, true, false, string, number, object, array)
|
||||
*/
|
||||
private Object parseValue() {
|
||||
skipWhitespace();
|
||||
@ -96,51 +95,50 @@ public class JSONParser {
|
||||
if (c == '[') return parseArray();
|
||||
if (c == '-' || isDigit(c)) return parseNumber();
|
||||
error("遇到意外字符 '" + c + "'");
|
||||
return null; // 永不到达
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 对象,返回 Map<String, Object>
|
||||
* 解析对象类型 { ... }
|
||||
*/
|
||||
private Map<String, Object> parseObject() {
|
||||
expect('{'); // 跳过 '{'
|
||||
expect('{');
|
||||
skipWhitespace();
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
// 空对象 {}
|
||||
if (currentChar() == '}') {
|
||||
advance(); // 跳过 '}'
|
||||
advance();
|
||||
return map;
|
||||
}
|
||||
// 多成员对象解析
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
String key = parseString(); // 解析键
|
||||
skipWhitespace(); expect(':'); skipWhitespace();
|
||||
Object val = parseValue(); // 解析值
|
||||
String key = parseString();
|
||||
skipWhitespace();
|
||||
expect(':');
|
||||
skipWhitespace();
|
||||
Object val = parseValue();
|
||||
map.put(key, val);
|
||||
skipWhitespace();
|
||||
if (currentChar() == '}') {
|
||||
advance(); // 跳过 '}'
|
||||
advance();
|
||||
break;
|
||||
}
|
||||
expect(','); skipWhitespace();
|
||||
expect(',');
|
||||
skipWhitespace();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 数组,返回 List<Object>
|
||||
* 解析数组类型 [ ... ]
|
||||
*/
|
||||
private List<Object> parseArray() {
|
||||
expect('[');
|
||||
skipWhitespace();
|
||||
List<Object> list = new ArrayList<>();
|
||||
// 空数组 []
|
||||
if (currentChar() == ']') {
|
||||
advance(); // 跳过 ']'
|
||||
advance();
|
||||
return list;
|
||||
}
|
||||
// 多元素数组解析
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
list.add(parseValue());
|
||||
@ -149,39 +147,49 @@ public class JSONParser {
|
||||
advance();
|
||||
break;
|
||||
}
|
||||
expect(','); skipWhitespace();
|
||||
expect(',');
|
||||
skipWhitespace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 字符串文字,处理转义字符
|
||||
* 解析字符串类型,支持标准 JSON 转义。
|
||||
*/
|
||||
private String parseString() {
|
||||
expect('"'); // 跳过开头 '"'
|
||||
expect('"');
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (true) {
|
||||
char c = currentChar();
|
||||
if (c == '"') {
|
||||
advance(); // 跳过结束 '"'
|
||||
advance();
|
||||
break;
|
||||
}
|
||||
if (c == '\\') {
|
||||
advance(); // 跳过 '\'
|
||||
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 'u': // 解析 Unicode 转义
|
||||
String hex = new String(buf, pos+1, 4);
|
||||
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':
|
||||
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 + "'");
|
||||
@ -196,15 +204,14 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 数值,支持整数、浮点及科学计数法
|
||||
* 解析数字类型(支持整数、小数、科学计数法)。
|
||||
*/
|
||||
private Number parseNumber() {
|
||||
int start = pos;
|
||||
if (currentChar() == '-') advance();
|
||||
while (isDigit(currentChar())) advance();
|
||||
if (currentChar() == '.') {
|
||||
do advance();
|
||||
while (isDigit(currentChar()));
|
||||
do { advance(); } while (isDigit(currentChar()));
|
||||
}
|
||||
if (currentChar() == 'e' || currentChar() == 'E') {
|
||||
advance();
|
||||
@ -212,7 +219,6 @@ public class JSONParser {
|
||||
while (isDigit(currentChar())) advance();
|
||||
}
|
||||
String num = new String(buf, start, pos - start);
|
||||
// 判断返回 Long 还是 Double
|
||||
if (num.indexOf('.') >= 0 || num.indexOf('e') >= 0 || num.indexOf('E') >= 0) {
|
||||
return Double.parseDouble(num);
|
||||
}
|
||||
@ -224,7 +230,7 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过所有空白字符,支持空格、制表符、回车、换行
|
||||
* 跳过所有空白符(含换行),同时维护行/列号。
|
||||
*/
|
||||
private void skipWhitespace() {
|
||||
while (pos < buf.length) {
|
||||
@ -237,15 +243,12 @@ public class JSONParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前位置字符,超出范围返回 '\0'
|
||||
*/
|
||||
private char currentChar() {
|
||||
return pos < buf.length ? buf[pos] : '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* 推进到下一个字符,并更新行列信息
|
||||
* 指针前移并更新行/列号。
|
||||
*/
|
||||
private void advance() {
|
||||
if (pos < buf.length) {
|
||||
@ -259,7 +262,7 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当前位置字符等于预期字符,否则抛出错误
|
||||
* 匹配下一个字符(或字符串),并前移指针。
|
||||
*/
|
||||
private void expect(char c) {
|
||||
if (currentChar() != c) {
|
||||
@ -269,7 +272,7 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试匹配给定字符串,匹配成功则移动位置并返回 true
|
||||
* 判断当前位置是否能完整匹配目标字符串,若能则移动指针。
|
||||
*/
|
||||
private boolean match(String s) {
|
||||
int len = s.length();
|
||||
@ -281,32 +284,30 @@ public class JSONParser {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符是否为数字
|
||||
*/
|
||||
private boolean isDigit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出带行列定位的解析错误
|
||||
* 抛出带行列号的解析异常。
|
||||
*/
|
||||
private void error(String msg) {
|
||||
throw new RuntimeException("Error at line " + line + ", column " + col + ": " + msg);
|
||||
throw new UnexpectedToken(
|
||||
"在第 " + line + " 行,第 " + col + " 列出现错误: " + msg,
|
||||
line,
|
||||
col
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ======= 内部序列化器 =======
|
||||
// ======= 内部序列化器实现 =======
|
||||
|
||||
/**
|
||||
* 负责高效地将 Java 对象写为 JSON 文本
|
||||
* 负责将 Java 对象序列化为 JSON 字符串。
|
||||
*/
|
||||
private static class Writer {
|
||||
/** 默认 StringBuilder 初始容量,避免频繁扩容 */
|
||||
private static final int DEFAULT_CAPACITY = 1024;
|
||||
|
||||
/**
|
||||
* 入口方法,根据 obj 类型分派写入逻辑
|
||||
*/
|
||||
static String write(Object obj) {
|
||||
StringBuilder sb = new StringBuilder(DEFAULT_CAPACITY);
|
||||
writeValue(obj, sb);
|
||||
@ -314,7 +315,7 @@ public class JSONParser {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对象类型选择合适的写入方式
|
||||
* 递归输出任意支持的 JSON 类型对象。
|
||||
*/
|
||||
private static void writeValue(Object obj, StringBuilder sb) {
|
||||
if (obj == null) {
|
||||
@ -344,24 +345,30 @@ public class JSONParser {
|
||||
}
|
||||
sb.append(']');
|
||||
} else {
|
||||
// 其他类型,使用 toString 并加引号
|
||||
quote(obj.toString(), sb);
|
||||
throw new UnsupportedOperationException(
|
||||
"不支持的 JSON 字符串化类型: " + obj.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为字符串添加双引号并转义必须的字符
|
||||
* JSON 字符串输出,处理所有必要的转义字符。
|
||||
*/
|
||||
private static void quote(String s, StringBuilder sb) {
|
||||
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('"');
|
||||
|
||||
@ -28,7 +28,7 @@ public final class SemanticAnalysisReporter {
|
||||
System.err.println("语义分析发现 " + errors.size() + " 个错误:");
|
||||
errors.forEach(err -> System.err.println(" " + err));
|
||||
} else {
|
||||
// System.out.println("## 语义分析通过,没有发现错误\n");
|
||||
System.out.println("\n## 语义分析通过,没有发现错误\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,13 +30,15 @@ public final class CleanTask implements Task {
|
||||
*/
|
||||
@Override
|
||||
public void run() throws IOException {
|
||||
deleteDir(Path.of("build"));
|
||||
deleteDir(Path.of("dist"));
|
||||
deleteDir(Path.of("build"), false);
|
||||
deleteDir(Path.of("dist"), false);
|
||||
|
||||
System.out.println("[clean] done.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除指定目录及其所有子文件和子目录。
|
||||
* 递归删除指定目录下的所有子文件和子目录。
|
||||
* 如需删除指定目录本身可将第二个参数 {@code containSelf} 设置为 true
|
||||
* <p>
|
||||
* 若目录不存在,则直接返回。
|
||||
* </p>
|
||||
@ -45,14 +47,18 @@ public final class CleanTask implements Task {
|
||||
* </p>
|
||||
*
|
||||
* @param dir 需要删除的目录路径
|
||||
* @param containSelf 是否删除指定目录本身
|
||||
* @throws IOException 删除目录或文件过程中发生 IO 错误时抛出
|
||||
*/
|
||||
private void deleteDir(Path dir) throws IOException {
|
||||
private void deleteDir(Path dir, boolean containSelf) throws IOException {
|
||||
if (Files.notExists(dir)) return;
|
||||
try (var stream = Files.walk(dir)) {
|
||||
stream.sorted(Comparator.reverseOrder()) // 先删子文件,后删父目录
|
||||
.forEach(p -> {
|
||||
try {
|
||||
if (!containSelf && p == dir) {
|
||||
return;
|
||||
}
|
||||
Files.delete(p);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@ -14,7 +14,9 @@ import org.jcnc.snow.vm.module.*;
|
||||
*/
|
||||
public class RetCommand implements Command {
|
||||
|
||||
/** Sentinel value that tells the VM loop to terminate gracefully. */
|
||||
/**
|
||||
* Sentinel value that tells the VM loop to terminate gracefully.
|
||||
*/
|
||||
private static final int PROGRAM_END = Integer.MAX_VALUE;
|
||||
|
||||
@Override
|
||||
@ -32,8 +34,8 @@ public class RetCommand implements Command {
|
||||
StackFrame topFrame = callStack.peekFrame();
|
||||
|
||||
/* ----- Root frame: do NOT pop, just end program ----- */
|
||||
if (topFrame.getReturnAddress() == 0) {
|
||||
System.out.println("Return 0");
|
||||
if (topFrame.getReturnAddress() == PROGRAM_END) {
|
||||
System.out.println("Return <root>");
|
||||
return PROGRAM_END; // VM main loop should break
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user