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);
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalError.java b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalError.java
new file mode 100644
index 0000000..81ac9ee
--- /dev/null
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalError.java
@@ -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;
+ }
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java
index 96cfc0c..cdd01cb 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/core/LexicalException.java
@@ -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; }
}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java
index a8a3313..90204ce 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java
@@ -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)。
+ *
+ * 负责将源码中的两种注释形式切分为 {@link TokenType#COMMENT COMMENT} token:
+ *
+ * - 单行注释:以 {@code //} 开头,直至行尾或文件末尾。
+ * - 多行注释:以 {@code /*} 开头,以
*/ 结束,可跨多行。
+ *
+ *
+ * 本扫描器遵循“发现即捕获”原则:注释文本被完整保留在 Token 中,供后续的文档提取、源映射等分析使用。
+ *
+ * 错误处理策略
*
- * - 单行注释(以 "//" 开头,直到行尾)
- * - 多行注释(以 "/*" 开头,以 "*/" 结尾)
+ * - 未终止的多行注释:若文件结束时仍未遇到
*/,抛出 {@link LexicalException}。
*
- *
- * 本扫描器会识别注释并生成 {@code TokenType.COMMENT} 类型的 Token,
- * 不会丢弃注释内容,而是将完整注释文本保留在 Token 中,便于后续分析(如文档提取、保留注释等场景)。
- *
*/
public class CommentTokenScanner extends AbstractTokenScanner {
/**
- * 判断是否可以处理当前位置的字符。
- * 当当前位置字符为 '/' 且下一个字符为 '/' 或 '*' 时,表示可能是注释的起始。
- *
- * @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 {
}
/**
- * 实现注释的扫描逻辑。
- * 支持两种注释格式:
- *
- * - 单行注释: 以 "//" 开头,直到遇到换行符
- * - 多行注释: 以 "/*" 开头,直到遇到 "*/" 结束
- *
+ * 执行注释扫描,生成 {@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;
- // 处理单行注释 //
- 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()); // 消费 '/'
+ // 读取注释起始符
+ literal.append(ctx.advance()); // 消费首个 '/'
+
+ while (!ctx.isAtEnd()) {
+ 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 // 多行注释结束状态
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java
index 633d834..1e18cbb 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java
@@ -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;
/**
- * 标识符扫描器:处理标识符的识别,如变量名、函数名等。
- *
- * 识别规则如下:
+ * {@code IdentifierTokenScanner} —— 标识符扫描器,负责识别源代码中的标识符(如变量名、函数名等)。
+ *
+ *
标识符的识别遵循以下规则:
*
- * - 必须以字母或下划线(_)开头
- * - 后续字符可以是字母、数字或下划线
+ * - 标识符必须以字母(A-Z,a-z)或下划线(_)开头。
+ * - 标识符的后续字符可以是字母、数字(0-9)或下划线。
*
- *
- * 扫描完成后会调用 {@link TokenFactory} 自动判断是否为关键字,
- * 并返回对应类型的 {@link Token}。
+ *
+ *
在扫描过程中,标识符会被处理为一个 {@link Token} 对象。如果该标识符是一个关键字,
+ * 扫描器会通过 {@link TokenFactory} 自动识别并返回相应的 {@link TokenType}。
+ *
+ * 本扫描器实现了一个有限状态机(FSM),它能够在不同状态之间转换,确保标识符的正确识别。
*/
public class IdentifierTokenScanner extends AbstractTokenScanner {
/**
- * 判断是否可以处理当前位置的字符。
- * 如果字符为字母或下划线,则认为是标识符的起始。
+ * 判断当前字符是否可以作为标识符的起始字符。
+ * 如果字符为字母或下划线,则认为是标识符的开始。
*
* @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 {
}
/**
- * 执行标识符的扫描逻辑。
- * 连续读取满足标识符规则的字符序列,交由 {@code TokenFactory} 创建对应的 Token。
+ * 执行标识符扫描。
+ * 使用状态机模式扫描标识符。首先从初始状态开始,读取标识符的起始字符(字母或下划线)。
+ * 然后,进入标识符状态,继续读取标识符字符(字母、数字或下划线)。一旦遇到不符合标识符规则的字符,
+ * 标识符扫描结束,返回一个 {@link Token}。
*
- * @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 // 标识符状态,继续读取标识符字符
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java
index daea57c..0f63e70 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java
@@ -7,10 +7,19 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
/**
* 换行符扫描器:将源代码中的换行符(\n)识别为 {@code NEWLINE} 类型的 Token。
*
- * 通常用于记录行的分界,辅助语法分析阶段进行行敏感的判断或保持结构清晰。
+ * 用于记录行的分界,辅助语法分析阶段进行行敏感的判断或保持结构清晰。
*/
public class NewlineTokenScanner extends AbstractTokenScanner {
+ // 定义状态枚举
+ private enum State {
+ INITIAL,
+ NEWLINE
+ }
+
+ // 当前状态
+ private State currentState = State.INITIAL;
+
/**
* 判断是否可以处理当前位置的字符。
*
当字符为换行符(\n)时返回 true。
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java
index 517509f..88592b9 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java
@@ -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;
/**
- * 数字扫描器:识别整数、小数以及带有类型后缀的数字字面量。
+ * NumberTokenScanner —— 基于有限状态机(FSM)的数字字面量解析器。
*
- * 支持的格式示例:
+ * 该扫描器负责将源码中的数字字符串切分为 NUMBER_LITERAL token,当前支持:
+ *
+ * - 十进制整数(如 0,42,123456)
+ * - 十进制小数(如 3.14,0.5)
+ * - 单字符类型后缀(如 2.0f,255B,合法集合见 SUFFIX_CHARS)
+ *
+ *
+ * 如果后续需要支持科学计数法、下划线分隔符、不同进制等,只需扩展现有状态机的转移规则。
+ *
+ *
+ * 状态机简述:
+ * INT_PART --'.'--> DEC_POINT
+ * | |
+ * | v
+ * else------------> END
+ * |
+ * v
+ * DEC_POINT --digit--> FRAC_PART
+ *
+ * 状态说明:
*
- * - 整数:123、0、45678
- * - 小数:3.14、0.5、12.0
- * - 带类型后缀:2.0f、42L、7s、255B
+ * - INT_PART :读取整数部分,遇到 '.' 进入 DEC_POINT,否则结束。
+ * - DEC_POINT :已读到小数点,必须下一个字符是数字,否则报错。
+ * - FRAC_PART :读取小数部分,遇非法字符则结束主体。
+ * - END :主体扫描结束,进入后缀/尾随字符判定。
*
- *
- * 语法允许在数字 (整数或小数) 末尾添加以下单字符后缀来显式指定常量类型:
- *
b | s | l | f | d // 分别对应 byte、short、long、float、double
- * B | S | L | F | D // 同上,大小写皆可
- * 生成的 Token 类型始终为 {@code NUMBER_LITERAL},词法单元将携带完整的文本(含后缀,若存在)。
+ *
+ * 错误处理策略:
+ *
+ * - 数字后跟未知字母(如 42X)—— 抛出 LexicalException
+ * - 数字与合法后缀间有空白(如 3 L)—— 抛出 LexicalException
+ * - 小数点后缺失数字(如 1.)—— 抛出 LexicalException
+ *
+ *
+ * 支持的单字符类型后缀包括:b, s, l, f, d 及其大写形式。若需支持多字符后缀,可将该集合扩展为 Set。
*/
public class NumberTokenScanner extends AbstractTokenScanner {
/**
- * 可选类型后缀字符集合 (大小写均可)。
- * 与 {@code ExpressionBuilder} 内的后缀解析逻辑保持一致。
+ * 支持的单字符类型后缀集合。
+ * 包含:b, s, l, f, d 及其大写形式。
+ * 对于多字符后缀,可扩展为 Set 并在扫描尾部做贪婪匹配。
*/
private static final String SUFFIX_CHARS = "bslfdBSLFD";
/**
- * 判断是否可以处理当前位置的字符。
- * 当字符为数字时,表示可能是数字字面量的起始。
+ * 判断是否由该扫描器处理。
+ * 仅当首字符为数字时,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 {
}
/**
- * 执行数字扫描逻辑。
- *
- * - 连续读取数字字符,允许出现一个小数点,用于识别整数或小数。
- * - 读取完主体后,一次性检查下一个字符,若属于合法类型后缀则吸收。
- *
- * 这样可以保证诸如 {@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());
- } else {
- break; // 遇到非数字/第二个点 => 主体结束
+ /* ───── 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 {
+ 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
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java
index ec2f2bf..951b1c5 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java
@@ -45,80 +45,84 @@ 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 '=':
if (ctx.match('=')) {
lexeme = "==";
- type = TokenType.DOUBLE_EQUALS;
+ type = TokenType.DOUBLE_EQUALS;
} else {
- lexeme = "=";
- type = TokenType.EQUALS;
+ type = TokenType.EQUALS;
}
break;
case '!':
if (ctx.match('=')) {
lexeme = "!=";
- type = TokenType.NOT_EQUALS;
+ type = TokenType.NOT_EQUALS;
} else {
- lexeme = "!";
- type = TokenType.NOT;
+ type = TokenType.NOT;
}
break;
case '>':
if (ctx.match('=')) {
lexeme = ">=";
- type = TokenType.GREATER_EQUAL;
+ type = TokenType.GREATER_EQUAL;
} else {
- lexeme = ">";
- type = TokenType.GREATER_THAN;
+ type = TokenType.GREATER_THAN;
}
break;
case '<':
if (ctx.match('=')) {
lexeme = "<=";
- type = TokenType.LESS_EQUAL;
+ type = TokenType.LESS_EQUAL;
} else {
- lexeme = "<";
- type = TokenType.LESS_THAN;
+ type = TokenType.LESS_THAN;
}
break;
case '%':
- lexeme = "%";
- type = TokenType.MODULO;
+ type = TokenType.MODULO;
break;
case '&':
if (ctx.match('&')) {
lexeme = "&&";
- type = TokenType.AND;
- } else {
- lexeme = "&";
- type = TokenType.UNKNOWN;
+ type = TokenType.AND;
}
break;
case '|':
if (ctx.match('|')) {
lexeme = "||";
- type = TokenType.OR;
- } else {
- lexeme = "|";
- type = TokenType.UNKNOWN;
+ type = TokenType.OR;
}
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 // 无法识别的状态
+ }
}
diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java
index a8643e4..a610d06 100644
--- a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java
+++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java
@@ -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 == '"') {
- break;
+ 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 // 字符串结束状态
+ }
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/ast/CallExpressionNode.java b/src/main/java/org/jcnc/snow/compiler/parser/ast/CallExpressionNode.java
index 22f47bc..d7dbce9 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/ast/CallExpressionNode.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/ast/CallExpressionNode.java
@@ -67,5 +67,7 @@ public record CallExpressionNode(
*
* @return 当前表达式所在的文件名。
*/
- public String file() { return file; }
+ public String file() {
+ return file;
+ }
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/MissingToken.java b/src/main/java/org/jcnc/snow/compiler/parser/context/MissingToken.java
new file mode 100644
index 0000000..3610426
--- /dev/null
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/MissingToken.java
@@ -0,0 +1,22 @@
+package org.jcnc.snow.compiler.parser.context;
+
+/**
+ * 表示在语法分析过程中,必须出现的 Token 缺失时抛出的异常。
+ *
+ * 当分析器检测到输入流中缺少某个预期 Token 时,会抛出此异常,以便准确地指明语法错误位置。
+ * 该异常包含了缺失 Token 的名称以及发生缺失的位置(行号和列号),便于错误定位和后续处理。
+ *
+ */
+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);
+ }
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/ParseError.java b/src/main/java/org/jcnc/snow/compiler/parser/context/ParseError.java
new file mode 100644
index 0000000..24c1d27
--- /dev/null
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/ParseError.java
@@ -0,0 +1,45 @@
+package org.jcnc.snow.compiler.parser.context;
+
+/**
+ * 语法错误的数据传输对象(DTO)。
+ *
+ * 用于收集和展示语法分析过程中检测到的错误信息,便于错误定位和报告。
+ * 包含出错文件、行号、列号和具体错误信息等字段。
+ *
+ */
+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;
+ }
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/ParseException.java b/src/main/java/org/jcnc/snow/compiler/parser/context/ParseException.java
index 0313d7a..97ceb33 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/context/ParseException.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/ParseException.java
@@ -1,24 +1,75 @@
package org.jcnc.snow.compiler.parser.context;
/**
- * {@code ParseException} 表示语法分析阶段发生的错误。
+ * 语法分析阶段所有错误的基类。
*
- * 当语法分析器遇到非法的语法结构或无法继续处理的标记序列时,
- * 应抛出该异常以中断当前解析流程,并向调用方报告错误信息。
+ * 本异常作为语法分析相关错误的统一父类,屏蔽了堆栈信息,确保在命令行界面(CLI)输出时只占用一行,方便用户快速定位问题。
+ * 通过 {@code permits} 关键字,限定了可被继承的异常类型,增强类型安全性。
*
+ *
*
- * 该异常通常由 {@code ParserContext} 或各类语法规则处理器主动抛出,
- * 用于提示编译器前端或 IDE 系统进行错误提示与恢复。
+ * 该异常携带错误发生的行号、列号和具体原因信息,用于语法错误的精确报告和输出展示。
*
*/
-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;
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/ParserContext.java b/src/main/java/org/jcnc/snow/compiler/parser/context/ParserContext.java
index 5ff228a..b9c9852 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/context/ParserContext.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/ParserContext.java
@@ -15,10 +15,14 @@ import java.util.List;
*/
public class ParserContext {
- /** 当前语法分析所使用的 Token 流 */
+ /**
+ * 当前语法分析所使用的 Token 流
+ */
private final TokenStream tokens;
- /** 当前语法分析所使用的资源文件名 */
+ /**
+ * 当前语法分析所使用的资源文件名
+ */
private final String sourceName;
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/TokenStream.java b/src/main/java/org/jcnc/snow/compiler/parser/context/TokenStream.java
index 5841b7b..07a4ffb 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/context/TokenStream.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/TokenStream.java
@@ -6,37 +6,49 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
import java.util.List;
/**
- * {@code TokenStream} 封装了一个 Token 列表并维护当前解析位置,
- * 是语法分析器读取词法单元的核心工具类。
+ * {@code TokenStream} 封装了 Token 序列并维护当前解析位置,是语法分析器读取词法单元的核心工具类。
*
- * 提供前瞻(peek)、消费(next)、匹配(match)、断言(expect)等常用操作,
- * 支持前向查看和异常处理,适用于递归下降解析等常见语法构建策略。
+ * 该类提供前瞻(peek)、消费(next)、匹配(match)、断言(expect)等常用操作,
+ * 支持前向查看和异常处理,适用于递归下降等常见语法解析策略。
+ * 设计上自动跳过注释(COMMENT)token,并对越界情况提供自动构造的 EOF(文件结束)token,
+ * 有效提升语法处理的健壮性与易用性。
*
*/
public class TokenStream {
- /** 源 Token 列表 */
+ /**
+ * 源 Token 列表
+ */
private final List tokens;
-
- /** 当前解析位置索引 */
+ /**
+ * 当前解析位置索引
+ */
private int pos = 0;
/**
* 使用 Token 列表构造 TokenStream。
*
- * @param tokens 由词法分析器产生的 Token 集合
+ * @param tokens 词法分析器输出的 Token 集合
+ * @throws NullPointerException 如果 tokens 为 null
*/
public TokenStream(List 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;
}
-
-}
\ No newline at end of file
+ /**
+ * 跳过所有连续的注释(COMMENT)token,使解析器总是定位在第一个有效 Token 上。
+ */
+ private void skipTrivia() {
+ while (pos < tokens.size()
+ && tokens.get(pos).getType() == TokenType.COMMENT) {
+ pos++;
+ }
+ }
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/UnexpectedToken.java b/src/main/java/org/jcnc/snow/compiler/parser/context/UnexpectedToken.java
new file mode 100644
index 0000000..ebaaef7
--- /dev/null
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/UnexpectedToken.java
@@ -0,0 +1,21 @@
+package org.jcnc.snow.compiler.parser.context;
+
+/**
+ * 表示在语法分析过程中遇到意料之外或无法识别的 Token 时抛出的异常。
+ *
+ * 当分析器检测到实际遇到的 Token 不符合语法规则,或与预期类型不符时会抛出本异常,便于错误定位和报告。
+ *
+ */
+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);
+ }
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/context/UnsupportedFeature.java b/src/main/java/org/jcnc/snow/compiler/parser/context/UnsupportedFeature.java
new file mode 100644
index 0000000..a1288bb
--- /dev/null
+++ b/src/main/java/org/jcnc/snow/compiler/parser/context/UnsupportedFeature.java
@@ -0,0 +1,21 @@
+package org.jcnc.snow.compiler.parser.context;
+
+/**
+ * 表示在语法分析过程中使用了尚未支持的语法或语言特性时抛出的异常。
+ *
+ * 当用户使用了当前编译器实现尚不支持的语法、关键字或特性时,语法分析器将抛出此异常,用于清晰提示和错误报告。
+ *
+ */
+public final class UnsupportedFeature extends ParseException {
+
+ /**
+ * 构造一个“暂未支持的语法/特性”异常。
+ *
+ * @param feature 未被支持的语法或特性描述
+ * @param line 发生异常的行号
+ * @param column 发生异常的列号
+ */
+ public UnsupportedFeature(String feature, int line, int column) {
+ super("暂未支持的语法/特性: " + feature, line, column);
+ }
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java b/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java
index 7d0854f..93f3547 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/core/ParserEngine.java
@@ -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)。
+ *
+ * 负责驱动顶层语法解析,并统一处理、收集所有语法异常,防止死循环,确保整体解析流程的健壮性与鲁棒性。
+ * 支持基于同步点的错误恢复,适用于命令式和脚本式语法环境。
+ *
+ *
+ *
+ * 本引擎以异常收集为核心设计,所有捕获到的 {@link ParseException} 会被聚合,在分析结束后一次性统一抛出。
+ * 同时,在解析出错时会通过同步(synchronize)机制,跳过错误片段以恢复到有效解析点,避免因指针停滞导致的死循环。
+ *
+ */
public record ParserEngine(ParserContext ctx) {
+ /**
+ * 解析整个 TokenStream,返回顶层 AST 节点列表。
+ *
+ * 过程中如遇语法异常,均会被收集并在最后聚合抛出,避免单点失败导致整个解析中断。
+ *
+ *
+ * @return 解析所得的顶层 AST 节点列表
+ * @throws UnexpectedToken 当存在语法错误时,统一抛出聚合异常
+ */
public List parse() {
List nodes = new ArrayList<>();
- List errs = new ArrayList<>();
- TokenStream ts = ctx.getTokens();
+ List 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;
}
/**
- * 错误同步:跳到下一行或下一个已注册顶层关键字
+ * 同步:跳过当前行或直到遇到显式注册的顶层关键字。
+ *
+ * 该机制用于语法出错后恢复到下一个可能的有效解析点,防止指针停滞导致死循环或重复抛错。
+ * 同步过程中会优先跳过本行所有未识别 token,并在遇到换行或注册关键字时停止,随后跳过连续空行。
+ *
+ *
+ * @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();
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/expression/PrattExpressionParser.java b/src/main/java/org/jcnc/snow/compiler/parser/expression/PrattExpressionParser.java
index 4f79274..df2ee36 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/expression/PrattExpressionParser.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/expression/PrattExpressionParser.java
@@ -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,64 +13,57 @@ import java.util.HashMap;
import java.util.Map;
/**
- * {@code PrattExpressionParser} 是基于 Pratt 算法实现的表达式解析器。
+ * {@code PrattExpressionParser} 基于 Pratt 算法的表达式解析器实现。
*
- * 它支持灵活的运算符优先级控制,结合前缀(PrefixParselet)和中缀(InfixParselet)解析器,
- * 可高效解析复杂表达式结构,包括:
- *
- * - 字面量(数字、字符串)
- * - 标识符
- * - 函数调用、成员访问
- * - 带括号的表达式、二元运算符
- *
- * 本类提供统一注册机制和递归表达式解析入口。
+ * 该类通过前缀(PrefixParselet)和中缀(InfixParselet)解析器注册表,
+ * 支持灵活扩展的表达式语法,包括字面量、变量、函数调用、成员访问和各种运算符表达式。
+ *
+ *
+ * 运算符优先级通过枚举控制,结合递归解析实现高效的优先级处理和语法结构解析。
+ * 未注册的语法类型或运算符会统一抛出 {@link UnsupportedFeature} 异常。
*
*/
public class PrattExpressionParser implements ExpressionParser {
- /**
- * 前缀解析器注册表:按 Token 类型映射
- */
+ /** 前缀解析器注册表(按 Token 类型名索引) */
private static final Map prefixes = new HashMap<>();
-
- /**
- * 中缀解析器注册表:按运算符词素映射
- */
+ /** 中缀解析器注册表(按运算符词素索引) */
private static final Map 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.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());
+ 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));
- infixes.put("/", new BinaryOperatorParselet(Precedence.PRODUCT, true));
- infixes.put("%", new BinaryOperatorParselet(Precedence.PRODUCT, true));
- infixes.put(">", new BinaryOperatorParselet(Precedence.SUM, true));
- infixes.put("<", new BinaryOperatorParselet(Precedence.SUM, true));
- infixes.put("==", new BinaryOperatorParselet(Precedence.SUM, true));
- infixes.put("!=", new BinaryOperatorParselet(Precedence.SUM, true));
- infixes.put(">=", new BinaryOperatorParselet(Precedence.SUM, true));
- infixes.put("<=", new BinaryOperatorParselet(Precedence.SUM, true));
- infixes.put("(", new CallParselet());
- infixes.put(".", new MemberParselet());
+ // 中缀解析器注册
+ infixes.put("+", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("-", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("*", new BinaryOperatorParselet(Precedence.PRODUCT, true));
+ infixes.put("/", new BinaryOperatorParselet(Precedence.PRODUCT, true));
+ infixes.put("%", new BinaryOperatorParselet(Precedence.PRODUCT, true));
+ infixes.put(">", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("<", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("==", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("!=", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put(">=", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("<=", new BinaryOperatorParselet(Precedence.SUM, true));
+ infixes.put("(", new CallParselet());
+ infixes.put(".", new MemberParselet());
}
/**
- * 表达式解析入口,使用最低优先级启动递归解析。
+ * 表达式解析统一入口。
+ * 以最低优先级启动递归下降,适配任意表达式复杂度。
*
- * @param ctx 当前语法解析上下文
- * @return 表达式抽象语法树节点
+ * @param ctx 当前解析上下文
+ * @return 解析后的表达式 AST 节点
*/
@Override
public ExpressionNode parse(ParserContext ctx) {
@@ -77,36 +71,52 @@ public class PrattExpressionParser implements ExpressionParser {
}
/**
- * 根据指定优先级解析表达式。
+ * 按指定优先级解析表达式。Pratt 算法主循环。
+ *
+ * 先根据当前 Token 类型查找前缀解析器进行初始解析,
+ * 然后根据优先级不断递归处理中缀运算符和右侧表达式。
+ *
*
- * @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());
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java b/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java
index 7fa779c..82c486f 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/factory/TopLevelParserFactory.java
@@ -8,22 +8,38 @@ import org.jcnc.snow.compiler.parser.top.ScriptTopLevelParser;
import java.util.Map;
import java.util.HashMap;
+/**
+ * {@code TopLevelParserFactory} 用于根据源码中顶层关键字取得对应的解析器。
+ *
+ * 若关键字未注册,则回退到脚本模式解析器 {@link ScriptTopLevelParser}。
+ */
public class TopLevelParserFactory {
+ /** 关键字 → 解析器注册表 */
private static final Map registry = new HashMap<>();
- private static final TopLevelParser DEFAULT = new ScriptTopLevelParser(); // ← 默认解析器
+
+ /** 缺省解析器:脚本模式(单条语句可执行) */
+ private static final TopLevelParser DEFAULT = new ScriptTopLevelParser();
static {
- // 顶层结构解析器
+ // 在此注册所有受支持的顶层结构关键字
registry.put("module", new ModuleParser());
registry.put("function", new FunctionParser());
- // 也可按需继续注册其它关键字
+ // 若未来新增顶层结构,可继续在此处注册
}
/**
- * 根据关键字获取解析器;若未注册,回退到脚本语句解析。
+ * 依据关键字返回解析器;若未注册则返回脚本解析器。
*/
public static TopLevelParser get(String keyword) {
return registry.getOrDefault(keyword, DEFAULT);
}
+
+ /**
+ * 判断某关键字是否已显式注册为顶层结构,
+ * 供同步恢复逻辑使用,避免死循环。
+ */
+ public static boolean isRegistered(String keyword) {
+ return registry.containsKey(keyword);
+ }
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/module/ModuleParser.java b/src/main/java/org/jcnc/snow/compiler/parser/module/ModuleParser.java
index ac9e05d..8a21e70 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/module/ModuleParser.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/module/ModuleParser.java
@@ -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} 负责解析源码中的模块结构,是顶层结构解析器实现之一。
*
- * 模块中可包含多个导入语句和函数定义,导入语句可在模块中任意位置出现,
- * 同时支持空行,空行将被自动忽略,不影响语法结构的正确性。
+ * 模块定义可包含多个导入(import)语句和函数定义(function),
+ * 导入语句可在模块中任意位置出现,且允许模块体中穿插任意数量的空行(空行会被自动忽略,不影响语法结构)。
+ *
+ *
+ *
+ * 典型模块语法结构:
+ *
+ * module: mymod
+ * import ...
+ * function ...
+ * ...
+ * end module
+ *
+ *
*/
public class ModuleParser implements TopLevelParser {
/**
- * 解析一个模块定义块,返回构建好的 {@link ModuleNode} 对象。
+ * 解析一个模块定义块,返回完整的 {@link ModuleNode} 语法树节点。
*
- * 本方法的语法流程包括:
+ * 解析过程包括:
*
- * - 匹配模块声明开头 {@code module: IDENTIFIER}。
- * - 收集模块体中的 import 语句与 function 定义,允许穿插空行。
- * - 模块结尾必须为 {@code end module},且后接换行符。
+ * - 匹配模块声明起始 {@code module: IDENTIFIER}。
+ * - 收集模块体内所有 import 和 function 语句,允许穿插空行。
+ * - 匹配模块结束 {@code end module}。
*
- * 所有语法错误将在解析过程中抛出异常,以便准确反馈问题位置和原因。
+ * 若遇到未识别的语句,将抛出 {@link UnexpectedToken} 异常,定位错误位置和原因。
+ *
*
- * @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 imports = new ArrayList<>();
List 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);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/statement/ExpressionStatementParser.java b/src/main/java/org/jcnc/snow/compiler/parser/statement/ExpressionStatementParser.java
index 526dbf0..f824b6b 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/statement/ExpressionStatementParser.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/statement/ExpressionStatementParser.java
@@ -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} 用于解析通用表达式语句(赋值或普通表达式)。
*
- * 支持的语法结构如下:
+ * 支持以下两种语法结构:
*
{@code
* x = 1 + 2 // 赋值语句
- * doSomething() // 函数调用等普通表达式语句
+ * doSomething() // 一般表达式语句
* }
*
- * - 若以标识符开头,且后接等号 {@code =},则视为赋值语句,解析为 {@link AssignmentNode}。
+ * - 以标识符开头且后接 {@code =} 时,解析为 {@link AssignmentNode}。
* - 否则视为普通表达式,解析为 {@link ExpressionStatementNode}。
- * - 所有表达式语句必须以换行符 {@code NEWLINE} 结束。
+ * - 所有表达式语句必须以换行符({@code NEWLINE})结尾。
*
- * 不允许以关键字或空行作为表达式的起始,若遇到非法开头,将抛出解析异常。
+ * 若语句起始为关键字或空行,将直接抛出异常,防止非法语法进入表达式解析流程。
*/
public class ExpressionStatementParser implements StatementParser {
/**
- * 解析一个表达式语句,根据上下文决定其为赋值或一般表达式。
- *
- * 具体逻辑如下:
- *
- * - 若当前行为标识符后接等号,则作为赋值处理。
- * - 否则解析整个表达式作为单独语句。
- * - 所有语句都必须以换行符结束。
- * - 若表达式以关键字或空行开头,将立即抛出异常,避免非法解析。
- *
+ * 解析单行表达式语句,根据上下文判断其为赋值语句或普通表达式语句。
*
- * @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);
}
-
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/top/ScriptTopLevelParser.java b/src/main/java/org/jcnc/snow/compiler/parser/top/ScriptTopLevelParser.java
index f669c8e..3403dcf 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/top/ScriptTopLevelParser.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/top/ScriptTopLevelParser.java
@@ -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);
}
}
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/utils/FlexibleSectionParser.java b/src/main/java/org/jcnc/snow/compiler/parser/utils/FlexibleSectionParser.java
index c319d4f..692e5a4 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/utils/FlexibleSectionParser.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/utils/FlexibleSectionParser.java
@@ -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)解析工具。
*
- * 该工具支持解析由关键字标识的多段结构化区块内容,常用于解析函数、类、模块、循环等语法单元中的命名子结构。
- * 相比传统硬编码方式,提供更灵活、可组合的解析能力,允许解析器模块动态注册处理逻辑,而非将所有逻辑写死在主流程中。
+ * 支持通过注册表驱动的方式解析具有区块关键字标识的多段结构内容,
+ * 常用于函数、类、模块、循环等语法单元中的命名子结构。
+ * 通过外部注册解析逻辑,支持高度可扩展与复用。
+ *
*
- * 典型应用包括:
+ *
+ * 典型用途包括:
*
* - 函数体解析中的 {@code params}、{@code returns}、{@code body} 等部分
* - 模块定义中的 {@code imports}、{@code functions} 等部分
- * - 用户自定义 DSL 的可扩展语法结构
+ * - 可扩展 DSL 的结构化语法区块
*
+ *
*
- * 该工具具备以下能力:
+ *
主要特性:
*
* - 自动跳过注释与空行
- * - 根据区块名称调用外部提供的解析器
- * - 支持终止标志(如 {@code end})来退出解析流程
+ * - 区块入口通过关键字匹配和可选条件判断
+ * - 解析逻辑由外部以函数式接口方式注册
+ * - 支持遇到终止关键字(如 {@code end})时自动停止
*
*/
public class FlexibleSectionParser {
/**
- * 启动结构化区块的统一解析流程。
- *
- * 每次调用会:
- *
- * - 从 token 流中跳过空行与注释
- * - 依照当前 token 判断是否匹配某个区块
- * - 调用对应 {@link SectionDefinition} 执行区块解析逻辑
- * - 若遇到 {@code end} 关键字,则终止解析过程
- * - 若当前 token 不匹配任何已注册区块,抛出异常
- *
+ * 解析并分派处理多区块结构。
*
- * @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 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)。
- *
- * 该方法用于在区块之间清理无效 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 {
}
/**
- * 表示一个结构区块的定义,包含匹配条件与解析器。
- *
- * 每个区块由两部分组成:
- *
- * - {@code condition}:用于判断当前 token 是否应进入该区块
- * - {@code parser}:该区块对应的实际解析逻辑
- *
- * 可实现懒加载、多语言支持或 DSL 的结构化扩展。
+ * 区块定义,包含进入区块的判断条件与具体解析逻辑。
*
- * @param condition 判断是否触发该区块的谓词函数
- * @param parser 区块解析逻辑(消费语法上下文与 token 流)
+ * @param condition 匹配区块的前置条件
+ * @param parser 区块内容的具体解析操作
*/
public record SectionDefinition(Predicate condition,
BiConsumer parser) {
diff --git a/src/main/java/org/jcnc/snow/compiler/parser/utils/JSONParser.java b/src/main/java/org/jcnc/snow/compiler/parser/utils/JSONParser.java
index ce30193..41eb1fa 100644
--- a/src/main/java/org/jcnc/snow/compiler/parser/utils/JSONParser.java
+++ b/src/main/java/org/jcnc/snow/compiler/parser/utils/JSONParser.java
@@ -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 解析与序列化能力。
*
- * - 解析:将合法的 JSON 文本转换为 Java 原生对象(Map、List、String、Number、Boolean 或 null)
- * - 序列化:将 Java 原生对象转换为符合 JSON 标准的字符串
- *
- * 设计要点:
- * 1. 使用静态方法作为唯一入口,避免状态共享导致的线程安全问题
- * 2. 解析器内部使用 char[] 缓冲区,提高访问性能
- * 3. 维护行列号信息,抛出异常时能精确定位错误位置
- * 4. 序列化器基于 StringBuilder,预分配容量,减少中间字符串创建
+ * 主要功能:
+ *
+ * - 解析:将合法的 JSON 文本转换为 Java 原生对象(Map、List、String、Number、Boolean 或 null)
+ * - 序列化:将 Java 原生对象转换为符合 JSON 标准的字符串
+ *
+ *
+ *
+ * 设计要点:
+ *
+ * - 仅提供静态方法入口,无状态,线程安全
+ * - 解析器内部采用 char[] 缓冲区,支持高性能处理
+ * - 精确维护行列号信息,异常可定位错误文本位置
+ * - 序列化器使用 StringBuilder,默认预分配容量
+ *
*/
public class JSONParser {
private JSONParser() {}
/**
- * 将 JSON 文本解析为对应的 Java 对象
+ * 解析 JSON 格式字符串为对应的 Java 对象。
+ *
* @param input JSON 格式字符串
- * @return 对应的 Java 原生对象:
- * - JSON 对象 -> Map
- * - JSON 数组 -> List