feat: 支持行内注释

This commit is contained in:
Luke 2025-07-01 10:45:56 +08:00
parent 4507e3589f
commit 7b9bd37900
3 changed files with 65 additions and 25 deletions

View File

@ -6,37 +6,49 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
import java.util.List;
/**
* {@code TokenStream} 封装了一个 Token 列表并维护当前解析位置
* 是语法分析器读取词法单元的核心工具类
* <p>
* 提供前瞻peek消费next匹配match断言expect等常用操作
* 支持前向查看和异常处理适用于递归下降解析等常见语法构建策略
* </p>
* {@code TokenStream} 封装了一个 Token 列表并维护当前解析位置是语法分析器读取词法单元的核心工具类
*
* <p>提供前瞻peek消费next匹配match断言expect等常用操作
* 支持前向查看和异常处理适用于递归下降解析等常见语法构建策略</p>
*
*/
public class TokenStream {
/** 源 Token 列表 */
/**
* Token 列表
*/
private final List<Token> tokens;
/** 当前解析位置索引 */
/**
* 当前解析位置索引
*/
private int pos = 0;
/**
* 使用 Token 列表构造 TokenStream
*
* @param tokens 由词法分析器产生的 Token 集合
* @throws NullPointerException 如果 tokens null
*/
public TokenStream(List<Token> tokens) {
if (tokens == null) {
throw new NullPointerException("Token list cannot be null.");
}
this.tokens = tokens;
}
/**
* 向前查看指定偏移量处的 Token不移动位置
* 会在 offset==0 时自动跳过当前位置的所有注释COMMENTtoken
*
* @param offset 相对当前位置的偏移量0 表示当前
* @param offset 相对当前位置的偏移量0 表示当前 token
* @return 指定位置的 Token若越界则返回自动构造的 EOF Token
*/
public Token peek(int offset) {
// 只在 offset==0 时跳注释向前多步 peek 由调用方控制
if (offset == 0) {
skipTrivia();
}
int idx = pos + offset;
if (idx >= tokens.size()) {
return Token.eof(tokens.size() + 1);
@ -47,28 +59,30 @@ public class TokenStream {
/**
* 查看当前位置的 Token等效于 {@code peek(0)}
*
* @return 当前 Token
* @return 当前有效 Token已跳过注释
*/
public Token peek() {
skipTrivia();
return peek(0);
}
/**
* 消费当前位置的 Token 并返回位置前移
* 消费当前位置的 Token 并返回位置前移注释 token 会被自动跳过
*
* @return 当前 Token
* @return 被消费的有效 Token已跳过注释
*/
public Token next() {
Token t = peek();
pos++;
Token t = peek(); // peek() 已跳过注释
pos++; // 指针指向下一个 raw token
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)) {
@ -80,6 +94,7 @@ public class TokenStream {
/**
* 断言当前 Token 的词素与指定值相符否则抛出 {@link ParseException}
* 匹配成功会消费该 token 并前移指针
*
* @param lexeme 期望的词素值
* @return 匹配成功的 Token
@ -98,6 +113,7 @@ public class TokenStream {
/**
* 断言当前 Token 类型为指定类型否则抛出 {@link ParseException}
* 匹配成功会消费该 token 并前移指针
*
* @param type 期望的 Token 类型
* @return 匹配成功的 Token
@ -115,13 +131,37 @@ public class TokenStream {
}
/**
* 判断是否已经到达 EOF
* 判断是否已经到达文件末尾EOF
*
* @return 若当前位置 Token EOF则返回 true否则 false
* @return 若当前位置 Token EOF则返回 true否则返回 false
*/
public boolean isAtEnd() {
return peek().getType() == TokenType.EOF;
}
/**
* 跳过所有连续的注释COMMENTtoken
*
* <p>
* 此方法会检查当前指针 <code>pos</code> 所指向的 token
* 如果其类型为 <code>TokenType.COMMENT</code>则直接将指针递增
* 直到遇到非 COMMENT 类型或到达 token 列表末尾
* </p>
*
* <p>
* 注意此方法<strong>只会跳过注释</strong>不会递归或调用任何
* 会产生递归的方法 peek()/next()以避免堆栈溢出
* </p>
*
* <p>
* 使用场景词法分析产物中允许出现注释 token语法分析时需要自动跳过它们
* 保证 parser 只处理有效语法 token
* </p>
*/
private void skipTrivia() {
while (pos < tokens.size()
&& tokens.get(pos).getType() == TokenType.COMMENT) {
pos++; // 直接跳过 COMMENT 类型
}
}
}

View File

@ -17,7 +17,7 @@ public record ParserEngine(ParserContext ctx) {
List<String> errs = new ArrayList<>();
TokenStream ts = ctx.getTokens();
while (!ts.isAtEnd()) {
while (ts.isAtEnd()) {
// 跳过空行
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
@ -46,7 +46,7 @@ public record ParserEngine(ParserContext ctx) {
* 错误同步跳到下一行或下一个已注册顶层关键字
*/
private void synchronize(TokenStream ts) {
while (!ts.isAtEnd()) {
while (ts.isAtEnd()) {
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
break;
@ -57,7 +57,7 @@ public record ParserEngine(ParserContext ctx) {
ts.next();
}
// 连续空行全部吃掉
while (!ts.isAtEnd() && ts.peek().getType() == TokenType.NEWLINE) {
while (ts.isAtEnd() && ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
}
}

View File

@ -92,7 +92,7 @@ public class PrattExpressionParser implements ExpressionParser {
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);