SCompiler/doc/Snow语言文档.md

72 KiB
Raw Blame History

Snow 语言文档

整体架构与运行流程

SCompiler 项目将源代码依次经过多个阶段处理,最终生成字节码并由自定义虚拟机执行。编译器前端包括词法分析、语法分析和语义分析接着将AST转换为中间表示IR再由编译器后端生成虚拟机可执行的指令序列,最后交由虚拟机引擎解释运行。具体流程如下:

  1. 词法分析LexerEngine读取源码文本输出有序的Token序列。
  2. 语法分析ParserEngine将Token序列解析为抽象语法树AST节点列表。本语言采用类似Python的模块/函数/缩进语法,由顶层的ModuleParser解析模块,FunctionParser解析函数,内部使用运算符优先解析表达式。
  3. 语义分析SemanticAnalyzerRunner遍历AST检查标识符引用、类型匹配等语义规则将符号登记到符号表并报告语义错误。
  4. IR生成IRProgramBuilder遍历AST构建中间表示IRIntermediate Representation每个函数生成对应的IRFunction,以三地址码形式表示计算过程。
  5. 目标代码生成对每个IR函数先进行寄存器分配将IR的虚拟寄存器映射到虚拟机的局部槽位再由VMCodeGenerator将IR指令翻译为等价的虚拟机指令序列。例如对一个加法表达式产生形如LOADADDSTORE的指令。函数调用指令在生成时会填充或留存被调函数地址以供回填。
  6. 虚拟机执行VirtualMachineEngine加载指令列表,初始化运行栈,然后开始取指解释执行。每条指令由命令处理器解析并操作模拟的操作数栈、局部变量表和调用栈,直至程序结束。下面将对各模块进行详细分析。

词法分析模块

词法分析器(Lexer) 将源码文本转换成Token序列。LexerEngine是核心入口类,其构造时会根据定义的规则初始化一组TokenScanner子扫描器,并立即对输入源码进行扫描。各扫描器按优先级排列,包括:

  • WhitespaceTokenScanner 跳过空白字符(空格、制表符等)
  • NewlineTokenScanner 识别换行符生成换行类型Token
  • CommentTokenScanner 识别单行或多行注释文本
  • NumberTokenScanner 识别整数和浮点数字面量
  • IdentifierTokenScanner 识别标识符和关键字(如module,if,return等)
  • StringTokenScanner 识别字符串字面量
  • OperatorTokenScanner 识别运算符符号(+、-、*、/、== 等)
  • SymbolTokenScanner 识别分隔符和符号(如逗号、括号、冒号等)
  • UnknownTokenScanner 捕获无法识别的字符并标记为错误Token

扫描过程采用 多通道扫描Lexer按字符顺序读取输入对每个字符依次尝试上述扫描器哪个扫描器的canHandle()方法返回true就交由其handle()处理。每个扫描器从LexerContext获取当前字符流状态,消费相应字符序列并通过TokenFactory创建Token加入结果列表。例如IdentifierScanner读取字母序列后由TokenFactory判断是标识符还是关键字。Lexer会跳过空白和注释不生成多余Token。扫描循环持续直到输入结尾然后显式追加一个EOF(TokenType.EOF)作为结束标记。整个词法分析的输出是有序的Token列表可供语法分析器消费。

语法分析模块

语法分析器(Parser) 将Token序列按照语言文法规则还原为抽象语法树(AST)。SCompiler语言的文法大致为模块module 模块名:及缩进的多个函数定义组成,模块以end module结束;函数定义function 函数名(参数列表):开始,内部缩进块包含若干语句,以end function结束。语句包括变量声明、赋值、条件if/else、循环loop、返回return以及表达式调用等。

Parser采用递归下降运算符优先解析相结合的方法顶层结构模块、导入、函数由不同解析器处理表达式部分使用Pratt算法。主要组件如下

  • ParserEngine 语法分析驱动器从Token流中识别顶层构造。它不断查看下一个Token如果是模块关键字则调用模块解析器直到遇到EOF。目前顶层只注册了module关键字对应的解析器。若遇到未注册的顶级标记则抛异常。ParserEngine还会跳过空行以提高容错性。(注意:源码中while (ts.isAtEnd())用来循环读取顶层构造,但isAtEnd()的实现返回值逻辑相反,实际应为!isAtEnd(),这一细节在实现中有所疏漏)

  • ModuleParser 模块解析器,实现了接口TopLevelParser。它期望当前Token序列形如module: 模块名 NEWLINE ... end module。描述了模块解析流程:首先匹配关键字module及冒号,然后读取模块名称(IDENTIFIER),并期望接着一个换行开始模块体。显示了模块体内部的解析逻辑:循环读取内部语句,跳过空行;遇到import开头则调用ImportParser解析导入语句列表遇到function则调用FunctionParser解析函数定义直到遇到end关键字标志模块结束。模块结尾强制要求end module成对出现。每个模块解析完成后构造一个AST的ModuleNode节点包含模块名、导入列表和函数列表。

  • ImportParser 导入语句解析器,处理import 模块名形式将被导入模块名加入AST的ImportNode列表。项目中ImportParser会一次解析可能的多个连续导入语句将结果返回给ModuleParser。需要注意的是目前Import仅解析但并未触发跨模块代码加载这在语义分析阶段验证

  • FunctionParser 函数定义解析器,实现了TopLevelParser接口模块内也会用到。FunctionParser利用了一个自定义的区块解析工具FlexibleSectionParser,将函数定义的不同部分(参数列表、返回类型、函数体)作为可选区块处理。函数解析流程:匹配function:标识并函数名,读入函数名后期望换行;然后通过getSectionDefinitions()预先注册三个区块解析器:parameter参数列表、return_type返回类型和body函数体。接着调用FlexibleSectionParser.parse()循环解析这几个部分,直到遇到end关键字。其中:

    • 参数列表Expect关键字parameter:后换行,然后对每行形如declare 名称: 类型的声明解析为ParameterNode节点。Parser会忽略参数声明行尾的注释。所有参数节点收集在列表中。
    • 返回类型Expectreturn_type:后读取类型名如果省略则返回类型为null表示无返回。
    • 函数体Expectbody:后逐行解析函数内部语句,直到遇到end body结束块。函数体解析使用了StatementParserFactory按当前行首关键字选择对应的语句解析器。所有解析出的StatementNode语句节点加入函数体列表。函数末尾期望end function结束。最后组装FunctionNode节点包含函数名、参数列表、返回类型和函数体AST节点列表。
  • StatementParsers 语句解析器集合,用于解析函数体内各类语句。根据语言设计,项目实现了若干种语句解析器并在StatementParserFactory中静态注册:包括变量声明(DeclarationStatementParser解析declare x: int = 表达式)、赋值语句(AssignmentParser解析x = 表达式实现在Semantic阶段处理语法上赋值被当作表达式或通过特殊形式解析)、条件语句(IfStatementParser解析if ... then ... else ... end块)、循环语句(LoopStatementParser解析loop ... end块)、返回语句(ReturnStatementParser解析return 表达式?)等。此外注册了默认的ExpressionStatementParser用于处理不匹配上述关键字的行解释为一般表达式语句。StatementParserFactory在获取解析器时会根据当前行第一个Token的字面量选择相应解析器没有匹配的则使用默认表达式语句解析器。例如函数体中的一行如果以if开头则交给IfStatementParser否则如果是标识符开头且不在其他分类就作为表达式语句解析常用于函数调用语句等

  • 表达式解析 采用 Pratt 算法实现,位于parser.expression包下。由PrattExpressionParser根据运算符优先级解析中缀表达式,结合PrefixParseletInfixParselet子类处理不同运算符。支持的表达式类型包括二元运算加减乘除、比较等由BinaryOperatorParselet按优先级处理、字面量NumberLiteralParselet, StringLiteralParselet、标识符引用(IdentifierParselet)、函数调用(CallParselet识别( )调用)、成员访问(MemberParselet识别.运算符)、括号分组(GroupingParselet)等。表达式解析结果构建对应的ExpressionNode AST节点如BinaryExpressionNode、CallExpressionNode等。优先级和结合性在Precedence枚举中定义。

  • AST节点 抽象语法树采用面向对象节点类层次表示。基类Node定义了通用接口各子类对应不同语法成分。主要节点类例如ModuleNode模块含名称和函数列表、FunctionNode函数定义含名称、参数列表、返回类型、函数体、ImportNode导入声明、ParameterNode形参定义含名和类型、Block结构节点如IfNode、LoopNode包含条件和内部语句块以及表达式节点如 IdentifierNode、NumberLiteralNode、BinaryExpressionNode 等。AST节点在解析时被实例化语义分析阶段可能会在节点上附加类型等注释信息。整个Parser输出一个AST节点列表一般情况下包含一个ModuleNode源文件级模块作为根。如果语法有误解析器会抛出带错误位置和原因的异常停止编译。

语义分析模块

语义分析器(Semantic Analyzer) 以AST为输入在不改变结构的前提下检查和补充程序的意义信息。主要任务包括标识符解析、作用域和生命周期管理、类型检查如果有静态类型、控制流合法性检查等。SCompiler的语义分析由SemanticAnalyzerRunner统一调度,内部调用SemanticAnalyzer执行具体步骤:

  1. 模块和符号表初始化SemanticAnalyzer首先收集所有ModuleNode注册模块到全局模块表(ModuleRegistry)。每个模块对应一个ModuleInfo对象存储模块名、包含的函数签名等信息。接着调用SignatureRegistrar登记函数签名。SignatureRegistrar遍历每个模块验证Import的模块是否存在于ModuleRegistry中并将该模块内每个函数的名称、参数类型、返回类型登记到ModuleInfo中。如果参数或返回类型名称无法识别例如不存在的类型则记录语义错误但尽可能继续分析。这样在正式检查函数体前就建立了跨模块的函数签名表便于检测函数调用是否合法。

  2. 函数体语义检查随后SemanticAnalyzer对每个模块内的函数逐一检查FunctionChecker执行具体逻辑。FunctionChecker会为每个函数创建局部符号表SymbolTable并将函数形参作为变量首先注册到符号表中。然后遍历函数体中的每条语句节点利用预先注册的语义分析器StatementAnalyzer/ExpressionAnalyzer对不同类型的语句和表达式进行检查。例如对变量赋值语句AssignmentAnalyzer会检查变量是否已在当前或上层作用域声明、赋值类型是否兼容对函数调用CallExpressionAnalyzer会检索被调用函数在ModuleRegistry中是否存在以及参数数量和类型是否匹配被调函数签名等对控制结构如IfAnalyzer检查条件表达式类型预期为布尔以及then/else分支的返回路径一致性LoopAnalyzer检查循环初始化和更新部分等。每个Analyzer通过Context上下文对象获取所需信息并记录错误。符号表在进入函数时创建离开函数时销毁当前实现中无嵌套块作用域每个函数体使用一个SymbolTable参数和局部变量都登记其中。对于未声明的变量或不支持的语法分析器将添加SemanticError错误对象。SemanticAnalyzer汇总所有错误SemanticAnalysisReporter统一输出错误列表;如果存在语义错误则终止后续编译流程。

项目提供了一些基础的语义检查实现变量重复定义检测、变量未声明就使用、函数重复声明、函数调用不存在、参数不匹配等。类型系统方面SCompiler目前是静态类型雏形定义了BuiltinTypeRegistry含基本类型int,string,float,bool,double等枚举和值对象尚未严格执行类型检查。例如变量声明了类型但赋值时类型不符的情况Analyzer可能未完全实现检查。在SignatureRegistrar会对未知类型记录错误但像算术运算左右项类型兼容性、条件表达式类型等并未全面检查。这是因为当前语言实现对类型要求不严格更像动态类型处理改进加强类型检查逻辑在ExpressionAnalyzer中加入对操作数类型的校验对不匹配情况给出错误。

语义分析结束后若无错误即可确保AST上的每个标识符引用都有定义每个函数调用可解析到目标函数每条语句在语义上是合理的。这为后续代码生成提供了可靠基础。语义阶段还可以在AST节点上附加类型注解便于代码生成选择合适的指令。目前Context的parseType会将类型名字符串转换为Type对象如BuiltinType.INT等供符号记录但AST节点未存储类型属性后端主要依赖变量的Type信息选择指令。整个SemanticAnalyzer设计采用了注册表 + 分派模式通过AnalyzerRegistrar将具体AST节点类型与相应SemanticAnalyzer关联FunctionChecker运行时按节点类型获取分析器。这种设计方便扩展新的语义检查规则只需增加对应节点的Analyzer并注册即可。

健壮性: 语义分析目前存在一些可改进点:二是三是四是

上面是Semantic相关介绍

需要你完成的

  1. 解压然后读取项目里面全部相关代码,然后解决下面的问题
  2. 错误收集不完善,目前遇到语义错误就记录并在最后可能退出,但对于不同模块或函数的多个错误能否一次收集输出没有详述。改进为非终止检查,在尽可能继续分析后面的代码前提下收集所有错误。
  3. 模块导入解析还不完整Import只验证模块是否存在于当前编译单元内的ModuleRegistry需要支持跨文件模块需要扩展编译器能够根据import加载其它源文件或已编译中间文件
  4. 作用域管理仅支持函数级缺少块级作用域支持如在if/loop内部声明的变量不单独成域需要更细粒度的SymbolTable嵌套和Analyzer处理需实现。
  5. 类型系统如上所述比较原始没有检查表达式的类型正确性例如算术运算混用int和string未来可引入类型推断或强制类型检查使语言更加健壮。
  6. 帮我解决以上问题,确保修改后和后面模块兼容,给我修改后的代码和新增的代码

中间代码(IR)生成

在通过语法和语义检查后编译器将AST转换为中间表示IRIntermediate Representation。IR是一种适合进一步翻译优化的抽象指令序列通常比AST更贴近目标机但又独立于具体机器。本项目IR设计为基于虚拟寄存器的三地址码形式每条IR指令类似于dest = op(arg1, arg2)的结构。主要实现位于org.jcnc.snow.compiler.ir包下包括IR指令类、值类和IR构建器。

IRProgramBuilder负责遍历AST构建完整的IR程序。其buildProgram(List<Node> roots)方法对传入的AST根节点列表依次处理若节点是ModuleNode则对该模块下每个FunctionNode构建IRFunction加入IRProgram若节点本身是FunctionNode顶层函数直接构建加入若是StatementNode顶层语句则会被包裹成一个伪函数“_start”再构建。这样支持了脚本模式下没有显式函数的语句执行。IRProgram相当于整个程序的IR表示内部维护一个IRFunction列表及全局常量池等当前实现主要管理函数列表

IRFunction对应源代码中的一个函数包含该函数的IR指令序列、虚拟寄存器集合等信息。构建IRFunction由FunctionBuilder完成它以AST的FunctionNode为输入创建一个空的IRFunction然后

  • 参数处理为FunctionNode的每个参数创建一个新的虚拟寄存器(IRVirtualRegister)。IRFunction维护一个按序参数寄存器列表参数寄存器也算作函数体可用的虚拟寄存器。IRContext会将参数名绑定到对应寄存器上以便函数体后续IR生成时能找到参数。如所示对每个参数调用irFunction.newRegister()生成新虚拟寄存器,然后通过irContext.getScope().declare(参数名, 寄存器)建立名字到寄存器的映射并记录到IRFunction参数列表中。

  • 函数体处理FunctionBuilder创建StatementBuilder用于生成函数体内部各语句的IR。随后循环遍历AST函数体的每个StatementNode由StatementBuilder逐个处理。StatementBuilder根据语句类型构建不同的IR序列具体见下文。它持有同一个IRContext保证多个语句间能共享和更新作用域信息。所有IR指令通过irContext.addInstruction()添加到IRFunction的指令列表中。处理完所有语句后一个IRFunction就构造完成并返回给IRProgramBuilder后者加入IRProgram。中各分支确保所有顶层代码最终都转换为IRFunction若有顶层Statement会包装到_start函数,从而统一处理方式)。

IR指令由抽象类IRInstruction及其子类表示每种中间操作对应一个类。例如IRAddInstruction表示整数加法有属性dest结果寄存器、lhs和rhs操作数可为寄存器或常量IRReturnInstruction表示函数返回IRJumpInstruction表示无条件跳转CallInstruction表示函数调用等。每个IRInstruction实现方法dest()返回目标寄存器(若有),operands()返回操作数列表,以及op()返回操作码枚举IROpCode。IROpCode是内部定义的枚举列举了IR支持的操作码类型如ADD_I32、SUB_I32、CALL等。当前IR实现针对32位整数运算进行了主要支持例如IRAddInstruction的op()固定返回ADD_I32。虽然设计上考虑了不同数据类型IROpCode中可能有ADD_F32等并在ExpressionBuilder中有解析数值字面量后缀以决定操作数类型的逻辑如识别100L为long型但实际IR指令类并未针对其他类型定制不同类型常量目前均以NumberLiteralNode和IRConstant统一处理算术IR指令也都以I32后缀为主。这意味着当前IR阶段未充分区分多种数据类型浮点和长整型等运算可能被错误地按I32处理或不支持。这属于功能上的不完整未来可扩充更多IR指令种类或参数化IR指令以支持不同类型运算。

StatementBuilder实现将AST语句节点转换为一系列IR指令。它内部通过exprBuilder = new ExpressionBuilder(irContext)来处理表达式部分,从而实现递归构建。例如列出了不同语句的处理要点:

  • ExpressionStatementNode表达式语句直接调用ExpressionBuilder构建表达式将结果存入一个寄存器但由于表达式语句的结果不需要保存生成指令后忽略结果寄存器。比如函数调用语句会生成CallInstruction得到返回值寄存器但不进一步使用。
  • AssignmentNode(赋值语句):先构建右侧表达式获取结果寄存器,然后检查赋值的变量名在当前作用域是否已声明。如果未声明,说明是第一次赋值,则通过irContext.getScope().declare(名称, 寄存器)将变量绑定到该寄存器(相当于变量声明);如果已存在,则用scope.put()更新变量名绑定的寄存器为新值寄存器。这种做法意味着每次赋值都会产生新的虚拟寄存器旧值的寄存器不再作为该变量的当前值形成一种Static Single Assignment风格。这样在IR层实现了变量重新赋值而无需单独的Store指令。
  • DeclarationNode声明语句如果带初始化器则构建初始化表达式的IR将结果寄存器声明绑定到变量名如果没有初始化则仅调用scope.declare(name)分配一个新寄存器给该变量但不赋值。声明语句本身不产生独立的IR指令除非有初始化表达式需要先计算出结果
  • ReturnNode返回语句若有返回值表达式则先构建表达式IR得到值寄存器再调用InstructionFactory.ret生成ReturnInstruction将该寄存器标记为返回值如果无返回值则生成RetVoid指令表示返回。Return指令在IR级别会作为函数结尾指令用于后端生成适当的RET/HALT等。
  • 控制流语句If/Loop等目前StatementBuilder中没有针对IfNode或LoopNode的处理分支。实际上项目实现了IfNode、LoopNode的AST和相应SemanticAnalyzer但IR生成尚未支持它们。这意味着遇到条件或循环语句StatementBuilder会落入默认分支抛出异常“Unsupported statement”。因此当前版本编译器无法直接编译含有if/loop的函数除非采取特殊措施绕过IR阶段。这是实现上的一大不足后续需补充相应IR构造逻辑。例如可为IfNode生成条件判断和跳转IR类似“三地址码”中的条件跳转为LoopNode生成循环初始化、条件检查、跳转等IR指令如IRJumpInstruction。目前项目中虽有IRJumpInstruction类等但未被使用。这属于功能不完整的部分,下文将建议改进。

IR生成完毕后IRProgram中汇集了所有函数的IR表示。调用IRProgram.toString()可以打印出IR的简要文本表示例如变量寄存器通常显示为%0, %1等格式,指令形如%2 = %0 + %1。在SnowCompiler主程序中会打印IR用于调试。IR作为中间产物一方面可以进行优化当前未实现显式优化流程仅留下IROptimizer接口作为扩展点【17†output】),另一方面便于进行下一阶段目标码生成。

目标代码生成与虚拟机指令集

代码生成(Code Generation) 负责将IR翻译为虚拟机能够执行的指令序列。SCompiler的目标指令集是定制的Snow虚拟机字节码(文本形式表示)设计思想接近JVM采用操作数栈+局部变量槽模型,并区分不同数据类型的操作。代码生成主要分两步:寄存器分配指令序列生成

  • 寄存器分配(Register Allocation)IR中的虚拟寄存器需要映射到虚拟机的局部变量槽(slot)。RegisterAllocator.allocate(IRFunction fn)完成这一任务。策略是简单的线性分配首先按函数参数列表顺序为每个参数分配连续槽位0,1,...然后扫描函数体每条IR指令若指令有dest结果寄存器且尚未分配槽则分配新槽再检查指令的每个源操作数凡是IRVirtualRegister且未分配槽的也立即分配。这种方式确保一个函数内每个出现过的虚拟寄存器都唯一定义一个槽号寄存器生命周期不重叠也不会复用槽未做活跃分析优化。RegisterAllocator返回一个不可变的映射表Map<IRVirtualRegister, Integer>供后续使用。例如若IRFunction有参数%0、%1则它们映射slot0和1函数体第一次出现的新寄存器%2映射slot2以此类推。这样一来变量和中间计算在虚拟机中都有了各自的“存储地址”。

  • 虚拟机指令生成VMCodeGenerator负责将单个IRFunction翻译为具体的指令文本。构造VMCodeGenerator时传入上一步计算的slotMap和一个VMProgramBuilder输出器。生成过程通过遍历IRFunction的指令列表实现对不同IR指令类型调用不同的生成函数

    • 常量加载IR的LoadConstInstruction表示将立即数载入目标寄存器。生成两条虚拟机指令首先将常量值压入操作数栈例如使用I_PUSH <value>将整数常量压栈),然后从栈弹出该值存入目标槽位(如I_STORE slot将值存到本地变量表指定槽)。引用示例:IRConstant 5 -> I_PUSH 5; I_STORE 0将5存入slot0
    • 算术/二元运算IR的BinaryOperationInstruction涵盖加减乘除比较等操作。生成流程是将两个操作数从本地槽加载到栈I_LOAD slotA; I_LOAD slotB执行对应运算指令然后将结果存回目标槽。例如IRAdd(dest=%3, lhs=%1, rhs=%2)假设%1映射slot1、%2映射slot2则输出指令I_LOAD 1; I_LOAD 2; I_ADD; I_STORE 3。这里I_ADD是Snow VM定义的“32位整数加法”操作码。实际上VMCodeGenerator通过IROpCodeMapper.toVMOp()将IR指令的IROpCode转换为相应的VM操作码助记符。目前由于IR基本都为I32类型操作码映射多是直观对应的如ADD_I32 -> I_ADD。若后续扩展不同类型运算IROpCodeMapper会相应扩充映射比如ADD_F32映射F_ADD等。
    • 一元运算如IR的UnaryOperationInstruction目前可能用于数值取负等生成类似过程加载操作数执行一元运算指令将结果存回。
    • 函数调用IR的CallInstruction包含目标函数名和实参列表IRValue列表以及返回值寄存器。VMCodeGenerator对每个实参依次生成I_LOAD argSlot指令,将参数值压入栈。然后调用VMProgramBuilder.emitCall(函数名, 参数个数)生成一条调用指令CALL <addr> <nArgs>。这里是被调函数入口地址如果该函数已处理过已在labelAddr映射中则直接填入其起始地址如果尚未出现则暂时填入占位符-1并记录到unresolved列表待后面真正定义该函数时回补地址。这种单遍代码生成+回填机制使函数可以在定义前被调用。最后,如果调用有返回值,则紧接生成一条I_STORE destSlot将栈顶返回值存入调用方的目标槽。例如调用CommonTasks.test(a,b)返回值赋给%5slot5且test函数在最终代码中位于地址10且有2参数则调用处指令可能是I_LOAD <slot_a>; I_LOAD <slot_b>; CALL 10 2; I_STORE 5
    • 返回IRReturnInstruction翻译为虚拟机的返回或终止指令。若有返回值寄存器则先加载该值到栈I_LOAD slotX然后需要根据返回的是主程序入口函数还是一般函数决定操作码Snow VM约定主函数返回时应终止整个程序而普通函数返回时跳回调用点继续执行。因此VMCodeGenerator做了判断如果当前函数名是“main”则输出HALT指令终止VM否则输出一般的RET指令表示函数返回。在Snow虚拟机中HALT会使主循环退出而RET则弹出当前栈帧并跳转到先前保存的返回地址继续执行。这种特殊处理方式意味着**入口函数必须命名为"main"**才能在结束时正确停止虚拟机。如果用户主程序模块函数不用main命名当前实现会错误地用RET返回导致VM尝试跳转到调用者但main没有调用者)而可能引发异常或无效果。这是不够健壮之处,后文将讨论改进。

代码生成完成后,所有函数的指令已添加到VMProgramBuilder中。VMProgramBuilder的职责类似于汇编器它维护一个code列表按顺序存放最终指令字符串,并处理函数标签和调用修补。当beginFunction(name)被调用时会记录当前指令地址pc作为该函数入口地址并回填之前所有针对该函数的未解析调用。SnowCompiler主程序对每个IRFunction顺序调用VMCodeGenerator.generate后最后调用builder.build()得到完整指令列表。在未出现未解析调用目标的前提下否则build()会抛异常)即可进入执行阶段。

Snow虚拟机指令集支持整数、长整数、短整数、浮点、双浮点、字节等多种类型操作对应不同前缀的操作码I_, L_, S_, F_, D_, B_。指令可分为栈操作PUSH, POP, DUP, SWAP等、算术运算ADD, SUB, MUL, DIV, MOD及一元NEG按类型区分如I_ADD, D_ADD等、位操作AND, OR, XOR等、类型转换例如I2F, F2D将栈顶值类型转换、内存读写LOAD/STORE将局部槽与栈之间传值如I_LOAD n, I_STORE n、函数调用与返回CALL addr nArgs, RET、条件跳转比较指令如ICG=“if greater”等通常会消耗栈顶两个操作数做比较并跳转以及虚拟机控制HALT停机。在本项目生成代码中主要用到了I_LOAD/STORE, I_PUSH, I_ADD等I_*系列指令以及CALL/RET/HALT等指令针对其他类型的指令类虽然实现了许多如BAddCommand, DAddCommand等但由于编译器尚未生成这些指令因此在执行中未实际用到。

虚拟机执行逻辑

编译生成的指令序列交由Snow虚拟机执行。虚拟机由org.jcnc.snow.vm包的一系列类实现,包括引擎、指令定义、运行时栈和辅助工具。其设计采用栈式虚拟机模型:每条指令从操作数栈弹出操作数,计算后再将结果压回栈,或在需要时与方法的本地变量区交互。主要组件如下:

  • VirtualMachineEngine:虚拟机核心引擎,提供execute(List<String> program)方法执行指令列表。Engine内部维护操作数栈(OperandStack)、全局本地变量存储(LocalVariableStore)、调用栈(CallStack)、程序计数器(programCounter)等状态。执行前,会调用ensureRootFrame()创建一个“根调用帧”(对应主模块运行环境)并压入调用栈。根帧的返回地址通常无效,用于占位。然后进入主循环,每次根据programCounter取出对应指令字符串解析其操作码和操作数。Engine通过parseOpCode()将助记符转为内部整数opcode通常来自VMOpCode枚举。随后调用commandExecutionHandler.handle(opCode, parts, currentPC)执行该指令。handler返回下一个指令的地址nextPCEngine更新programCounter进入下一循环。循环持续直到遇到终止条件如果nextPC返回特殊值PROGRAM_END-1例如HALT指令或运行中出错则跳出循环。执行完成后引擎提供方法打印最终操作数栈和局部变量表供调试。整个执行过程即典型的取指-译码-执行循环

  • 命令执行器(CommandExecutionHandler)封装具体指令执行逻辑的调度。Engine在每次循环取到opcode后将实际执行委托给CommandExecutionHandler。Handler持有对OperandStack、LocalVariableStore、CallStack的引用。其handle(int opCode, String[] parts, int currentPC)首先通过CommandFactory.getInstruction(opCode)获取对应的Command对象实例如果找不到则抛异常。然后调用该Command的execute(String[] parts, int pc, OperandStack, LocalVariableStore, CallStack)方法,执行指令所需操作,并返回下一条指令地址。所有具体指令的实现都遵循Command接口在其execute方法中编写针对OperandStack等的数据操作和状态变更。例如IAddCommand的execute会从OperandStack弹出两个int值相加将结果压回栈并返回pc+1指向下一指令CALL指令的execute会读取操作数函数地址、参数个数创建新的StackFrame并调整调用栈设置programCounter跳转到目标地址开始执行被调函数RET指令的execute会弹出当前栈帧并设置返回地址为nextPCHALT指令则返回-1以通知引擎停止执行。

  • 运行时栈与帧:调用栈(CallStack)管理着一系列活动的栈帧(StackFrame)。每个StackFrame封装一次函数调用的执行环境包括返回地址(returnAddress)、局部变量存储引用和在其中的起始索引、以及一个方法上下文(MethodContext保存函数名等调试信息)。LocalVariableStore在Snow VM中被设计为所有帧共享的内存类似JVM将所有栈帧的本地变量保存在同一个连续内存由每个帧记录基址和大小。例如根帧初始化时base=0长度=…调用新的函数时可能在LocalVariableStore中分配新段。LocalVariableStore提供根据帧base和索引存取局部变量的方法并支持可视化LocalVariableStoreSwing GUI类用于调试查看局部变量。OperandStack用于算术运算和临时值传递是所有指令操作的主要对象。指令集通过严格的入栈/出栈顺序实现运算次序,比如前述I_LOAD n指令就是将本地槽n的值复制压入栈顶I_ADD则弹出两个栈顶数相加,再将结果压栈。

  • 指令类org.jcnc.snow.vm.commands包按功能分类定义了大量Command实现类每个类对应一种字节码指令。例如IAddCommand实现将OperandStack弹出两个int相加IStoreCommand实现从栈弹出一个值存入指定局部槽CallCommand实现创建新帧并跳转RetCommand实现返回上一帧。这些类大多遵循命名约定例如IAddCommand处理int加法LAddCommand处理long加法等。指令执行中的类型转换、算术溢出等未做特别检查直接利用Java对应运算因此可能存在如整数溢出不检测的问题属一般情况。CommandFactory内部预建了一个opcode到Command对象的映射表或工厂方法用于快速获取指令实例具体实现可能通过静态初始化将各Command注册到一个数组。通过这种工厂方式每次执行无需反射创建新对象而是重用已有Command实例减少开销。优化考虑当前Engine每步都字符串拆分指令然后查表获取Command理论上可以在加载指令时就将文本解析为Opcode和参数并存储这样执行时直接使用预解析结果可提升速度。但由于实现简洁优先目前在每次循环中进行解析对一般规模程序影响不大。

综上Snow VM以相对经典的栈机方式运行字节码指令支持函数调用栈和局部变量区实现了基本的流程控制和运算功能。配合前端编译器输出的正确指令序列即可完成源程序的实际运行。例如对于前述示例函数CommonTasks.test(将两个整数相加),编译器可能输出指令:I_LOAD 0; I_LOAD 1; I_ADD; I_STORE 2; I_LOAD 2; RET假设参数num1/num2槽为0/1结果槽2。虚拟机执行这些指令最终将正确的计算结果留在操作数栈或局部变量从而实现预期的功能。

健壮性: 目前虚拟机实现基本完备但仍有改进空间。例如主函数返回采用HALT特殊判断不够优雅如用户误用导致未HALT可能悬挂进程。更好的方式是引入程序入口概念由VM配置哪个函数作为入口并在其返回时自动HALT。又如错误处理方面CommandExecutionHandler捕获了执行过程中的异常并终止程序。对于常见运行时错误如除零、空栈等可考虑提前检查并提供明确错误信息。目前指令集的冗余也值得注意——大量类似的指令类如加法指令就有6种类型导致代码重复率高可以通过泛型或模板减少重复。性能方面如果追求更高执行效率可考虑直接将字节码编译为主机机器码JIT或采用更紧凑的二进制格式并优化取指循环。但这些在当前1.0版本中都不是首要目标。

源码结构与关键模块概览

SCompiler项目源码按功能划分为编译器虚拟机两大部分,各自下设子模块。下面以表格形式列出主要源码文件/类及其功能:

模块/文件 功能简介
org.jcnc.snow.compiler.lexer 词法分析模块将源码文本读入并切分为Token序列。
LexerEngine 核心词法分析器初始化各种Token扫描器并驱动扫描过程。调用getAllTokens()可获取完整Token列表。
LexerContext 提供字符流读取、回退、位置跟踪等功能。
TokenScanner接口 定义扫描器规范每种类型Token对应一个实现类如WhitespaceTokenScanner、NumberTokenScanner等由LexerEngine按顺序调用其canHandle/handle进行识别。
Token/TokenType 封装词法单元信息类型枚举包括IDENTIFIER, KEYWORD, NUMBER等。Token记录词法类型、原始文本及行列位置等。
• 其他:TokenFactory根据扫描到的字面串判断关键字或类型;TokenPrinter用于调试打印Token序列。
org.jcnc.snow.compiler.parser 语法分析模块将Token列表解析为AST。
ParserEngine 语法分析主控循环读取TokenStream根据下一个Token选择顶层解析器处理。跳过空行并确保遇到未知结构抛错。
TokenStream 封装Token列表并维护当前位置提供peek, next, expect等方法辅助解析。注意: isAtEnd()逻辑有误,返回条件与命名相反,需更正。
顶层解析器ModuleParser解析模块定义(匹配module 名称:开头,内部包括导入和函数,结束于end moduleImportParser解析导入列表;FunctionParser解析函数定义(支持parameter/return_type/body区块利用FlexibleSectionParser实现。顶层解析器统一实现接口TopLevelParser,工厂TopLevelParserFactory按关键字注册目前仅“module”注册。
语句解析StatementParserFactory根据语句起始关键字分派相应解析器。已实现解析器Declaration、Assignment、If、Loop、Return以及默认Expression解析器等。每个解析器将构造对应的AST节点如IfParser构造IfNode含条件ExpressionNode和then/else子节点列表
表达式解析采用Pratt算法PrattExpressionParser协调多个Parselet完成。包内定义了各种PrefixParselet如解析字面量、标识符和InfixParselet解析运算符优先级子类。ExpressionNode派生类如BinaryExpressionNode、CallExpressionNode等用以表示表达式AST。
AST节点类parser.ast包定义了语法树的结构。Node为基类,StatementNodeExpressionNode为语句/表达式抽象基类。重要子类包括ModuleNode、FunctionNode、ImportNode、ParameterNode等用于表示程序结构及各种具体Statement/Expression节点IfNode, LoopNode, ReturnNode, IdentifierNode, LiteralNode等。节点类主要存储语法属性如FunctionNode含函数名、参数列表等语义属性在分析阶段可附加。多数节点还提供toString()accept(visitor)方法用于调试或遍历。
其它ParserContext封装当前解析所需的TokenStream和符号表等上下文符号表在语义阶段使用ASTPrinter/ASTJsonSerializer用于打印或序列化AST树结构便于调试。
org.jcnc.snow.compiler.semantic 语义分析模块检查AST的正确性并准备后端信息。
SemanticAnalyzerRunner 语义分析入口提取所有ModuleNode并调用SemanticAnalyzer执行完整检查流程。
SemanticAnalyzer 核心分析器,依次执行模块注册、函数签名登记、函数体检查步骤。
符号表与上下文Context类维护全局信息如ModuleRegistry已加载模块信息, BuiltinTypeRegistry内置类型, 以及当前累积的SemanticError列表。还包含AnalyzerRegistrar用于获取对应节点的分析器。SymbolTable以链表形式记录符号(变量/函数)及其类型、种类(SymbolKind)等,用于作用域解析。
分析器:采用Visitor模式对不同AST节点实现不同检查。AnalyzerRegistry集中管理映射内部按AST节点类型保存对应的StatementAnalyzer或ExpressionAnalyzer实例。主要分析器类BinaryExpressionAnalyzer/CallExpressionAnalyzer等检查表达式节点DeclarationAnalyzer/AssignmentAnalyzer/ReturnAnalyzer/IfAnalyzer/LoopAnalyzer等检查语句节点。例如AssignmentAnalyzer会确保左侧变量已声明且未标记常量IfAnalyzer检查条件类型是否布尔等。当前实现简化了类型检查,更多是验证符号是否存在、控制流位置是否合法等。
函数签名登记SignatureRegistrar遍历模块的Import和Function将导入模块加入全局模块表函数名称和其FunctionType参数类型列表+返回类型注册到所属ModuleInfo。这样CallExpressionAnalyzer能通过ModuleRegistry找到被调用函数签名匹配。
函数体检查FunctionChecker对每个FunctionNode创建局部SymbolTable将参数列表加入符号表。然后对函数体的每条StatementNode调用相应StatementAnalyzer检查。在此过程中如果发现语义错误就通过Context记录SemanticError包含节点位置和错误信息。如无分析器匹配某节点类型则报“不支持的语法”错误。检查结束后SymbolTable随函数退出作用域销毁。
类型系统BuiltinType枚举了基本类型(int, float, string等)Type类是类型抽象。目前变量和函数定义可指定类型名但表达式类型推断不完善多数Analyzer未严格验证类型一致性这部分留待后续增强。
错误报告SemanticAnalysisReporter收集Context中的SemanticError列表输出错误详情并在有致命错误时终止编译。当前错误恢复策略有限一旦遇错通常中止后续阶段。
org.jcnc.snow.compiler.ir 中间表示(IR)模块将AST翻译为IR代码并准备后端生成。
IRProgram 表示整个程序的IR容器包含多个IRFunction。提供functions()遍历函数等方法支持打印IR内容。
IRFunction 表示单个函数的IR包含函数名、指令列表(List)、参数寄存器列表、虚拟寄存器计数器等。通过newRegister()生成新IRVirtualRegister其编号自动递增addInstruction()添加IR指令到函数尾部。
IRInstruction 抽象类 所有IR指令的基类定义了dest()目标、operands()操作数列表、op()操作码等抽象方法,以及toString()用于调试打印。具体IR指令子类BinaryOperationInstruction/UnaryOperationInstruction抽象类扩展IRInstruction以表示有两个/一个操作数的算术指令IRAddInstruction、IRSubInstruction等二元运算实现CallInstruction(函数调用,保存函数名及参数列表)、LoadConstInstruction(将常量载入新寄存器)、IRJumpInstruction/IRReturnInstruction(控制流指令)等。IRValue接口统一了IR操作数可以是IRVirtualRegister虚拟寄存器或IRConstant常量值或IRLabel跳转目标标签
IRVirtualRegister 表示一个虚拟寄存器内部仅有一个int编号标识。例如%0,%1这样的表示方法。
IR构建IRProgramBuilder遍历AST构建IRProgram。遇到ModuleNode则处理其内部函数FunctionNode直接处理顶层Statement则包装入临时FunctionNode再处理。实际构建由FunctionBuilder.build(FunctionNode)完成它创建IRFunction并用IRContext管理当前作用域。然后对函数参数依次生成虚拟寄存器并declare绑定。接着用StatementBuilder构建函数体。
StatementBuilder 核心IR生成器根据不同AST语句节点构建IR。列举了几种处理表达式语句 -> 直接build表达式丢弃结果赋值语句 -> build右侧表达式得到新寄存器然后变量若首次出现则declare相当于变相声明否则更新绑定声明语句 -> 若有初始化则build并declare变量否则仅declare空变量Return语句 -> build返回值表达式若有然后生成Ret或RetVoid指令If/Loop等控制语句 -> 目前未实现,将导致不支持异常。
ExpressionBuilder 递归构建表达式IR。对常量、变量引用返回IRConstant或已有寄存器对算术或比较表达式会build子表达式得到IRVirtualRegister再生成对应IR指令如IRAddInstruction对函数调用表达式先构建每个实参获得寄存器列表再生成CallInstruction目标函数名字符串根据是否有模块前缀拼接。所有ExpressionBuilder.build返回一个IRVirtualRegister供上层指令使用。
InstructionFactory IR指令工厂提供静态方法创建特定IR指令并自动添加到IRContext。例如InstructionFactory.ret(irContext, vr)会创建IRReturnInstruction并加入当前函数指令列表降低直接操作IRContext的重复代码。
IR优化当前IR阶段未包含优化。代码中提及IROptimizer【17†output】等计划但未实现实际优化步骤。IR的作用主要是简化后端代码生成。
org.jcnc.snow.compiler.backend 后端代码生成模块负责将IR转换为Snow VM指令序列。
RegisterAllocator 寄存器分配器将IR虚拟寄存器映射到局部变量槽索引。先分配参数槽位再扫描指令分配剩余寄存器槽。算法简单未考虑生存期重用未来可优化。
VMProgramBuilder 虚拟机指令构建器。维护内部List存放指令文本以及函数名->地址映射表labelAddr和调用待回填列表。提供beginFunction(name)标记新函数入口、emit(String line)添加一条指令并递增PC、emitCall(targetFn, nArgs)生成CALL指令并处理未解析目标、build()返回最终代码列表。通过这些接口,后端可以顺序生成指令且无需关心函数地址回填细节。
VMCodeGenerator 核心代码生成器将单个IRFunction翻译为VM指令。初始化时保存slotMap和VMProgramBuilder。generate(fn)按顺序遍历IR指令针对不同类型调用genLoadConstgenBinOpgenUnarygenCallgenRet等方法生成指令。每个方法利用VMProgramBuilder.emit封装具体输出例如genBinOp会输出形如I_LOAD a; I_LOAD b; <OP>; I_STORE c指令序列。这里<OP>通过IROpCodeMapper.toVMOp()将IR操作码映射到VM操作码助记符如ADD_I32->I_ADD。项目实现了常见指令的映射表IROpCodeMapper。slotMap用于将IR寄存器转为具体槽号slot(vr)函数得到某IRVirtualRegister对应整数索引用于LOAD/STORE指令参数。
• 其他:IROpCodeMapper定义静态映射从IR操作码枚举常量到VMOpCode枚举常量或助记符字符串。例如将IROpCode.ADD_I32映射为"ADD"或对应VMOpCode对象供VMCodeGenerator使用。VMMode定义虚拟机运行模式RUN/STEP等当前基本用RUN
(注backend模块的命名有些混乱理论上VMCodeGenerator和VMProgramBuilder关系更近似于“汇编器”而非生成可独立的目标代码格式。但这里将Snow VM字节码视作目标机器码进行生成。)
org.jcnc.snow.vm 虚拟机模块:解释执行编译后的指令序列,实现程序运行。
VirtualMachineEngine 虚拟机核心类,包含主循环执行逻辑。初始化时根据模式构造OperandStackLocalVariableStoreCallStack并创建CommandExecutionHandler。execute()方法按PC循环取指、解析、执行直到遇到HALT等终止。提供printStack/printLocalVariables用于调试输出当前栈内容。
OperandStack 操作数栈类内部用列表或数组模拟栈操作支持push/pop/peek等。
LocalVariableStore 局部变量存储模拟内存来存放各帧的局部变量值。实现上可用一个Object数组+每帧基址管理也可按帧链表各自维护。提供按索引存取的方法。LocalVariableStoreSwing是其GUI表示用于在窗口中监视各槽位值变化。
CallStack 调用栈内部维护一个栈结构的StackFrame列表。提供pushFrame(StackFrame)和popFrame()实现函数调用和返回。还有printCallStack用于调试输出调用链。
StackFrame 栈帧类保存单次函数调用的状态包含returnAddress调用者下一执行地址、对LocalVariableStore的引用及当前帧局部变量起始偏移、方法上下文(MethodContext)等。MethodContext包括函数名、所属模块等元信息辅助日志和调试。
命令集合vm.commands子包按照指令类型分目录包含数十个Command类。如commands.arithmetic下有IAddCommand/ISubCommand等实现整数算术LAddCommand等实现长整运算control子包下ICGCommand等实现条件跳转Compare Greater、JumpCommand无条件跳转stack子包有Push/Pop/Dup/Swap等栈操作memory子包有各类型Load/Store命令function包有CallCommand和RetCommandvm包有HaltCommand终止执行。每个Command类实现接口Command的execute方法利用OperandStack/LocalVariableStore完成操作并返回下一个PC。示例IAddCommand.execute会pop出两个int加法运算后push结果并返回currentPC+1。CallCommand.execute则会根据参数创建新StackFrame将当前PC+1作为returnAddress压栈并返回被调函数入口地址实现跳转调用。RetCommand.execute弹出当前帧取得先前保存的returnAddress返回。展示了CommandFactory获取并执行Command的过程。
CommandFactory 命令工厂,提供静态方法getInstruction(int opcode)返回Optional。中通过opcode查找对应Command实例。如果opcode无效则抛IllegalArgumentException。CommandFactory很可能在静态初始化时就创建好所有Command实例映射以避免执行时频繁创建对象。
VMOpCode 虚拟机操作码枚举每个枚举值代表一种指令并携带一个整数编码和助记符字符串等属性。提供从字符串助记符解析opcode的方法valueOf()Engine使用它在parseOpCode时将文本opcode转为内部code。具体实现可能直接用Enum.name()匹配或维护Map映射
LoggingUtils/VMStateLogger 日志工具类用于根据需要打印调试信息比如每执行一步的栈和变量状态等。可配置虚拟机在不同VMMode下输出不同粒度log。当前RUN模式下日志默认关闭。

存在的问题与可改进之处

综合分析当前实现SCompiler项目在功能和设计上还有一些不足与可以改进的地方

  • 词法/语法错误处理不够健壮Lexer遇到非法字符仅生成UNKNOWN Token继续使后续解析难以定位问题。不如在Lexer阶段就抛出带位置的LexicalException或收集错误。Parser对错误恢复也未实现一旦遇错整个编译停止这在编译大型源码时不友好。改进建议实现Lexer错误异常并中止解析或者Lexer将错误存入列表供最终输出Parser则可在某些错误后尝试跳到下一语句继续解析以发现更多错误最后集中报告。

  • 顶层结构限制:目前必须有module包装源码缺少直接编译脚本或REPL段代码的能力。如果需要支持可在ParserEngine中增加对无模块情况的处理例如隐式创建一个默认ModuleNode包含所有顶层函数/语句。事实上IR生成器已有对应思路识别顶层Statement包装_start函数但Parser未跟进。通过在TopLevelParserFactory注册例如“function”关键字解析器来允许顶层函数或对无module时由编译器注入一个默认模块都可以增强灵活性。

  • 状态标志命名逻辑错误Parser部分的TokenStream.isAtEnd()实现与常规含义相反,导致代码中使用它的逻辑也颠倒。当前isAtEnd()返回true表示还没有到流末尾peek() != EOF这既不直观也易埋下bug。事实上已经导致ParserEnginewhile (ts.isAtEnd())循环解析表面看逻辑正确但实则依赖了错误实现应为while not at end。建议修正isAtEnd()定义为return peek().getType() == EOF并相应修改调用处逻辑为while(!isAtEnd())等。保持命名和语义一致可减少维护困扰。

  • 缩进和块结构处理设计上DSL类似Python用缩进表示块但实现上引入了end关键字辅助结束。这混合风格虽然工作正常,但存在冗余。比如函数定义用了function name:end function包围同时还需正确缩进代码块。当前编译器实际上并未根据缩进深度产生INDENT/DEDENT Token也就是说缩进仅在视觉上要求在解析时可有任意空格而不影响结果Parser跳过多余空行和空格。这可能导致一些不一致如果用户缩进层级不对但写了正确的end,编译器仍接受;反之缩进对齐却少写end会报错。因此改进方向是统一块结构语法要么完全采用缩进严格决定层次像Python不需要end要么采用显式块标记如花括号或end而忽略缩进。选择其一并修改Lexer/Parser相应地大幅简化规则或提高代码健壮性。目前这种混用方式主要是为了易实现但从语言设计角度应在后续版本调整。

  • 控制流IR/代码生成未完成:目前编译器尚不能处理if/elseloop语句的代码生成。虽然AST和Semantic对它们已有表示和检查但IR阶段StatementBuilder直接将它们标记为不支持。这意味着源代码中如果出现条件或循环将在编译阶段报错终止。这显然是功能缺失的部分。应尽快完善IfNode和LoopNode的IR翻译例如为IfNode生成条件比较(IR可能需要一个IRCompare指令)及两条有条件跳转IR指令跳过then块或跳过else块还需要在IR中引入标签IRLabel表示跳转目的地等。LoopNode可转换为类似initializer; loopStartLabel: if (!condition) jump loopEnd; (loop body IR); jump loopStart; loopEndLabel:的IR序列。当前IRInstruction已有IRJump等类说明计划中考虑了这些操作只是未集成。完成这部分将极大拓展语言功能。相应地VMCodeGenerator也需增加对条件跳转和循环的指令生成例如实现比较后条件跳转Snow VM已有ICG/ICL等指令。总之补全控制流编译应是下一步重点。

  • 类型支持不完整SCompiler词法上支持如123L后缀表示long、3.14表示double等TokenFactory也能将int、string、bool、float、double识别为类型Token。但是编译流程并未真正区分这些类型的操作。IR层所有算术指令都假定32位整数ADD_I32等即使Literal有后缀dfExpressionBuilder的resolveSuffix尝试识别类型却未被用于选择不同IR操作码IRAddInstruction的op()始终返回ADD_I32。虚拟机虽然实现了不同类型指令I_ADD, L_ADD, F_ADD等但编译器从未生成过后者。结果就是float a = 1.2; float b = 3.4; float c = a + b;这样的代码编译器仍当作int相加生成I_ADD而不是F_ADD。显然这是不正确的。改进措施完善类型系统在AST节点附加类型信息在ExpressionAnalyzer中推导表达式类型并在IR生成时根据节点类型选择相应IROpCode如浮点加法应产生ADD_F32等。目前预留的resolveSuffix等逻辑可配合类型规则使用。一旦IR准确区分类型VMCodeGenerator的IROpCodeMapper需同步更新映射以输出正确的指令助记符如ADD_F32->F_ADD。这样才能真正支持多类型运算。另外诸如字符串拼接+、不同类型间运算的转换也需要在语义阶段明确和在代码生成阶段处理比如插入I2F等类型转换指令。总的来说当前类型检查与对应代码生成是不完整的需要加强以避免隐患和拓展语言能力。

  • 冗余的VM指令类与优化Snow VM为不同数据类型的每个操作都定义了独立的Command类如IAddCommand, LAddCommand逻辑几乎相同区别只是操作数类型不同。这样做直观但导致代码大量重复不利于维护和扩展。如果以后增加新类型比如字节、短整型已经增加了BAdd, SAdd等每种操作都要添加新类。可以考虑优化设计采用模板泛型或继承来减少重复实现一个参数化的AddCommand根据类型参数决定执行int还是long运算或者在Command.execute内部判断操作数类型并执行不同逻辑但这会牺牲执行效率。另一种方式是在指令集中引入统一操作码比如把IAdd/ LAdd/ FAdd等的公共部分抽象为一个指令加一个类型码参数。不过这改变较大。性能层面当前VM每步都解析字符串opcode并查找Command解析开销相对执行计算可忽略但仍有提升空间可以在加载字节码时预把指令字符串转换为更紧凑的中间形式如指令对象列表或(opcode, args)对这样execute循环无需split字符串每步直接取对象执行。Java实现里也可考虑将频繁的小方法如各Command.execute改为内联或减少虚函数调用但这由JIT自动优化即可不必手动干预。若追求极致可将VM用Java代码重写为一个大的switch执行类似解释器线性代码省去很多间接调用不过会失去面向对象的清晰结构。总之在功能完善后可视情况对VM执行部分进行优化。

  • 主函数返回和程序结束当前约定以函数名“main”识别主程序并在其返回时用HALT终止。这种做法隐含假设每个编译单元的主模块函数叫main否则可能出现“进程未HALT”的情况。如前所述若用户没有使用main而用其他名字当作入口函数程序运行完RET后不会退出主循环除非调用者frame为空时Engine检测到programCounter越界而退出但这依赖隐含行为。这需要改进。可以在编译阶段明确标记入口函数例如第一个Module的第一个Function默认为入口在代码生成时对该函数无论名称为何都使用HALT结尾。或者在Engine中当返回到根帧时自动HALT。目前StackFrame.root的returnAddress通常是0RetCommand弹出根帧后programCounter=0会导致主循环重新从0执行从而重复运行程序可能Engine的PROGRAM_END常量用于避免此情况但代码未展示完整逻辑。实际测试可能因为callStack为空跳出但不明显可靠。建议增加对入口点的配置或者发射特定指令标记程序结束。一种方案是在指令序列末尾无条件附加HALT确保结束。总之需要让编译器输出的代码在任何情况下都能正常结束而不是依赖名称约定。

  • 模块与导入的扩展目前Import只是检查被导入模块是否存在于同一编译单元。没有支持真正多文件编译例如import一个外部模块并自动加载编译。目前用户如果想引用另一模块的函数需将两个模块源文件一起提供给编译器或者手动把被导入模块代码也合并。理想改进是在编译时检测到Import未解析可以根据模块名去文件系统查找对应源码或已编译文件并编译/链接。这需要编译器具备分步编译和符号链接功能。目前pom.xml等未显示具体实现因此猜测暂未支持。未来可引入例如编译输出中间二进制格式类似JVM的class文件在导入时加载这些中间码从而实现真正的模块化编译。此外对于重复导入、循环导入等情况也需要完善检测避免无限递归加载等问题。

  • 调试和工具项目虽然有LoggingUtils、VMStateLogger等但目前命令行接口简单缺少调试器、单步执行等功能。可以考虑实现断点调试模式比如利用VMMode.STEP逐条执行指令并输出状态从而手动调试程序。也可以开发AST/IR可视化工具将编译过程的各阶段结果呈现给开发者有助于理解和优化。另一个改进方向是提供更加友好的错误信息目前错误通常只有简短描述和Token文本最好能包含行号列号Token已记录位置信息但SemanticError等输出未利用这些【17†output】。完善错误提示有助于使用者调试源代码。

  • 性能优化:对于复杂程序,编译性能和运行性能都可以优化。编译性能方面,可优化语法分析算法如引入预测分析减少回溯(当前输入规模小影响不大)。运行性能方面,可尝试JIT将热点字节码翻译为宿主机器指令执行或者直接目标生成原生代码而非解释这样运行速度会有数量级提升。不过这将显著增加实现复杂度不一定是项目目标。另外考虑利用Java本身的性能例如把Snow源码直接编译为Java字节码利用ASM等库动态生成类然后在JVM中加载执行可省去自己写的解释器开销。这相当于把Snow作为一个DSL直接构建到JVM上也是可探索的优化方向。

  • 代码结构与工程随着功能扩展目前代码组织上也可做一些重构提高可维护性。例如虚拟机指令类众多可以通过自动代码生成来避免手写重复代码比如用模板文件生成各类型算术指令类。又如为每个模块内子包都写了个doc/README.md记录设计很有益处但如果文档内容与代码实现不符要及时更新。目前readme.md提到的一些设计如未实现的IR优化阶段、基于缩进的语法等和最终代码存在出入需注意保持文档同步以免误导开发者。项目pom.xml简单但可以考虑拆分编译器和VM为独立模块便于单独测试和重用。最后更多的测试用例和示例也是必要的。当前仓库只附带了一两个示例模块(CommonTasks等),应增加单元测试,覆盖各语法和功能点,确保改进不引入回归。

综上SCompiler在实现了基本的编译运行流程的同时也暴露出一些典型问题。幸运的是其架构清晰模块分明大部分改进可以各自独立进行。例如可先完善控制流IR/指令这一大功能,再逐步强化类型系统,最后针对性能和架构优化。

后续优化方向

结合以上分析SCompiler项目后续可从以下几个方面着手优化和扩展

  • 丰富语言特性:实现条件分支和循环等控制结构的完整支持,增加如break/continue控制支持函数递归和更复杂的表达式组合。同时引入更多数据类型char、数组、结构体等以提高语言表达能力。可以考虑增加字符串拼接、逻辑运算符(&&,||)等语法糖以及简单的输入输出API提升Snow语言实用性。

  • 加强类型系统完善静态类型检查支持基本类型的自动转换规则例如int可用在float运算中通过插入I2F指令和类型兼容性检测。引入布尔类型并强制条件判断必须为布尔表达式而不是用数字非零即真。后续甚至可增加面向对象特性引入类和对象、方法调度等但这需要更大的架构调整。

  • 中间代码优化实现IR层的优化模块例如常量折叠编译期计算常量表达式、死代码消除移除永不执行的代码、公共子表达式消除等以减少生成指令数量。还可引入简单的寄存器分配优化目前RegisterAllocator映射后很多槽可能未被充分利用可通过分析寄存器活跃区间来复用槽位降低局部变量空间占用这对减少栈桢大小有益。此外可考虑在IR级别进行函数内联、循环展开等更高级优化,不过需权衡编译时间和收益。

  • 改进编译性能对于大型源码解析和语义分析阶段可以通过更好的算法提速。例如使用预生成的预测分析器如ANTLR之类的工具替代手写Parser可自动处理错误同步和大部分繁琐逻辑。不过手写解析器在这个项目规模下尚可接受。编译性能另一点在于IO和中间过程打印尽量减少不必要的调试输出当前默认打印IR和字节码可在非调试模式下关闭。如果需要批量编译多个文件可考虑实现增量编译或多线程并行编译不同模块。

  • 增强虚拟机性能如前所述引入字节码预解析或直接JIT。可以尝试实现一个字节码到Java字节码的转换器把Snow程序动态转为等价的Java类加载执行借助JVM优化获得性能提升。此外若保持解释执行可优化指令调度比如使用直跳表switch-case替代Command对象调用以降低每步的分派开销。对于热点函数可以探讨动态编译:在运行时收集函数执行次数,对高频函数生成优化机器码替换解释执行。

  • 工具链和调试支持:开发交互式REPL允许用户逐句输入Snow代码立即编译执行便于调试。增强错误信息包含源代码片段、高亮出错位置等。构建更完善的测试集涵盖各语法元素组合和边界情况以防止将来修改引入回归。可以编写性能基准测试,评估不同规模程序编译和执行耗时,为优化提供依据。考虑编写文档和教程,包括语言规范、使用指南等,降低他人上手门槛。

  • 模块化和生态:实现真正的多文件模块编译机制,支持将模块编译为中间文件并在别的程序中链接使用。可以设计自己的二进制格式(比如.snb文件,包含字节码和元数据),由虚拟机直接加载执行或静态链接多个模块。随着功能增强,可以编写一些标准库模块提供常用功能字符串处理、数学运算等以丰富Snow语言生态。

  • 代码质量和维护:在重构方面,可考虑精简重复代码,例如利用元编程自动生成各类型指令类的模板代码,或为相似逻辑的解析器/分析器编写共用辅助函数。引入静态代码检查工具和格式化工具保证编码规范统一。由于项目采用了Java 17的新特性如record,模式匹配等继续关注这些语言特性在本项目中的应用可以让代码更简洁。例如可以使用Java的sealed类来限制IRInstruction子类明确哪几种指令类型利于优化switch分支。