增加注释

This commit is contained in:
Luke 2025-05-11 23:59:29 +08:00
parent 3a992ccd37
commit 55526d530a
13 changed files with 462 additions and 247 deletions

View File

@ -18,53 +18,71 @@ import java.util.List;
import java.util.Map;
/**
* 顶层的函数定义解析器
* {@code FunctionParser} 是顶层函数定义的语法解析器
* 实现 {@link TopLevelParser} 接口用于将源代码中的函数块解析为抽象语法树AST中的 {@link FunctionNode}
*
* <p>
* 通过 FlexibleSectionParser 按区块顺序解析函数的各个部分
* 函数头名称参数列表返回类型函数体并最终生成 FunctionNode
* 支持在参数或返回类型声明中出现注释自动跳过注释和空行而不干扰语法解析
* 本类使用 {@link FlexibleSectionParser} 机制按照语义区块结构对函数进行模块化解析支持以下部分
* </p>
*
* <ul>
* <li>函数头关键字 {@code function:} 与函数名</li>
* <li>参数列表parameter 区块</li>
* <li>返回类型return_type 区块</li>
* <li>函数体body 区块</li>
* <li>函数结束关键字 {@code end function}</li>
* </ul>
*
* <p>
* 各区块允许包含注释类型 {@code COMMENT}与空行类型 {@code NEWLINE}解析器将自动跳过无效 token 保持语法连续性
* </p>
*
* <p>
* 最终将函数结构封装为 {@link FunctionNode} 并返回供后续编译阶段使用
* </p>
*/
public class FunctionParser implements TopLevelParser {
/**
* 顶层解析入口解析完整个函数定义并返回 FunctionNode
* 顶层语法解析入口
*
* @param ctx 解析上下文包含 TokenStream 和作用域信息
* @return 构建好的 FunctionNode
* <p>
* 该方法负责完整解析函数定义包括其所有组成部分并构建对应的 {@link FunctionNode}
* </p>
*
* @param ctx 当前解析上下文包含 {@link TokenStream} 和符号表等作用域信息
* @return 构建完成的 {@link FunctionNode} 抽象语法树节点
*/
@Override
public FunctionNode parse(ParserContext ctx) {
TokenStream ts = ctx.getTokens();
// 1. 解析函数头function:
parseFunctionHeader(ts);
// 2. 读取并消费函数名称
String functionName = parseFunctionName(ts);
// 容器用于收集解析结果
List<ParameterNode> parameters = new ArrayList<>();
String[] returnType = new String[1]; // 用一元素数组模拟可变引用
String[] returnType = new String[1];
List<StatementNode> body = new ArrayList<>();
// 3. 构建区块解析逻辑并使用 FlexibleSectionParser 解析
Map<String, SectionDefinition> sections = getSectionDefinitions(parameters, returnType, body);
FlexibleSectionParser.parse(ctx, ts, sections);
// 4. 解析函数尾部end function
parseFunctionFooter(ts);
// 5. 构造并返回 AST
return new FunctionNode(functionName, parameters, returnType[0], body);
}
/**
* 构造各区块的解析定义
* 参数parameter返回类型return_type函数体body
* 构造函数定义中各区块的解析规则parameterreturn_typebody
*
* @param params 参数节点列表容器
* @param returnType 返回类型容器
* @param body 函数体语句列表容器
* @return 区块关键字到解析逻辑的映射
* <p>
* 每个 {@link SectionDefinition} 包含两个部分区块起始判断器基于关键字与具体的解析逻辑
* </p>
*
* @param params 参数节点收集容器解析结果将存入此列表
* @param returnType 返回类型容器以单元素数组方式模拟引用传递
* @param body 函数体语句节点列表容器
* @return 区块关键字到解析定义的映射表
*/
private Map<String, SectionDefinition> getSectionDefinitions(
List<ParameterNode> params,
@ -72,19 +90,16 @@ public class FunctionParser implements TopLevelParser {
List<StatementNode> body) {
Map<String, SectionDefinition> map = new HashMap<>();
// 参数区块
map.put("parameter", new SectionDefinition(
(TokenStream stream) -> stream.peek().getLexeme().equals("parameter"),
(ParserContext context, TokenStream stream) -> params.addAll(parseParameters(stream))
));
// 返回类型区块
map.put("return_type", new SectionDefinition(
(TokenStream stream) -> stream.peek().getLexeme().equals("return_type"),
(ParserContext context, TokenStream stream) -> returnType[0] = parseReturnType(stream)
));
// 函数体区块
map.put("body", new SectionDefinition(
(TokenStream stream) -> stream.peek().getLexeme().equals("body"),
(ParserContext context, TokenStream stream) -> body.addAll(parseFunctionBody(context, stream))
@ -94,9 +109,9 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 解析函数头部匹配 "function:"并跳过后续的注释和空行
* 解析函数头部标识符 {@code function:}并跳过其后多余注释与空行
*
* @param ts TokenStream
* @param ts 当前使用的 {@link TokenStream}
*/
private void parseFunctionHeader(TokenStream ts) {
ts.expect("function");
@ -106,10 +121,10 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 解析函数名称IDENTIFIER + 换行
* 解析函数名称标识符并跳过换行
*
* @param ts TokenStream
* @return 函数名
* @param ts 当前使用的 {@link TokenStream}
* @return 函数名字符串
*/
private String parseFunctionName(TokenStream ts) {
String name = ts.expectType(TokenType.IDENTIFIER).getLexeme();
@ -118,9 +133,9 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 解析函数尾部匹配 "end function" + 换行
* 解析函数结束标记 {@code end function}
*
* @param ts TokenStream
* @param ts 当前使用的 {@link TokenStream}
*/
private void parseFunctionFooter(TokenStream ts) {
ts.expect("end");
@ -129,16 +144,19 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 解析参数区块参数列表支持在声明行尾添加注释
* 格式
* 解析函数参数列表
*
* <p>
* 支持声明后附加注释格式示例
* <pre>
* parameter:
* declare x: int // 注释
* ...
* declare x: int // 说明文字
* declare y: float
* </pre>
* </p>
*
* @param ts TokenStream
* @return ParameterNode 列表
* @param ts 当前使用的 {@link TokenStream}
* @return 所有参数节点的列表
*/
private List<ParameterNode> parseParameters(TokenStream ts) {
ts.expect("parameter");
@ -162,7 +180,6 @@ public class FunctionParser implements TopLevelParser {
String pname = ts.expectType(TokenType.IDENTIFIER).getLexeme();
ts.expect(":");
String ptype = ts.expectType(TokenType.TYPE).getLexeme();
// 跳过行内注释
skipComments(ts);
ts.expectType(TokenType.NEWLINE);
list.add(new ParameterNode(pname, ptype));
@ -171,35 +188,37 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 解析返回类型区块支持在类型声明前后添加注释
* 格式<pre>return_type: TYPE</pre>
* 解析返回类型声明
*
* @param ts TokenStream
* @return 返回类型字符串
* <p>
* 格式为 {@code return_type: TYPE}支持前置或行尾注释
* </p>
*
* @param ts 当前使用的 {@link TokenStream}
* @return 返回类型名称字符串
*/
private String parseReturnType(TokenStream ts) {
ts.expect("return_type");
ts.expect(":");
// 跳过块前注释
skipComments(ts);
// 捕获类型 token
Token typeToken = ts.expectType(TokenType.TYPE);
String rtype = typeToken.getLexeme();
// 跳过行内注释
skipComments(ts);
// 匹配换行
ts.expectType(TokenType.NEWLINE);
// 跳过多余空行
skipNewlines(ts);
return rtype;
}
/**
* 解析函数体区块直到遇到 "end body"
* 解析函数体区块直到遇到 {@code end body}
*
* @param ctx ParserContext
* @param ts TokenStream
* @return StatementNode 列表
* <p>
* 每一行由对应的语句解析器处理可嵌套控制结构返回语句表达式等
* </p>
*
* @param ctx 当前解析上下文
* @param ts 当前使用的 {@link TokenStream}
* @return 所有函数体语句节点的列表
*/
private List<StatementNode> parseFunctionBody(ParserContext ctx, TokenStream ts) {
ts.expect("body");
@ -228,9 +247,9 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 跳过所有连续的注释行COMMENT
* 跳过连续的注释 token类型 {@code COMMENT}
*
* @param ts TokenStream
* @param ts 当前使用的 {@link TokenStream}
*/
private void skipComments(TokenStream ts) {
while (ts.peek().getType() == TokenType.COMMENT) {
@ -239,13 +258,13 @@ public class FunctionParser implements TopLevelParser {
}
/**
* 跳过所有连续的空行NEWLINE
* 跳过连续的空行 token类型 {@code NEWLINE}
*
* @param ts TokenStream
* @param ts 当前使用的 {@link TokenStream}
*/
private void skipNewlines(TokenStream ts) {
while (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
}
}
}
}

View File

@ -8,36 +8,57 @@ import java.util.ArrayList;
import java.util.List;
/**
* 负责解析 import 语句的解析器
* 支持在一行中导入多个模块格式如
* {@code import: mod1, mod2, mod3}
* {@code ImportParser} 类用于解析源码中的 import 导入语句
* <p>
* 支持以下格式的语法
* <pre>
* import: module1, module2, module3
* </pre>
* 每个模块名称必须为合法的标识符多个模块之间使用英文逗号,分隔语句末尾必须以换行符结尾
* 本类的职责是识别并提取所有导入模块的名称并将其封装为 {@link ImportNode} 节点供后续语法树构建或语义分析阶段使用
*/
public class ImportParser {
/**
* 解析一条 import 声明语句返回对应的 {@link ImportNode} 列表
* 格式必须为{@code import: MODULE[, MODULE]* NEWLINE}
* 解析 import 语句并返回表示被导入模块的语法树节点列表
* <p>
* 该方法会依次执行以下操作
* <ol>
* <li>确认当前语句以关键字 {@code import} 开头</li>
* <li>确认后跟一个冒号 {@code :}</li>
* <li>解析至少一个模块名称标识符多个模块使用逗号分隔</li>
* <li>确认语句以换行符 {@code NEWLINE} 结束</li>
* </ol>
* 若语法不符合上述规则将在解析过程中抛出异常
*
* @param ctx 当前的解析上下文
* @return 所有被导入模块的节点列表
* @param ctx 表示当前解析器所处的上下文环境包含词法流及语法状态信息
* @return 返回一个包含所有被导入模块的 {@link ImportNode} 实例列表
*/
public List<ImportNode> parse(ParserContext ctx) {
// 期望第一个 token "import" 关键字
ctx.getTokens().expect("import");
// 紧接其后必须是冒号 ":"
ctx.getTokens().expect(":");
// 用于存储解析得到的 ImportNode 对象
List<ImportNode> imports = new ArrayList<>();
// 读取第一个模块名然后继续读取逗号分隔的模块名
// 解析一个或多个模块名标识符允许使用逗号分隔多个模块
do {
// 获取当前标识符类型的词法单元并提取其原始词素
String mod = ctx.getTokens()
.expectType(TokenType.IDENTIFIER)
.getLexeme();
imports.add(new ImportNode(mod));
} while (ctx.getTokens().match(","));
// 消费行尾换行符
// 创建 ImportNode 节点并加入列表
imports.add(new ImportNode(mod));
} while (ctx.getTokens().match(",")); // 如果匹配到逗号继续解析下一个模块名
// 最后必须匹配换行符标志 import 语句的结束
ctx.getTokens().expectType(TokenType.NEWLINE);
// 返回完整的 ImportNode 列表
return imports;
}
}

View File

@ -13,79 +13,84 @@ import java.util.ArrayList;
import java.util.List;
/**
* 模块解析器用于解析形如 module 块的顶层结构
* 语法示例
* <pre>{@code
* module: my.module
* import: mod1, mod2
* function: ...
* end module
* }</pre>
* 支持导入语句和多个函数定义允许中间包含空行
* {@code ModuleParser} 类负责解析源码中的模块定义结构属于顶层结构解析器的一种
* <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>
* </ol>
* 所有语法错误将在解析过程中抛出异常以便准确反馈问题位置和原因
*
* @param ctx 解析上下文
* @return 模块节点包含模块名导入列表和函数列表
* @param ctx 当前解析器上下文包含词法流状态信息等
* @return 返回一个 {@link ModuleNode} 实例表示完整模块的语法结构
* @throws IllegalStateException 当模块体中出现未识别的语句时抛出
*/
@Override
public ModuleNode parse(ParserContext ctx) {
// 获取当前上下文中提供的词法流
TokenStream ts = ctx.getTokens();
// 期望 "module" 开头
// 期望模块声明以关键字 "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();
// 解析模块主体内容支持多个 import function 语句
// 进入模块主体内容解析循环
while (true) {
// 跳过空行多个空行不会导致错误
// 跳过所有空行即连续的 NEWLINE
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
continue;
}
// 模块体结束判断遇到 "end" 关键字
// 若遇到 "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);
}
}
// 解析模块结尾结构确保以 "end module"
// 确保模块体 "end module"
ts.expect("end");
ts.expect("module");
ts.expectType(TokenType.NEWLINE);
// 返回构建完成的模块语法树节点
// 构建并返回完整的模块语法树节点
return new ModuleNode(name, imports, functions);
}
}

View File

@ -7,47 +7,68 @@ import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.expression.PrattExpressionParser;
/**
* 解析变量声明语句的解析器
* 支持语法格式
* {@code DeclarationStatementParser} 类负责解析变量声明语句是语句级解析器的一部分
* <p>
* 本解析器支持以下两种形式的声明语法
* <pre>{@code
* declare name:type
* declare name:type = expression
* declare myVar:Integer
* declare myVar:Integer = 42 + 3
* }</pre>
* 每条语句必须以换行NEWLINE结束
* 其中
* <ul>
* <li>{@code myVar} 为变量名必须为标识符类型</li>
* <li>{@code Integer} 为类型标注必须为类型标记</li>
* <li>可选的初始化表达式由 {@link PrattExpressionParser} 解析</li>
* <li>每条声明语句必须以换行符{@code NEWLINE}结束</li>
* </ul>
* 若语法不满足上述结构将在解析过程中抛出异常
*/
public class DeclarationStatementParser implements StatementParser {
/**
* 解析一条 declare 声明语句
* 解析一条 {@code declare} 声明语句并返回对应的抽象语法树节点 {@link DeclarationNode}
* <p>
* 解析流程如下
* <ol>
* <li>匹配关键字 {@code declare}</li>
* <li>读取变量名称标识符类型</li>
* <li>读取类型标注在冒号后要求为 {@code TYPE} 类型</li>
* <li>若存在 {@code =}则继续解析其后的表达式作为初始化值</li>
* <li>最终必须匹配 {@code NEWLINE} 表示语句结束</li>
* </ol>
* 若遇到非法语法结构将触发异常并中断解析过程
*
* @param ctx 当前解析上下文
* @return 构造好的 {@link DeclarationNode} AST 节点
* @param ctx 当前语法解析上下文包含词法流错误信息等
* @return 返回一个 {@link DeclarationNode} 节点表示解析完成的声明语法结构
*/
@Override
public DeclarationNode parse(ParserContext ctx) {
// 声明语句必须以 "declare" 开头
ctx.getTokens().expect("declare");
// 获取变量名
// 获取变量名标识符
String name = ctx.getTokens()
.expectType(TokenType.IDENTIFIER)
.getLexeme();
// 类型标注的冒号分隔符
ctx.getTokens().expect(":");
// 获取变量类型
// 获取变量类型类型标识符
String type = ctx.getTokens()
.expectType(TokenType.TYPE)
.getLexeme();
// 可选初始化表达式
// 可选初始化表达式若存在 "="则解析等号右侧表达式
ExpressionNode init = null;
if (ctx.getTokens().match("=")) {
init = new PrattExpressionParser().parse(ctx);
}
// 声明语句必须以 NEWLINE 结束
// 声明语句必须以换行符结尾
ctx.getTokens().expectType(TokenType.NEWLINE);
// 返回构建好的声明语法树节点
return new DeclarationNode(name, type, init);
}
}

View File

@ -10,46 +10,61 @@ import org.jcnc.snow.compiler.parser.context.TokenStream;
import org.jcnc.snow.compiler.parser.expression.PrattExpressionParser;
/**
* 表达式语句解析器将赋值表达式或任意表达式作为语句进行解析
* 支持的形式包括
* <pre>
* identifier = expression
* expression
* </pre>
* 两者都必须以换行NEWLINE结尾
* {@code ExpressionStatementParser} 负责解析通用表达式语句包括赋值语句和单一表达式语句
* <p>
* 支持的语法结构如下
* <pre>{@code
* x = 1 + 2 // 赋值语句
* doSomething() // 函数调用等普通表达式语句
* }</pre>
* <ul>
* <li>若以标识符开头且后接等号 {@code =}则视为赋值语句解析为 {@link AssignmentNode}</li>
* <li>否则视为普通表达式解析为 {@link ExpressionStatementNode}</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 表达式语句节点或赋值语句节点
* @param ctx 当前解析上下文提供词法流与状态信息
* @return 返回 {@link AssignmentNode} {@link ExpressionStatementNode} 表示的语法节点
* @throws IllegalStateException 若表达式起始为关键字或语法非法
*/
@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());
}
// 判断是否是赋值语句形如identifier = expr
// 处理赋值语句格式为 identifier = expression
if (ts.peek().getType() == TokenType.IDENTIFIER
&& ts.peek(1).getLexeme().equals("=")) {
String varName = ts.next().getLexeme(); // consume identifier
ts.expect("="); // consume '='
ExpressionNode value = new PrattExpressionParser().parse(ctx);
ts.expectType(TokenType.NEWLINE);
return new AssignmentNode(varName, value);
String varName = ts.next().getLexeme(); // 消耗标识符
ts.expect("="); // 消耗等号
ExpressionNode value = new PrattExpressionParser().parse(ctx); // 解析表达式
ts.expectType(TokenType.NEWLINE); // 语句必须以换行符结束
return new AssignmentNode(varName, value); // 返回赋值节点
}
// 普通表达式语句如函数调用
// 处理普通表达式语句如函数调用字面量运算表达式等
ExpressionNode expr = new PrattExpressionParser().parse(ctx);
ts.expectType(TokenType.NEWLINE);
return new ExpressionStatementNode(expr);
ts.expectType(TokenType.NEWLINE); // 语句必须以换行符结束
return new ExpressionStatementNode(expr); // 返回表达式语句节点
}
}

View File

@ -12,75 +12,96 @@ import java.util.ArrayList;
import java.util.List;
/**
* 解析 if 语句
* {@code IfStatementParser} 类负责解析 if 条件语句是语句级解析器中的条件分支处理器
* <p>
* 支持格式
* 本解析器支持以下结构的条件语法
* <pre>{@code
* if <condition> then
* <then-statements>
* <then-statements>
* [else
* <else-statements>]
* <else-statements>]
* end if
* }</pre>
* 其中
* <ul>
* <li>{@code <condition>} 为任意可解析的布尔或数值表达式使用 {@link PrattExpressionParser} 解析</li>
* <li>{@code <then-statements>} {@code <else-statements>} 可包含多条语句自动跳过空行</li>
* <li>{@code else} 分支为可选若存在必须紧跟换行与语句</li>
* <li>{@code end if} 为终止标识表示整个 if 语句块的结束</li>
* </ul>
* 所有语句的实际解析由 {@link StatementParserFactory} 根据关键词动态分派处理
*/
public class IfStatementParser implements StatementParser {
/**
* 解析一条完整的 if 条件语句返回语法树中对应的 {@link IfNode} 节点
* <p>
* 本方法支持 then 分支和可选的 else 分支并确保以 {@code end if} 正确结尾
* 在解析过程中自动跳过空行遇到未知关键字或不符合预期的 token 时会抛出异常
*
* @param ctx 当前的语法解析上下文包含 token 流和语义环境
* @return 构造完成的 {@link IfNode}包含条件表达式then 分支和 else 分支语句列表
* @throws IllegalStateException 若语法结构不完整或存在非法 token
*/
@Override
public IfNode parse(ParserContext ctx) {
var ts = ctx.getTokens(); // 获取 Token 流管理器
var ts = ctx.getTokens(); // 获取 token 流引用
ts.expect("if"); // 消费 "if" 关键字
// 消耗起始关键字 "if"
ts.expect("if");
// 使用 Pratt 解析器解析布尔表达式或其他条件表达式
// 使用 Pratt 算法解析 if 条件表达式
var condition = new PrattExpressionParser().parse(ctx);
// 消费 "then" 关键字和随后的换行符
// 条件表达式后必须紧跟 "then" 和换行
ts.expect("then");
ts.expectType(TokenType.NEWLINE);
// 准备容器存储 then else 分支语句
// 初始化 then else 分支语句列表
List<StatementNode> thenBranch = new ArrayList<>();
List<StatementNode> elseBranch = new ArrayList<>();
// -------------------
// 解析 THEN 分支语句
// -------------------
// -------------------------
// 解析 THEN 分支语句
// -------------------------
while (true) {
Token peek = ts.peek();
// 忽略空行
// 跳过空行
if (peek.getType() == TokenType.NEWLINE) {
ts.next();
continue;
}
// 检测是否进入 else end跳出 then 区块
// 遇到 else end 表示 then 分支结束
if (peek.getType() == TokenType.KEYWORD &&
(peek.getLexeme().equals("else") || peek.getLexeme().equals("end"))) {
break;
}
// 提取关键词交由 StatementParserFactory 派发对应解析器
// 获取当前语句的关键字调用工厂获取对应解析器
String keyword = peek.getType() == TokenType.KEYWORD ? peek.getLexeme() : "";
StatementNode stmt = StatementParserFactory.get(keyword).parse(ctx);
thenBranch.add(stmt);
}
// -------------------
// 解析 ELSE 分支语句
// -------------------
// -------------------------
// 解析 ELSE 分支语句可选
// -------------------------
if (ts.peek().getLexeme().equals("else")) {
ts.next(); // "else"
ts.expectType(TokenType.NEWLINE); // 费换行
ts.next(); // "else"
ts.expectType(TokenType.NEWLINE); // 耗换行符
while (true) {
Token peek = ts.peek();
// 忽略空行
// 跳过空行
if (peek.getType() == TokenType.NEWLINE) {
ts.next();
continue;
}
// "end" 关键字表示 else 结束
// "end" 表示 else 分支结束
if (peek.getType() == TokenType.KEYWORD && peek.getLexeme().equals("end")) {
break;
}
@ -91,12 +112,14 @@ public class IfStatementParser implements StatementParser {
}
}
// 统一消费 "end if" 和其后的换行
// -------------------------
// 统一结束处理end if
// -------------------------
ts.expect("end");
ts.expect("if");
ts.expectType(TokenType.NEWLINE);
// AST 节点返回 IfNode 包含条件then 分支else 分支
// 建并返回 IfNode包含条件then 分支和 else 分支
return new IfNode(condition, thenBranch, elseBranch);
}
}

View File

@ -18,7 +18,9 @@ import java.util.List;
import java.util.Map;
/**
* 用于解析 loop 语句块支持如下结构
* {@code LoopStatementParser} 类负责解析自定义结构化的 {@code loop} 语句块
* <p>
* 该语法结构参考了传统的 for-loop并将其拆解为命名的语义区块
* <pre>{@code
* loop:
* initializer:
@ -28,31 +30,53 @@ import java.util.Map;
* update:
* i = i + 1
* body:
* ...语句...
* print(i)
* end body
* end loop
* }</pre>
* 使用 FlexibleSectionParser 解析各区块并统一入口出口处理
*
* 各区块说明
* <ul>
* <li>{@code initializer}初始化语句通常为变量声明</li>
* <li>{@code condition}循环判断条件必须为布尔或数值表达式</li>
* <li>{@code update}每轮执行后更新逻辑通常为赋值语句</li>
* <li>{@code body}主执行语句块支持任意多条语句</li>
* </ul>
* 本类依赖 {@link FlexibleSectionParser} 实现各区块的统一处理确保结构明确可扩展
*/
public class LoopStatementParser implements StatementParser {
/**
* 解析 {@code loop} 语句块构建出对应的 {@link LoopNode} 抽象语法树节点
* <p>
* 本方法会按顺序检查各个命名区块可乱序书写并分别绑定其对应语义解析器
* <ul>
* <li>通过 {@link ParserUtils#matchHeader} 匹配区块开头</li>
* <li>通过 {@link FlexibleSectionParser} 派发区块逻辑</li>
* <li>通过 {@link StatementParserFactory} 调用实际语句解析</li>
* <li>最后以 {@code end loop} 表示结构终止</li>
* </ul>
*
* @param ctx 当前解析上下文
* @return {@link LoopNode}包含初始化条件更新与循环体等信息
*/
@Override
public LoopNode parse(ParserContext ctx) {
TokenStream ts = ctx.getTokens();
// 匹配 loop: 开头
// 匹配 loop: 起始语法
ParserUtils.matchHeader(ts, "loop");
// 各区块中间值容器模拟引用
// 使用数组模拟引用以便在 lambda 中写入Java 不支持闭包内修改局部变量
final StatementNode[] initializer = new StatementNode[1];
final ExpressionNode[] condition = new ExpressionNode[1];
final AssignmentNode[] update = new AssignmentNode[1];
final List<StatementNode> body = new ArrayList<>();
// 构造区块定义
// 定义各命名区块的识别与处理逻辑
Map<String, FlexibleSectionParser.SectionDefinition> sections = new HashMap<>();
// initializer 区块解析初始化语句
// initializer 区块仅支持一条语句通常为 declare
sections.put("initializer", new FlexibleSectionParser.SectionDefinition(
ts1 -> ts1.peek().getLexeme().equals("initializer"),
(ctx1, ts1) -> {
@ -62,7 +86,7 @@ public class LoopStatementParser implements StatementParser {
}
));
// condition 区块解析布尔条件表达式
// condition 区块支持任意可解析为布尔的表达式
sections.put("condition", new FlexibleSectionParser.SectionDefinition(
ts1 -> ts1.peek().getLexeme().equals("condition"),
(ctx1, ts1) -> {
@ -73,7 +97,7 @@ public class LoopStatementParser implements StatementParser {
}
));
// update 区块解析变量赋值表达式
// update 区块目前仅支持单一变量赋值语句
sections.put("update", new FlexibleSectionParser.SectionDefinition(
ts1 -> ts1.peek().getLexeme().equals("update"),
(ctx1, ts1) -> {
@ -87,14 +111,14 @@ public class LoopStatementParser implements StatementParser {
}
));
// body 区块解析语句块
// body 区块支持多条语句直到遇到 end body
sections.put("body", new FlexibleSectionParser.SectionDefinition(
ts1 -> ts1.peek().getLexeme().equals("body"),
(ctx1, ts1) -> {
ParserUtils.matchHeader(ts1, "body");
while (!(ts1.peek().getLexeme().equals("end") &&
ts1.peek(1).getLexeme().equals("body"))) {
ts1.peek(1).getLexeme().equals("body"))) {
String keyword = ts1.peek().getType() == TokenType.KEYWORD
? ts1.peek().getLexeme()
: "";
@ -109,13 +133,13 @@ public class LoopStatementParser implements StatementParser {
}
));
// 使用 FlexibleSectionParser 解析所有结构部分
// 使用通用区块解析器处理各命名结构块
FlexibleSectionParser.parse(ctx, ts, sections);
// 匹配 end loop
// 解析结尾的 end loop 标记
ParserUtils.matchFooter(ts, "loop");
// 构造并返回 LoopNode 抽象语法树节点
// 返回构造完成的 LoopNode
return new LoopNode(initializer[0], condition[0], update[0], body);
}
}

View File

@ -7,35 +7,48 @@ import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.expression.PrattExpressionParser;
/**
* 用于解析 return 语句
* 支持有无返回值两种形式
* {@code ReturnStatementParser} 负责解析 return 语句是语句级解析器的一部分
* <p>
* 支持以下两种 return 语句形式
* <pre>{@code
* return
* return expression
* return // 无返回值
* return expression // 带返回值
* }</pre>
* 语句必须以换行符NEWLINE结束
* 所有 return 语句都必须以换行符{@code NEWLINE}结束返回值表达式若存在 {@link PrattExpressionParser} 负责解析
* 若语法结构不满足要求将在解析过程中抛出异常
*/
public class ReturnStatementParser implements StatementParser {
/**
* 解析 return 语句并构建 {@link ReturnNode}
* 解析一条 return 语句并返回对应的 {@link ReturnNode} 抽象语法树节点
* <p>
* 解析逻辑如下
* <ol>
* <li>匹配起始关键字 {@code return}</li>
* <li>判断其后是否为 {@code NEWLINE}若否则表示存在返回值表达式</li>
* <li>使用 {@link PrattExpressionParser} 解析返回值表达式若存在</li>
* <li>最后匹配换行符标志语句结束</li>
* </ol>
*
* @param ctx 当前解析上下文
* @return 表示 return 语句的 AST 节点
* @param ctx 当前解析上下文包含词法流与语法状态
* @return 构造完成的 {@link ReturnNode}表示 return 语句的语法树节点
*/
@Override
public ReturnNode parse(ParserContext ctx) {
// 消耗 "return" 关键字
ctx.getTokens().expect("return");
ExpressionNode expr = null;
// 如果不是换行说明有返回值
// 如果下一 token 不是换行符说明存在返回值表达式
if (ctx.getTokens().peek().getType() != TokenType.NEWLINE) {
expr = new PrattExpressionParser().parse(ctx);
}
// return 语句必须以换行符结束
ctx.getTokens().expectType(TokenType.NEWLINE);
// 构建并返回 ReturnNode可能为空表达式
return new ReturnNode(expr);
}
}

View File

@ -4,16 +4,23 @@ import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.ast.base.StatementNode;
/**
* 语句解析器接口用于将解析上下文中的 Token 转换为 {@link StatementNode}
* 各类语句如声明赋值条件循环返回等均应实现该接口
* {@code StatementParser} 是所有语句解析器的通用接口
* <p>
* 其职责是从给定的 {@link ParserContext} 中读取并分析当前语句构造并返回相应的抽象语法树节点
* 所有语句类型如变量声明赋值语句控制结构函数返回等应提供对应的实现类
*
* <p>
* 通常此接口的实现由 {@code StatementParserFactory} 根据当前关键字动态派发用于解析模块体
* 条件分支循环体或其他语句块中的单条语句
*/
public interface StatementParser {
/**
* 从给定的解析上下文中解析出一个语句节点
* 解析一条语句将其从词法表示转换为结构化语法树节点
*
* @param ctx 当前的解析上下文
* @return 表示语句的 AST 节点
* @param ctx 当前的解析上下文提供 token 状态与符号环境等
* @return 表示该语句的 AST 节点类型为 {@link StatementNode} 或其子类
* @throws IllegalStateException 若语法非法或结构不完整
*/
StatementNode parse(ParserContext ctx);
}

View File

@ -7,19 +7,32 @@ import org.jcnc.snow.compiler.parser.ast.base.Node;
import java.util.*;
/**
* ASTJsonSerializer 工具类
* {@code ASTJsonSerializer} 是抽象语法树AST序列化工具类
* <p>
* 将编译器生成的 AST抽象语法树节点列表转换为通用的 Map/List 结构
* 并借助 JSONParser.toJson(Object) 方法序列化为 JSON 字符串
* 该工具可将编译器内部构建的 AST 节点对象转换为通用的 {@code Map} {@code List} 结构
* 并可借助 {@code JSONParser.toJson(Object)} 方法将其序列化为 JSON 字符串用于调试
* 可视化或跨语言数据传输
* <p>
* 支持的节点类型包括ModuleNodeFunctionNodeDeclarationNode
* AssignmentNodeIfNodeLoopNodeReturnNodeExpressionStatementNode
* 以及各种 ExpressionNode BinaryExpressionNodeIdentifierNode
* 支持的节点类型包括
* <ul>
* <li>{@link ModuleNode}</li>
* <li>{@link FunctionNode}</li>
* <li>{@link DeclarationNode}</li>
* <li>{@link AssignmentNode}</li>
* <li>{@link IfNode}</li>
* <li>{@link LoopNode}</li>
* <li>{@link ReturnNode}</li>
* <li>{@link ExpressionStatementNode}</li>
* <li>各类 {@link ExpressionNode} 子类型 {@code BinaryExpressionNode}, {@code IdentifierNode} </li>
* </ul>
*/
public class ASTJsonSerializer {
/**
* 快速创建一个 LinkedHashMap并写入 type 字段
* 创建包含 {@code type} 字段的节点 Map用于标识节点类型
*
* @param type 节点类型字符串
* @return 一个初始化后的 Map 实例
*/
private static Map<String, Object> newNodeMap(String type) {
Map<String, Object> m = new LinkedHashMap<>();
@ -28,7 +41,11 @@ public class ASTJsonSerializer {
}
/**
* 用于构建表达式节点的 Map
* 构建表达式节点的 Map 表示支持动态键值对传参
*
* @param type 表达式类型
* @param kv 可变参数key-value 键值对
* @return 表示表达式节点的 Map
*/
private static Map<String, Object> exprMap(String type, Object... kv) {
Map<String, Object> m = new LinkedHashMap<>();
@ -40,10 +57,10 @@ public class ASTJsonSerializer {
}
/**
* AST 根节点列表序列化 JSON 字符串
* AST 根节点列表转换 JSON 字符串
*
* @param ast 表示抽象语法树根节点的 List<Node>
* @return 对应的 JSON 格式字符串
* @param ast 表示顶层语法树结构的节点列表
* @return 对应的 JSON 字符串表示形式
*/
public static String toJsonString(List<Node> ast) {
List<Object> list = new ArrayList<>(ast.size());
@ -54,20 +71,20 @@ public class ASTJsonSerializer {
}
/**
* 递归地将 AST 节点转换为 Map List 等通用结构
* 便于后续统一序列化
* 将任意 AST 节点递归转换为 Map/List 结构
*
* @param n 要转换的 AST 节点
* @return Map/List 表示的结构化数据
*/
private static Object nodeToMap(Node n) {
return switch (n) {
// 模块节点
case ModuleNode(String name, List<ImportNode> imports, List<FunctionNode> functions) -> {
Map<String, Object> map = newNodeMap("Module");
map.put("name", name);
List<Object> imps = new ArrayList<>(imports.size());
for (ImportNode imp : imports) {
imps.add(Map.of(
"type", "Import",
"module", imp.moduleName()
));
imps.add(Map.of("type", "Import", "module", imp.moduleName()));
}
map.put("imports", imps);
List<Object> funcs = new ArrayList<>(functions.size());
@ -77,15 +94,13 @@ public class ASTJsonSerializer {
map.put("functions", funcs);
yield map;
}
// 函数定义节点
case FunctionNode f -> {
Map<String, Object> map = newNodeMap("Function");
map.put("name", f.name());
List<Object> params = new ArrayList<>(f.parameters().size());
for (var p : f.parameters()) {
params.add(Map.of(
"name", p.name(),
"type", p.type()
));
params.add(Map.of("name", p.name(), "type", p.type()));
}
map.put("parameters", params);
map.put("returnType", f.returnType());
@ -96,19 +111,20 @@ public class ASTJsonSerializer {
map.put("body", body);
yield map;
}
// 变量声明节点
case DeclarationNode d -> {
Map<String, Object> map = newNodeMap("Declaration");
map.put("name", d.getName());
map.put("varType", d.getType());
map.put("initializer",
d.getInitializer().map(ASTJsonSerializer::exprToMap).orElse(null)
);
map.put("initializer", d.getInitializer().map(ASTJsonSerializer::exprToMap).orElse(null));
yield map;
}
// 赋值语句节点
case AssignmentNode a -> exprMap("Assignment",
"variable", a.variable(),
"value", exprToMap(a.value())
);
// 条件语句节点
case IfNode i -> {
Map<String, Object> map = newNodeMap("If");
map.put("condition", exprToMap(i.condition()));
@ -122,6 +138,7 @@ public class ASTJsonSerializer {
}
yield map;
}
// 循环语句节点
case LoopNode l -> {
Map<String, Object> map = newNodeMap("Loop");
map.put("initializer", l.initializer() != null ? nodeToMap(l.initializer()) : null);
@ -132,21 +149,28 @@ public class ASTJsonSerializer {
map.put("body", body);
yield map;
}
// return 语句节点
case ReturnNode r -> {
Map<String, Object> map = newNodeMap("Return");
r.getExpression().ifPresent(expr -> map.put("value", exprToMap(expr)));
yield map;
}
// 表达式语句节点
case ExpressionStatementNode e -> exprMap("ExpressionStatement",
"expression", exprToMap(e.expression())
);
// 通用表达式节点
case ExpressionNode expressionNode -> exprToMap(expressionNode);
// 其他类型兜底处理
default -> Map.of("type", n.getClass().getSimpleName());
};
}
/**
* 将表达式节点转换为 Map 表示
* 将表达式类型节点转换为 Map 表示形式
*
* @param expr 表达式 AST 节点
* @return 表示该表达式的 Map
*/
private static Object exprToMap(ExpressionNode expr) {
return switch (expr) {
@ -167,7 +191,8 @@ public class ASTJsonSerializer {
"object", exprToMap(object),
"member", member
);
// 默认兜底处理只写类型
default -> Map.of("type", expr.getClass().getSimpleName());
};
}
}
}

View File

@ -9,63 +9,84 @@ import java.util.function.BiConsumer;
import java.util.function.Predicate;
/**
* 通用的解析器用于解析结构化的内容部分完全解耦合关键字和语法
* {@code FlexibleSectionParser} 是一个通用的语法块解析工具
* <p>
* FlexibleSectionParser 提供了一个灵活的机制来解析可变的语法块每个语法块的解析逻辑通过外部提供
* 该类仅负责按顺序解析不同的区块直到遇到结束标记同时能够跳过并收集注释供后续使用
* <p>
* 使用此类可以解析如函数定义中的多个部分如参数返回类型函数体等而无需在解析器中硬编码这些结构
* 并且保留注释信息以便 IDE 或工具链进行注释跳转重构等操作
* 该工具支持解析由关键字标识的多段结构化区块内容常用于解析函数模块循环等语法单元中的命名子结构
* 相比传统硬编码方式提供更灵活可组合的解析能力允许解析器模块动态注册处理逻辑而非将所有逻辑写死在主流程中
*
* <p>典型应用包括
* <ul>
* <li>函数体解析中的 {@code params}{@code returns}{@code body} 等部分</li>
* <li>模块定义中的 {@code imports}{@code functions} 等部分</li>
* <li>用户自定义 DSL 的可扩展语法结构</li>
* </ul>
*
* <p>该工具具备以下能力
* <ul>
* <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 用于逐个查看或消耗 token
* @param sectionDefinitions 区块定义映射每个关键字 "params", "returns", "body"对应一个区块定义
* @param ctx 当前解析上下文提供语法环境与作用域信息
* @param tokens 当前 token
* @param sectionDefinitions 各个区块的定义映射key 为关键字value 为判断 + 解析逻辑组合
* @throws RuntimeException 若出现无法识别的关键字或未满足的匹配条件
*/
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)) {
// 遇到 'end' 则终止解析
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);
}
}
}
/**
* 跳过所有连续的注释行和空行
* 跳过连续出现的注释行或空行NEWLINE
* <p>
* 该方法用于在区块之间清理无效 token避免影响结构判断
*
* @param tokens 词法流
* @param tokens 当前 token
*/
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;
@ -73,13 +94,17 @@ public class FlexibleSectionParser {
}
/**
* 区块定义类表示一个语法区块的匹配条件和解析逻辑
* 每个区块由两个部分组成
* - 条件用于判断当前 token 流是否应进入该区块的解析
* - 解析器具体的解析逻辑通常是消费若干 token 并更新解析上下文
* 表示一个结构区块的定义包含匹配条件与解析器
* <p>
* 每个区块由两部分组成
* <ul>
* <li>{@code condition}用于判断当前 token 是否应进入该区块</li>
* <li>{@code parser}该区块对应的实际解析逻辑</li>
* </ul>
* 可实现懒加载多语言支持或 DSL 的结构化扩展
*
* @param condition 匹配条件返回 true 表示此区块应被解析
* @param parser 实际解析逻辑
* @param condition 判断是否触发该区块的谓词函数
* @param parser 区块解析逻辑消费语法上下文与 token
*/
public record SectionDefinition(Predicate<TokenStream> condition,
BiConsumer<ParserContext, TokenStream> parser) {

View File

@ -16,7 +16,7 @@ import java.util.Map.Entry;
* 4. 序列化器基于 StringBuilder预分配容量减少中间字符串创建
*/
public class JSONParser {
// 私有构造禁止外部实例化
private JSONParser() {}
/**

View File

@ -4,44 +4,61 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
import org.jcnc.snow.compiler.parser.context.TokenStream;
/**
* 语法结构通用辅助工具类
* 提供常用的结构匹配和容错功能
* {@code ParserUtils} 是语法结构解析过程中的通用辅助工具类
* <p>
* 提供一系列静态方法用于标准语法结构如结构头结构尾的匹配校验以及常用的容错处理操作
* 这些方法可在函数定义模块定义循环条件语句等语法块中复用有效减少冗余代码提高解析器稳定性
*
* <p>主要功能包括
* <ul>
* <li>匹配结构性语法起始标记 {@code loop:}{@code function:}</li>
* <li>匹配结构性语法结尾标记 {@code end loop}{@code end function}</li>
* <li>跳过多余换行符增强语法容错性</li>
* </ul>
*/
public class ParserUtils {
/**
* 匹配形如 "keyword:" 的语法结构头部并跳过换行
* 匹配结构语法的标准起始格式 {@code keyword:}并跳过其后的换行符
* <p>
* 该方法适用于需要标识结构起点的语法元素 {@code loop:}{@code function:}
* 若格式不匹配将抛出语法异常
*
* @param ts Token
* @param keyword 结构标识关键字 "loop", "function"
* @param ts 当前的 token
* @param keyword 结构起始关键字 "loop", "function", "initializer"
*/
public static void matchHeader(TokenStream ts, String keyword) {
ts.expect(keyword);
ts.expect(":");
ts.expectType(TokenType.NEWLINE);
skipNewlines(ts);
ts.expect(keyword); // 匹配关键字
ts.expect(":"); // 匹配冒号
ts.expectType(TokenType.NEWLINE); // 匹配行尾换行
skipNewlines(ts); // 跳过多余空行
}
/**
* 匹配形如 "end keyword" 的语法结构结尾
* 匹配结构语法的标准结尾格式 {@code end keyword}
* <p>
* 该方法用于验证结构块的结束例如 {@code end loop}{@code end if}
* 若格式不正确将抛出异常
*
* @param ts Token
* @param keyword 结构标识关键字 "loop", "function"
* @param ts 当前的 token
* @param keyword 对应的结构关键字必须与开始标记一致
*/
public static void matchFooter(TokenStream ts, String keyword) {
ts.expect("end");
ts.expect(keyword);
ts.expectType(TokenType.NEWLINE);
ts.expect("end"); // 匹配 'end'
ts.expect(keyword); // 匹配结构名
ts.expectType(TokenType.NEWLINE); // 匹配行尾
}
/**
* 跳过连续的换行符常用于容错与美化语法结构
* 跳过连续的换行符{@code NEWLINE}
* <p>
* 通常用于解析器之间的过渡阶段以消除格式干扰提升容错性
*
* @param ts Token
* @param ts 当前的 token
*/
public static void skipNewlines(TokenStream ts) {
while (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
ts.next(); // 连续消费换行符
}
}
}