Compare commits

...

10 Commits

Author SHA1 Message Date
c27899f8ed 增加pdf文档 2025-05-21 19:11:57 +08:00
25ec8844dd 修改文档 2025-05-21 17:14:46 +08:00
6f2f2bb38e 增加文档 2025-05-20 17:57:31 +08:00
d7bfee2098 增加文档 2025-05-20 17:56:57 +08:00
b3515335c1 增加注释 2025-05-16 17:21:14 +08:00
dc20eb4bf2 增加注释 2025-05-16 16:59:03 +08:00
56301944fa 增加注释 2025-05-16 16:58:43 +08:00
4f2ee4a9f0 fix:改进语义分析 2025-05-16 16:50:34 +08:00
1c9cfc3b4c fix:改进语法分析器支持顶层语句 2025-05-16 16:31:41 +08:00
87284c50d4 fix:改进语法分析器支持顶层语句 2025-05-16 16:08:35 +08:00
8 changed files with 476 additions and 345 deletions

328
doc/SCompiler.md Normal file
View File

@ -0,0 +1,328 @@
# SCompiler
## 项目整体结构概览
SCompiler 项目包括**编译器**和**虚拟机**两大部分,源代码按功能划分在不同包中。编译器部分负责将源代码转换为中间表示和可执行的指令序列;虚拟机部分负责加载并解释执行这些指令。整体结构如下:
* **编译器部分org.jcnc.snow\.compiler**:包含词法分析(`lexer`子包)、语法分析(`parser`子包)、语义分析(`semantic`子包)、中间表示IR构建(`ir`子包)、后端代码生成(`backend`子包)以及命令行接口(`cli`子包)。主要流程由`SnowCompiler`类驱动,按顺序调用各阶段模块完成从源代码到字节码的转换。
* **虚拟机部分org.jcnc.snow\.vm**:实现自定义字节码的运行时支持。主要包括虚拟机引擎(`engine`包)、指令集定义(`commands`包)、执行模块(`execution`包)、运行时数据结构(`module`包,如操作数栈、调用栈、栈帧等)和辅助工具(`factories`用于指令工厂,`utils`提供日志等)。核心类`VirtualMachineEngine`负责载入由编译器生成的指令序列并逐条执行,借助`VMCommandExecutor`调度具体`Command`指令处理器完成运算。每种字节码指令在`commands`子包中实现为一个类,例如算术指令`IAddCommand`32位整数加法`LAddCommand`64位整数加法等。
### 编译器部分结构
编译器前端按经典编译流程组织:
* **词法分析器Lexer**:由`lexer.core.LexerEngine`实现负责读取源代码文本将其拆解为有序的记号Token序列。LexerEngine 在构造时初始化一组`TokenScanner`子扫描器每个扫描器识别一种类别的Token。例如
* `WhitespaceTokenScanner` 跳过空白字符(空格、制表符等)。
* `CommentTokenScanner` 跳过注释文本。
* `IdentifierTokenScanner` 识别标识符和关键字(例如关键字`module``function``if``return``declare`等)。
* `NumberTokenScanner` 识别数字字面量(整数、浮点数)。
* `StringTokenScanner` 识别字符串字面量。
* `OperatorTokenScanner` 识别运算符符号(如 `+`, `-`, `*`, `/`, `==`, `!=` 等)。
* `SymbolTokenScanner` 识别分隔符和符号(如逗号、括号、冒号、换行等)。
LexerEngine 按优先级运行上述扫描器对源代码逐字符扫描生成Token列表。输出的Token序列会附带类型和词素lexeme信息并在代码中保留行列位置以便错误报告。扫描完成后还会在末尾插入文件结束标记`EOF`
* **语法分析器Parser**:由`parser.core.ParserEngine`驱动依据语言文法将Token序列解析为抽象语法树AST。ParserEngine 负责识别顶层结构并调用相应解析器:
* **模块解析**`ModuleParser` 处理`module`模块块。语法类似`module 模块名:`开始,缩进块内包含多个函数定义,以缩进或显式`end module`标志结束模块。
* **导入解析**`ImportParser` 处理`import`声明,引入其它模块(目前仅语法支持,实际链接有限)。
* **函数解析**`FunctionParser` 处理函数定义。函数定义以关键字`function`起始,例如`function foo(x: Int, y: Int): Int`,后跟缩进的函数体,函数体由若干语句组成,函数可在缩进结束处结束(可能通过撤销缩进或`end function`标记)。函数头部包括函数名、参数列表(由`ParameterNode`表示每个形参及类型)、返回类型注释等。
* **语句解析**函数体内部Parser使用`StatementParserFactory`根据每条语句起始的关键字选择相应的语句解析器:
* `IfStatementParser` 解析条件语句(`if ... else ...`结构)。
* `LoopStatementParser` 解析循环语句(`loop ...`结构,类似`while`或通用循环块)。
* `ReturnStatementParser` 解析返回语句(`return`语句)。
* `DeclarationStatementParser` 解析变量声明语句(`declare`开头的声明)。
* 对于未匹配上述关键字的情况,默认使用`ExpressionStatementParser`,将整行解析为表达式语句。`ExpressionStatementParser`内部还处理赋值语句的特殊情况:如果行首是标识符且紧随`=`,则识别为赋值语句,将其解析为`AssignmentNode`(左侧标识符和右侧表达式)。
* **表达式解析**:对于语句中的表达式部分,编译器采用 Pratt 算法(运算符优先解析)。定义了一系列前缀和中缀解析子器(parselet):例如`NumberLiteralParselet`处理数字字面量,`StringLiteralParselet`处理字符串,`IdentifierParselet`处理变量引用,`GroupingParselet`处理括号表达式,`CallParselet`处理函数调用(识别一对括号表示调用),`MemberParselet`处理成员访问(识别点号`.``BinaryOperatorParselet`根据运算符优先级解析二元运算符表达式等【7†】。Parser 根据定义的运算符优先级(`Precedence`类)解析出嵌套表达式树。例如,一个算术表达式会被解析成由`BinaryExpressionNode`节点连接的AST子树。
语法分析结果是一个AST节点列表通常整个源文件解析成一个`ModuleNode`根节点,其中包含模块名、导入列表、函数定义列表等子节点。如果在词法或语法阶段遇到非法输入,将抛出带有错误位置和原因的异常,中止编译。
* **语义分析器Semantic Analyzer**语法阶段产生AST后进入`semantic`包的语义检查阶段。`SemanticAnalyzerRunner`类会遍历AST节点对各种语义规则进行验证包括标识符引用解析、类型检查、函数签名检查等。主要组件有
* 符号表(`SymbolTable`用于维护当前作用域内的符号变量、函数、模块等的信息。SymbolTable 支持嵌套作用域,每个符号具有名称、种类(`SymbolKind`,区分变量、函数等)、类型(`Type`)等属性。语义分析过程中会根据作用域进入/离开情况创建或销毁符号表,例如进入函数时新建函数级符号表,离开时销毁。对变量声明,分析器会将其添加到当前符号表;对标识符引用,则沿着当前作用域向上查找符号定义以解析引用是否有效。
* 内置类型系统:`BuiltinType`枚举定义了语言支持的所有基础类型,包括**BYTE**(8位整数)、**SHORT**(16位整数)、**INT**(32位整数)、**LONG**(64位整数)、**FLOAT**(单精度浮点数)、**DOUBLE**(双精度浮点数)、**STRING**(字符串)、**VOID**(空类型,用于无返回值函数)【14†】。每个类型实现了`Type`接口并提供辅助方法,例如判断数值类型、类型兼容性检查(`isCompatible`)以及自动宽化转换(用于处理算术中小范围整数提升等)。
* 语义分析具体规则由各`Analyzer`类实现,分为表达式和语句两大类分析器:
* **表达式分析器**:如`BinaryExpressionAnalyzer`检查二元运算两侧操作数类型是否兼容(例如算术运算要求操作数为数值类型,比较运算要求两侧类型可比等),`CallExpressionAnalyzer`检查函数调用的参数类型和数量是否匹配被调用函数签名,`IdentifierAnalyzer`确保标识符已声明且在可见作用域内,`NumberLiteralAnalyzer``StringLiteralAnalyzer`主要标注字面量的类型等。对于暂未支持的表达式形式,会使用`UnsupportedExpressionAnalyzer`占位或报告错误。
* **语句分析器**:如`DeclarationAnalyzer`在变量声明时将新符号登记到符号表并检测重复定义,`AssignmentAnalyzer`检查赋值目标是否已声明且类型匹配,`IfAnalyzer``LoopAnalyzer`检查条件表达式类型是否正确(例如应该是布尔或可转换为布尔的值;目前由于没有独立布尔类型,可能规定为整数非零即真),并递归分析其内部语句块;`ReturnAnalyzer`检查返回语句是否在函数内以及返回值类型是否与函数声明的返回类型一致。如果语义检查发现错误,会通过`SemanticError`机制记录错误信息(如“不匹配的类型”“未定义的变量”等),通常编译器会收集所有语义错误然后停止编译。
经过语义分析后AST节点将附加必要的类型和符号引用信息确保后续阶段可以安全地生成代码。
* **中间表示IR生成**:在`compiler.ir`包中编译器将经过检查的AST转换为中间代码IRIntermediate Representation。IR采用**三地址码**风格的指令序列,抽象地表示计算过程。主要结构包括:
* `IRValue`及其子类表示IR中的操作数可以是常量(`IRConstant`)、虚拟寄存器(`IRVirtualRegister`,用于表示中间计算结果)或标签(`IRLabel`,用于控制流跳转目标)。
* `IRInstruction`及子类:表示各类中间指令。如算术运算`IRAddInstruction`、函数调用`CallInstruction`、条件跳转`IRJumpInstruction`、返回`IRReturnInstruction`等。这些IR指令通常是三地址形式`target = op (operand1, operand2)`)。
* IR构建由`IRProgramBuilder`驱动。它会遍历AST针对每个函数定义创建一个`IRFunction`来容纳该函数的指令序列。IR生成过程借助多个Builder类`ExpressionBuilder`负责表达式节点转换,`StatementBuilder`负责语句节点转换,二者利用`InstructionFactory`创建具体IR指令。例如对一个变量声明节点IR可能生成一条初始化赋值指令对一个加法表达式ASTExpressionBuilder会生成相应的取操作数指令和加法指令。所有指令追加到当前函数的IR列表中。
当前版本的编译器已实现基本的算术、函数调用等IR构造但**对控制流语句(如 if/loop)的IR支持尚不完整**。如果IR生成过程中遇到`IfNode``LoopNode`,由于`StatementBuilder`没有相应处理逻辑,将触发默认分支抛出 “Unsupported statement” 异常【33†】。换言之目前编译器无法将含有条件或循环的代码转换为IR。这是现阶段的一大局限亟待在将来补充例如为If节点生成条件判断和跳转的IR指令序列类似三地址码的条件跳转为Loop节点生成循环初始化、条件判断、跳转回loop开头等IR。项目中已经有基础的`IRJumpInstruction`等类但尚未整合进高层语言结构的IR生成。
* **后端代码生成**IR生成后编译器后端(`compiler.backend`包)会把IR转换为虚拟机可执行的字节码指令序列。主要步骤包括
* **寄存器分配**由于IR中的`IRVirtualRegister`是无限的抽象寄存器,需要映射到虚拟机实际可用的局部变量槽位。`RegisterAllocator`负责为每个IR函数分配本地变量槽编号。通常采用线性扫描等策略为IR函数内的虚拟寄存器分配一个较小范围的索引。
* **指令映射**`VMCodeGenerator`根据IR指令生成对应的虚拟机指令序列。通过`IROpCodeMapper`将IR操作码翻译为虚拟机操作码`VMOpCode`枚举定义了虚拟机支持的操作码。例如一个IR加法指令会被映射为虚拟机的加载操作(`LOAD`)、加法操作(`ADD`)和保存结果操作(`STORE`)等一系列指令。又如函数调用IR会映射为虚拟机的`Call`指令,在生成时还需处理被调函数地址:如果被调函数在当前编译单元内,可能立即确定地址;否则留待链接或运行时通过`CommandLoader`解析。后端生成的结果通常是每个函数对应一段指令序列以及入口点信息,汇总为可装载到虚拟机的程序表示(`VMProgramBuilder`可能负责构建最终指令列表)。
最终,编译器将产出包含虚拟机指令的程序对象,准备交由虚拟机执行。
### 虚拟机部分结构
SCompiler 的虚拟机(位于`org.jcnc.snow.vm`包)是一个**基于栈的解释器**,模拟执行编译器产生的指令。主要模块包括:
* **虚拟机引擎VirtualMachineEngine**:虚拟机的核心执行循环位于`vm.engine.VirtualMachineEngine`类中。它负责初始化运行时状态(如创建操作数栈、调用栈、全局/本地变量空间等),加载编译器生成的指令列表,然后进入主循环取指令执行。每条指令通过`VMCommandExecutor`分发给相应的`Command`处理类执行。VirtualMachineEngine 支持不同运行模式(`VMMode`),并提供必要的调试输出(通过`VMStateLogger`记录执行过程)。
* **指令集及执行**:指令以**操作码OpCode+ 可选操作数**的形式表示,项目中每种指令实现为一个`Command`接口的实现类,位于`vm.commands`子包下,并按照功能分类:
* **栈操作**:如`Push`压栈指令、`Pop`出栈、`Dup`复制栈顶、`Swap`交换栈顶等。其中又细分为不同数据类型版本,例如`IPushCommand`/`LPushCommand`分别将32位/64位整数压入栈`BPushCommand`压入字节,等等。虚拟机通过这些指令管理操作数栈上的数据。
* **算术运算**包括各整数类型8/16/32/64位和浮点类型的加减乘除、取模、取负、增量等。如`IAddCommand``FSubCommand``LNegCommand``SDivCommand`等等,每个类封装对应类型的运算逻辑。还有类型转换指令在`arithmetic.conversion`包中,例如`I2FCommand`Int转Float`F2DCommand`Float转Double实现数值类型之间的自动转换。
* **按位运算**如按位与、或、异或当前实现了32位和64位整数的相应指令`IAndCommand`/`LAndCommand``IOrCommand`/`LOrCommand``IXorCommand`/`LXorCommand`
* **内存读写**:这里指访问本地变量槽位的指令,在`commands.memory`包下。如`ILoadCommand`/`IStoreCommand`用于读取/写入32位整数到局部变量`FLoadCommand`/`FStoreCommand`用于浮点数,等等。通用的`MovCommand`可能用于将栈顶数据移动到特定槽位或从槽位移动到栈顶。通过这些指令虚拟机可以访问模拟的“栈帧”中的局部变量区域类似JVM的局部变量表
* **控制流**:包括无条件跳转`JumpCommand`以及条件分支跳转。目前仅针对32位整数实现了比较跳转指令`ICECommand`Int Compare Equal比较两个整数若相等则跳转`ICLCommand`Int Compare Less小于则跳转等六种比较操作==, !=, <, <=, >, >=)。这些指令通常由编译器在实现条件语句时利用(不过目前编译器尚未完成相应生成)。
* **函数调用**`CallCommand``RetCommand`用于函数调用和返回。调用指令会处理新的栈帧的创建、参数传递和跳转至目标函数地址,返回指令则清理栈帧、恢复调用点等。
* **虚拟机控制**`commands.vm`子包下可能包含虚拟机级别的指令,如`HaltCommand`用于停止程序执行等。
* **运行时数据结构**`vm.module`包定义了虚拟机执行所需的数据结构:
* `OperandStack`(操作数栈):后进先出栈,用于算术运算、函数调用等过程中临时存放操作数和结果,与各指令交互。
* `LocalVariableStore`(局部变量存储):模拟每个函数调用帧的本地变量区,通常以数组形式按索引存取。不同类型占据相应大小的槽位。
* `CallStack``StackFrame`:调用栈由一系列栈帧构成,每个`StackFrame`保存一次函数调用的上下文(包括局部变量存储、返回地址、上一帧引用等)。`CallStack`管理栈帧的压入弹出,实现函数调用链。
* `MethodContext`:可能封装一次方法执行过程中的上下文,方便指令访问操作数栈和本地变量等。
* **指令加载与执行流程**:当编译器产出字节码后,通常由`VMInitializer`/`VMLauncher`类加载程序。在运行时,`CommandLoader`根据操作码构造相应`Command`对象(通过`CommandFactory`工厂映射操作码到具体`Command`类)。`VirtualMachineEngine`依次取出指令操作码利用CommandFactory得到命令实例后调用其执行方法。每个`Command`类实现了具体执行逻辑比如二元运算指令会从OperandStack弹出两个操作数计算结果再压回Load指令会从当前栈帧的LocalVariableStore取值压入栈顶Jump指令会修改指令指针等。`CommandExecutionHandler`可能维护着指令指针和循环,逐条执行直到遇到`HaltCommand`或运行完所有指令。整个过程中`VMUtils``LoggingUtils`可以提供调试和日志支持。
总的来说,虚拟机按照**取指令-译码-执行**的循环运行,被设计为与编译器生成的指令集相契合。其指令集针对静态类型数据进行了划分,实现简单但可满足基本运算需求。
## 编译器支持的语法结构
当前版本的 SCompiler 编译器支持以下主要的语言元素和语法结构:
* **基本数据类型**支持多种原生数据类型包括整数8位Byte, 16位Short, 32位Int, 64位Long、浮点数32位Float, 64位Double、字符串(String)以及特殊的Void类型。变量和函数定义需要指明类型编译器在语义分析时会检查类型一致性。
* **变量声明与赋值**:使用关键字`declare`进行变量声明,例如:`declare x: INT = 5`。声明语句包含变量名、类型以及可选的初始化表达式。赋值则通过赋值运算符`=`实现,如`x = x + 1;`。赋值语句不需要特殊关键字,在语法解析时,如果一行以标识符开头并紧跟`=`, 则被识别为赋值操作。编译器允许在函数体内声明局部变量(模块层可能也允许全局变量声明),并支持用表达式初始化。此后即可在后续代码中引用该变量。
* **表达式**:支持丰富的表达式类型:
* 算术运算:`+`, `-`, `*`, `/`, `%` 等算术符,适用于数值类型。还支持一元负号(取反)。
* 比较运算:`==`, `!=`, `<`, `<=`, `>`, `>=` 等比较符通常产出一个表示真假的值目前未明确布尔类型用整数0/1表示真假
* 字面量:整数(支持不同范围,如`123`默认推导为Int或Long等、浮点数`3.14`推为Float/Double、字符串字面量`"hello"`)。编译器能正确标注字面量类型并将其用作常量。
* 标识符引用:对已有变量或常量的引用,可参与运算或赋值。
* 函数调用:使用括号`()`调用函数,支持传递实参表达式列表。例如`result = foo(a + 1, "test")`。解析器识别函数名后遇到`(`即进入调用解析将参数表达式列表构造成AST的`CallExpressionNode`。语义分析会检查被调用函数是否存在以及参数类型是否匹配定义。
* 成员访问:语法支持点号`.`进行成员访问。例如`moduleName.var`或将来类的字段访问。目前由于未实现类/结构,`.`主要用于模块命名空间引用通过import导入模块后可用ModuleName.element方式引用其公开符号
* 括号分组:支持使用`( ... )`改变运算优先级或作为强制分组,例如`(a + b) * c`。括号中的子表达式被视为一个整体节点。
* **控制流**:语言设计包含基本的流程控制结构:
* 条件分支if-else可以编写`if 条件:` 开始一个条件块,然后缩进编写分支语句。支持可选的`else:`块。语法分析会生成对应的`IfNode`含条件表达式、then块语句列表、以及可选的else块语句列表。注意当前编译器能识别if语法并通过语义检查但由于IR和虚拟机支持未完善**含有if的代码无法正确编译执行**(详见后续不足部分)。
* 循环loop支持类似`loop 条件:`或无条件的循环结构(具体语法可能类似`loop:`表示无限循环,或`loop 条件:`表示当条件为真时循环)。解析产生`LoopNode`其中包含循环条件表达式或无条件以及循环体语句列表。与if相似目前语法和AST层面支持循环但实际编译执行尚未完全实现。
* **函数定义与调用**:可以定义有参数和返回值的函数。函数定义以关键字`function`开始
函数可以有多个参数(形参列表带类型注解),以及指定返回类型。函数体内部可以声明局部变量、编写控制流等。支持在函数体内使用`return`语句返回值或提前退出。编译器对函数调用进行了支持包括解析调用表达式、检查参数、在IR中生成调用指令以及虚拟机执行函数调用机制。当前实现应允许运行时递归调用等基本功能。**注意**:函数参数在语义分析时会加入函数作用域符号表,并在调用时按值传递。返回类型为`VOID`表示不返回值,`return`语句可省略值。
* **模块与导入**:语言支持将代码组织为模块。每个源文件可以声明`module 模块名:`作为开头,模块内部可以定义多个函数和(可能的)全局变量,模块结尾以减少缩进(或`end module`标示)结束。可以使用`import 模块名`导入其他模块,以便调用其函数或使用其定义。当前实现的模块机制主要体现在解析和符号登记上:`ModuleParser``ImportParser`能够构建模块AST和导入AST`ModuleRegistry``ModuleInfo`在语义阶段记录模块信息。然而,由于项目规模所限,模块间调用可能还没有完全实现链接(例如可能需要在编译或加载时解析跨模块引用)。但至少语法上模块化是支持的,未来可扩展为多文件编译。
* **其他语句**:包括`return`语句用于函数返回,`return`后可跟表达式作为返回值或在void函数中单独使用。空语句或仅包含表达式求值比如函数调用而不使用返回值也是允许的作为Expression Statement。这些都由相应解析器和分析器处理。**注意**:目前没有显式的`break`/`continue`用于循环中断,也没有异常抛出/捕获语法。
综上SCompiler目前提供了一个小型静态类型语言的基本要素原生类型、变量、表达式、函数、模块、流程控制等核心结构在语法层面大多具备。但是由于开发进度限制有些虽然语法上存在但尚未完全支持执行如if/loop详见下节。
## 尚未支持的语法特性
尽管基础已经打好但与现代编程语言相比SCompiler当前缺少或未完善以下常见特性
* **类和面向对象特性**:不支持类(class)、对象、继承、多态等OOP结构。代码中没有类定义语法因而不能创建自定义复合类型或通过对象调用方法。这限制了用户只能使用过程式的模块和函数来组织代码。
* **更细粒度的作用域**目前仅有模块和函数级作用域。缺乏块级作用域Block Scope的完善支持。例如`if``loop`内部声明的变量,按理应只在该块可见,但由于语义实现未明确区分,这些变量可能被当作函数级变量处理。这可能导致作用域规则的不严格,变量名冲突或生命周期管理不够健全。后续需要引入块作用域,使得进入`if/else`、循环体时能推入新作用域SymbolTable离开时弹出以符合语言直觉。
* **数组和集合类型**:没有提供数组、列表等集合类型的数据结构及其语法。无法直接声明如`int[]`或使用下标访问元素。目前所有类型都是单值的基础类型和字符串,程序员无法方便地存储序列化数据。数组作为最基本的数据结构之一,应当在后续版本中考虑加入,包括其类型表示、字面量语法(如`[1,2,3]`以及相关的IR和指令支持如元素读取、写入
* **模块化的完善**虽然有模块声明和import语法但当前实现可能局限于单一编译单元缺少真正**模块分离编译和链接**的机制。例如导入的模块是否在编译时解析还是需要运行时加载目前不清晰。也没有命名空间管理机制来避免不同模块符号冲突。现代语言通常支持将模块编译为独立单元再链接SCompiler 需要进一步完善模块系统,使多文件项目的编译成为可能,包括符号导出/导入、依赖解析等。
* **错误处理机制**:没有提供异常处理或其他错误处理语法。例如`try-catch`结构在语言中不存在函数也没有声明抛出异常的概念。这意味着在运行时发生错误只能通过返回错误码或简单停止虚拟机。现代语言基本都有错误处理机制SCompiler在这方面还是空白。将来可能需要设计`throw/try/catch`或者其它错误处理形式,并在虚拟机中支持异常的传播与捕获。
* **布尔类型和逻辑运算**目前没有独立的Boolean布尔类型。条件判断使用整数代替布尔非0为真0为假这虽可行但不够直观和类型安全。另外逻辑短路运算符如`&&`, `||`, `!`等未提及估计未实现。如果需要编写复杂条件用户只能使用按位运算或嵌套if代替语义上不完善。应考虑加入布尔类型和逻辑运算支持使条件表达式更加清晰。
* **其他高级特性**:诸如三目运算符(`a ? b : c`)、`switch`/`match`多分支选择结构、函数重载/泛型、lambda表达式、宏等更高级的语言功能目前都未支持。这些虽然不是“最基本”特性但值得在语言扩展时考虑其取舍和实现难度。
综上所述SCompiler当前的语言特性集中在过程式编程的基本面尚未涉及面向对象、异常处理等更高层次的概念。一些已经在语法层出现的元素如if/loop、模块也因后端缺失而暂无法使用。这些不足在下一节的改进建议中将重点讨论。
## 编译-执行流程说明
下面结合以上结构,梳理编译器将源代码转换为可执行指令并由虚拟机运行的整体流程:
1. **词法分析**`LexerEngine`读取源文件文本字符流,一段一段地应用各`TokenScanner`规则。每当识别出一个记号如一个关键字、一串标识符、一个数字等就产生相应的Token对象加入Token序列。连续空白和注释被跳过。最终得到按出现顺序排列的Token列表作为语法分析的输入。
2. **语法分析**`ParserEngine`从Token流构建抽象语法树(AST)。它首先识别文件是否声明了`module`,如果有则调用`ModuleParser`生成一个`ModuleNode`作为AST根。然后依次读取后续Token遇到`import`关键字则用`ImportParser`解析导入声明加入AST遇到`function`关键字则调用`FunctionParser`解析函数定义节点(`FunctionNode`)并加入当前模块AST直到Token流结束。函数内部Parser会根据缩进层级处理嵌套结构函数体内的每一行语句由`StatementParserFactory`选择合适的解析器生成AST节点`IfNode`, `LoopNode`, `DeclarationNode`, `ReturnNode`, `AssignmentNode`或一般`ExpressionStatementNode`)。同时表达式部分用运算符优先解析构造子树。例如,一个包含函数和控制流的源文件最终会得到:模块节点下有多个函数节点,函数节点下有若干子语句节点,每个语句节点可能还有表达式子节点,形成树状层次。
3. **语义分析**`SemanticAnalyzerRunner`遍历上述AST检查静态语义正确性。比如每个`IdentifierNode`标识符引用在当前或外层作用域中是否有声明(通过符号表查找);函数调用的`CallExpressionNode`的目标函数是否存在、参数数量和类型是否匹配函数签名;算术`BinaryExpressionNode`两侧类型是否可计算(如不能把整数和字符串直接相加);`ReturnNode`是否出现在函数内且返回类型符合函数声明;控制流`IfNode`/`LoopNode`的条件部分应当是可判定真假的类型等等。语义分析阶段还会把符号变量、函数的类型信息附加到AST节点上以方便后续使用。如果发现语义错误则记录错误信息并可在编译器输出中报告。只有当AST通过所有语义检查后才进入下一阶段。
4. **IR生成**编译器将语义正确的AST转换为中间表示IR。`IRProgramBuilder`按函数进行处理对每个函数的AST创建对应的`IRFunction`容器,然后调用`StatementBuilder`遍历函数体的每个语句节点逐一生成IR指令并加入该IRFunction。对表达式则使用`ExpressionBuilder`生成指令序列并返回表示结果值的`IRValue`(通常是一个虚拟寄存器)。举例来说,对于语句`declare x: INT = 5`IR生成可能如下
* 为常量5创建一个`IRConstant`,然后生成`LoadConstInstruction`将5加载到某个虚拟寄存器v1
* 再生成一条赋值指令(可能也是通过`BinaryOperationInstruction`实现将v1赋给变量x对应的存储位置
对于算术表达式如`a + b * 2`IR可能顺序为加载b常量2到v2计算`b*2`结果存入v3再加载a到v4计算`v4 + v3`结果存入新的v5最后v5即表达式结果。需要注意当前版本**尚未实现 if/loop 等控制流**的IR生成遇到这些节点会抛出异常中止IR构建【33†】。
5. **寄存器分配与指令生成**得到每个函数的IR后编译器后端开始生成最终字节码指令。首先`RegisterAllocator`扫描IR函数统计其用到的虚拟寄存器将它们映射到实际的局部变量槽编号。例如某函数IR用了v1..v5五个虚拟寄存器则给它们分配0-4号槽位对应虚拟机函数帧的局部变量0到4。接着`VMCodeGenerator`遍历IR指令列表将每条IR指令翻译成等效的虚拟机指令序列
* 对于计算类IR指令如加法`IRAddInstruction(target = op1 + op2)`生成顺序可能是先发出“将op1加载到操作数栈”的指令(例如对应类型的LOAD指令)再“将op2加载栈”的指令然后发出对应类型的ADD指令完成运算最后把结果存入target的位置如果target是一个已经分配的槽位则用STORE指令保存栈顶到该槽
* 对于控制转移类IR`IRJumpInstruction`生成一个无条件跳转的VM指令`JumpCommand`带目标地址对于将来可能实现的条件跳转IR会生成类似`ICECommand`这类指令配合跳转。
* 对于函数调用`CallInstruction`,生成`CallCommand`并在操作数中注明被调函数的入口地址或索引。如果被调用函数尚未确定地址,可能需要记录以便在链接或加载阶段回填。
* 对于返回`IRReturnInstruction`,生成对应的`RetCommand`,可能包括返回值处理和栈帧清理。
`VMProgramBuilder`会收集所有函数的指令序列,构建程序对象。例如可以为每个函数保存起始指令的索引,用于函数调用时跳转。此外可生成一个全局指令数组(类似汇编的机器码序列),方便按索引跳转。
6. **虚拟机执行**:完成以上编译步骤后,最终产出的是虚拟机可执行的指令列表(类似字节码文件内容,但在内存中表示)。接下来将由虚拟机加载并运行:
* **加载**`VirtualMachineEngine`初始化时,通过`CommandLoader`读取指令列表,将每条指令的操作码翻译为具体的`Command`对象。可以理解为把纯操作码流“链接”成可执行的指令对象流。有的实现可能在首次执行遇到某指令操作码时才动态创建`Command`对象(按需加载)。
* **运行**:引擎设置好栈等环境后,设置指令指针(程序计数器)从入口点开始。循环执行:取出当前指令对象,调用其执行方法(`Command.execute(...)`)。这个执行过程中会对虚拟机状态进行读写。例如算术指令从OperandStack弹出操作数、运算并压入结果调用指令创建新StackFrame、调整指令指针到被调函数入口返回指令弹出当前StackFrame、恢复调用者指令指针跳转指令修改指令指针跳到目标位置等。每执行完一条指令若未跳转则指令指针顺序加一。如此循环直至遇到程序结束指令例如`HaltCommand`)或指令指针越界。期间如果有异常指令(目前没有异常机制,则可能是虚拟机检测到非法操作如栈溢出等),虚拟机会中止执行并报告错误。
* **输出和调试**:如有需要,虚拟机可以输出计算结果到控制台或者通过`FileIOUtils`写文件等视具体实现而定项目中FileIOUtils可能提供简单IO操作。调试方面`VMStateLogger`可以在每步执行后打印当前操作数栈、局部变量等状态,从而帮助开发者跟踪程序运行轨迹。
通过上述阶段SCompiler将源码成功地翻译并运行。
编译器会lexer分出`module`,`function`,`declare`,`return`等Tokenparser建立ModuleNode("Demo")下挂一个FunctionNode("add")里面有DeclarationNode(sum)和ReturnNodesemantic检查类型a、b为Inta+b结果Int赋给sum也为Int返回类型匹配IR生成创建IRFunction("add")发出指令将a和b加载、加法、存sum、再加载sum返回后端转为虚拟机指令序列如`ILoad 0; ILoad 1; IAdd; IStore 2; ILoad 2; Ret`假定a槽0, b槽1, sum槽2虚拟机执行这些指令最终计算出正确结果返回给调用者。整个流程体现了编译各阶段各司其职将高级源代码逐步细化为低级操作并执行的过程。
## 当前设计的不足和局限
虽然 SCompiler 已经搭建了编译器和虚拟机的雏形,但在功能实现上还存在一些不足和局限,需要我们注意:
### 1. 语言语法支持的完整性与一致性
* **控制流支持不完整**:如上所述,`if``loop`等控制流结构仅停留在语法和语义层面编译器并不能将其转换为可执行代码【33†】。这导致当前语言事实上**无法使用条件分支和循环**大大限制了可编写程序的复杂度。这种半吊子的支持也破坏了一致性——开发者可能看到语法允许if/loop却无法正常运行造成困扰。
* **缺少布尔类型**没有真正的Boolean类型使语言在逻辑判断上不够清晰和安全。目前是用整数代替布尔这在语义上不严格而且隐含约定0/非0的做法可能导致误用比如不小心把普通整数当布尔判断。同时缺少逻辑运算符&&, ||会使复合条件判断变得繁琐。总体而言,基础类型系统还不完善。
* **面向对象特性缺失**语言只支持过程抽象函数而无数据抽象。现代语言的结构化和可扩展性往往来自类和模块体系SCompiler仅有模块/函数,难以组织大型程序或表示复杂数据关系。同时,点运算符用于模块成员访问的场景有限,没有类的话`.`无法访问对象属性,这部分语法能力闲置,显得不一致。
* **标准库和内建函数**目前未提及任何标准库函数或内建功能如打印输出函数字符串操作等。一个语言通常会有一些内置支持但SCompiler尚未提供这使得即使虚拟机支持I/O通过语言本身却无直接接口。功能匮乏会影响语言实用性。
* **表达式和语句局限**例如没有三目运算符、switch语句等。这些并非必需但缺少会使某些逻辑表达不够简洁。另外似乎也没有提供`break`/`continue`,导致无法提前跳出循环,只能依靠条件控制,减少了灵活性。错误处理如前述也完全没有,程序健壮性方面不足。
总的来说,语言目前只是**基本可用**状态,很多常见结构不是缺席就是不完善。这在设计上一方面是功能未做完,另一方面也反映出某些不一致:比如语法允许的东西语义/IR未跟上实现存在断层。
### 2. 编译器模块结构的可拓展性
* **新语法扩展难度**:编译器采用较清晰的模块划分,但要增加一类新的语法元素,需要在多个地方做改动。以增加`while`循环为例假设与loop语义不同需修改词法分析增加`while`关键字、语法分析增加WhileParser/AST节点、语义分析增加WhileAnalyzer、IR生成增加While支持构造循环IR、可能还要在虚拟机增加相应指令。这种多点修改流程对单人维护还好但随着语言复杂度增长手动同步多个阶段逻辑容易出错。因此目前编译器架构对于**快速拓展新特性**支持一般,没有提供更自动化或元编程的手段。
* **模块职责可能有混杂**当前实现中每一层都有许多类结构清晰但也存在潜在问题。例如IR生成器里StatementBuilder要识别各种AST类型然后调用不同生成过程缺少一种**统一的访问者模式**来自动分派。这意味着每新增一种Statement节点都要修改StatementBuilder代码违反一定程度的开闭原则。类似地语义分析通过注册Analyzer类来处理不同节点这还算比较解耦但IR这层次的处理稍显硬编码。
* **错误处理与调试支持不足**编译器在Lexer/Parser阶段遇错会抛异常Semantic阶段收集错误但整个流程缺少一个统一的错误管理机制比如可以收集多个阶段错误一并报告而不是遇错立即终止。拓展编译器功能时错误和异常处理的分散可能导致健壮性问题。此外缺少调试或日志接口使得排查编译问题要读代码加大维护成本。
* **可重用性与模块化**目前编译器各部分紧密协作但如果想将某部分替换例如换一种后端或不同IR难度较高。例如IR到字节码的映射是硬编码在VMCodeGenerator里的无法方便地替换成输出别的格式。再比如词法规则、语法规则写死在代码中不能通过配置或脚本调整。这在一定程度上限制了编译器的**灵活性**。
* **性能优化欠缺**虽然目前功能为主但随着扩展需要考虑编译性能和生成代码质量的问题。当前没有任何优化步骤如常量折叠、死码消除等IR也较原始。如果后续拓展语言特性编译器结构需要容纳优化模块这方面的预留尚不明显。
### 3. 虚拟机指令设计的可扩展性
* **指令集合扩展繁琐**虚拟机采用了一条指令一个类的设计不同数据类型的同类操作也分开实现。这虽然直观但当需要支持新类型或新操作时需要成倍增加类和操作码。例如若增加一个BOOL布尔类型则可能需要增加BAnd, BOr, BXor等指令类增加64位浮点比较也要写对应的比较指令类。指令数量膨胀会增加维护负担也容易出现不一致忘记某型的某操作。理想情况下应该有更通用的方式描述指令逻辑减少重复代码。
* **缺少复杂指令**目前虚拟机指令偏底层如算术、加载、跳转已经覆盖基本操作。但如果语言将来增加更复杂行为如函数闭包、协程、面向对象调用等现有指令可能无法直接支持需要新增一系列指令。例如面向对象需要指令处理堆内存和对象字段访问异常机制需要指令来抛出和跳转到异常处理位置。当前VM设计较简单新增复杂指令可能涉及调整整个调用栈或内存模型这并非易事。
* **类型和操作强耦合**正如前述每种数据类型在VM里都是一套独立指令。这种设计在类型较少时问题不大但扩展性差。如果将来支持用户定义类型例如结构或类实例虚拟机难以为每种用户类型都定制指令。需要一种更加类型无关或泛型化的指令体系。目前的设计更接近JVM的字节码风格JVM也是区分int, long等指令集但现代一些VM趋向于更统一的操作码或基于栈元素类型动态决定行为。SCompiler VM暂未体现这方面弹性。
* **内存管理简单**:虚拟机目前没有提及垃圾回收或内存分配指令。像字符串这类可能驻留在常量区,未涉及动态内存。此外,没有数组/对象,也回避了内存管理问题。但若后续加入这些,必须扩展虚拟机以支持动态分配、引用类型和垃圾回收算法。当前设计里没有预留这样的机制,这也是以后扩展的一大挑战。
* **性能与调优**现有虚拟机以解释执行方式运行所有指令没有JIT或优化执行的概念。如果字节码指令集膨胀会进一步拖慢解释速度。而增加优化执行则需要对指令集和执行器做大改动。可见目前VM设计追求易懂实现但当扩展功能后性能和复杂度都会上升需要提前考虑架构上的改进。
总结来说,虚拟机指令设计目前可满足简单程序的运行,但要扩展功能,**需要付出较多的手工工作**,且某些高级功能的加入会冲击现有设计。指令集的可扩展性有待提高,以便更从容地支持未来的语言增强。
## 优化与扩展建议
针对上述发现的问题,下面提出一些面向未来版本的改进方向,分别从语言特性、编译器架构和功能规划三方面给出具体建议。
### 1. 扩展语言语法支持
* **完善控制流实现**:优先补全`if``loop`的编译支持。具体做法是在IR生成阶段为If和Loop节点添加处理
* If语句可以为条件表达式生成比较指令和条件跳转IR。例如构造两个IRLabel一个指向else块或结束一个指向if块结束后位置。大致流程计算条件到寄存器生成`IRJumpInstruction(condFalseLabel, JUMP_IF_FALSE)`和块内指令块结束时若有else则跳过else块的跳转等等。然后在虚拟机指令映射时利用已有的比较和Jump指令如ICE等实现跳转逻辑。
* Loop循环根据是`while`类似有条件还是无限循环生成循环开始和结束Label。条件判断放在循环入口或尾部根据语义决定。如果是`loop condition:`类似while先判断条件跳转出循环或进入循环体体内末尾再跳转回开头形成闭环。利用`JumpCommand`实现回跳。需注意break/continue等结构暂时可以不支持或通过特殊跳转IR标识实现。
* 完成上述将使if/loop真正可用。同时**建议**加入`break``continue`简单实现可以在LoopAnalyzer中识别这两个关键字如果引入在IR生成时将`break`转成跳转到循环后Label`continue`跳转到循环判定Label。
* **增加布尔类型和逻辑操作**:在`BuiltinType`中加入BOOLEAN类型让语义分析和类型检查更明确if条件要求Boolean类型。Lexer和Parser增加布尔字面量true/false关键字和逻辑运算符词法。编译期将`&&`, `||`实现为短路逻辑可在表达式解析时将它们优先级定义低于比较运算然后在IR生成时特别处理——例如`cond1 && cond2`可生成计算cond1如果假直接跳过cond2计算输出假否则计算cond2作为结果。这种短路逻辑可用条件跳转指令实现。VM也需新增布尔运算支持比如直接按布尔处理AND/OR或复用整数1/0实现但最好还是类型独立。引入Boolean有助于清晰表达条件并为将来可能的布尔代数优化奠定基础。
* **支持数组和字符串操作**:数组方面,语法上可以增加数组类型表示和元素访问:例如类型`INT[]`,字面量`[1,2,3]`,以及表达式`arr[index]`。实现上需要:
* 在类型系统中引入数组类型表示,可设计`ArrayType`类包含元素类型和维度。
* 解析器在遇到类型标注如`TYPE[]`时构造ArrayType在表达式解析识别`Identifier [ Expression ]`模式为数组元素访问AST节点。
* 语义检查数组越界等可能暂不做复杂分析,只需检查索引是整数类型。
* IR和VM增加指令`NewArray`用于创建数组(需要操作数指定长度),以及`LoadElement`/`StoreElement`用于按索引读写数组元素。因为当前VM没有动态内存可能需要模拟一块连续内存比如让`OperandStack`或一个全局堆来存储数组元素。实现难度较大但可以从一维定长数组开始。
* 字符串目前只支持字面量,并未提到连接操作。可考虑支持用`+`连接字符串(很多语言如此)。在语义上检测`String + String`IR生成可调用运行时库函数或生成特殊指令。或者至少提供一些库函数如`concat(str1, str2)`
* **引入类和结构**如有计划支持OOP可以逐步来
* 先支持**结构体(struct)**:允许用户定义简单数据类型组合。语法类似`struct Point: x: INT, y: INT end struct`。编译器将创建一个新的类型SymbolKind为STRUCT记录字段列表。然后支持`point.x`这样的成员访问。VM需支持根据偏移访问结构字段的指令或者将struct当做数组加字段索引映射。
* 真正的**class**:在结构基础上加入方法和封装。语义上更复杂,需要`this`引用、方法表等。由于工程量巨大可暂不实现完整继承只实现单纯的类实例创建和方法调用。方法调用可在编译时静态绑定无继承多态时。VM需要指令支持分配对象在堆上和调用实例方法可能传入对象引用
* 由于涉及内存管理,必须设计堆和垃圾回收方案。可以先使用简单引用计数或手动内存释放的方法。指令上,需要指针/引用类型支持比如LoadRef/StoreRef等操纵引用以及一套新的调用机制支持虚方法如多态时
* **完善模块系统**:使编译器真正支持多模块项目编译:
* 允许编译器将每个模块单独编译为中间格式(例如每个模块产生自己的指令列表和符号表导出列表),然后有一个链接步骤将多个模块的符号解析、指令列表合并。可以引入**符号链接**阶段:`ModuleRegistry`在所有模块编译后解析import关系检查所需符号是否在被导入模块导出然后填充调用指令的目标地址等。
* 支持模块初始化代码比如模块级别的变量初始化或执行代码需要在程序启动时统一跑一遍。可规定入口模块并让VMLauncher按import依赖顺序执行模块初始化。
* 命名空间处理:确保不同模块中可以有相同名字的符号互不冲突(通过模块名限定)。编译器在语义分析标识符时,如遇非本模块定义则尝试从导入模块符号表查找。这部分逻辑需要完善。
* **错误和异常处理**:为了提高程序健壮性,可考虑添加基本的异常抛出/捕获机制:
* 语法:例如`throw 表达式`抛出异常;以及`try:` ... `catch var:` ... `end catch`或类似Python的缩进try-except结构。初期可以限定只能抛出简单类型如字符串或整数作为错误码
* 语义:需要一个统一的异常类型或父类;`throw`一定在函数有异常可抛的地方使用;`catch`块需定义接收异常对象的变量。
* IR/VM增加`ThrowInstruction`对应VM的`ThrowCommand`执行时会触发异常流程。虚拟机需要维护异常发生时的调用栈处理可以在StackFrame增加异常捕获表如果当前帧没有catch匹配则弹栈继续往上直到找到catch或无则终止程序。实现完整异常较复杂可以从简单的“非受检异常”开始任何地方throw上层不catch则终止
* 当然,错误处理也可以一开始用简单方案:例如标准库一个`panic(msg)`函数直接终止程序或者要求函数通过返回值反映错误。但长期看语言层支持try-catch是值得的。
* **其他改进**:随着语言功能增强,可以逐步添加:
* **条件表达式**:支持三目运算符`condition ? expr1 : expr2`解析为特殊的三分支AST并在IR用条件跳转实现。或者像Python一样支持`expr1 if cond else expr2`
* **循环增强**:除了`loop`(类似while)外,可增加`for`循环语法,支持迭代特定次数或范围;甚至支持`foreach`遍历数组(需容器支持后)。
* **函数默认参数/可变参数**:提升函数调用灵活性。不过这些需要在语义阶段处理默认值填充或参数列表打包解包,也需要审慎设计。
* **标准库**哪怕语言本身不直接提供IO语法也可以扩展标准库函数比如print/println用于输出len()用于取字符串或数组长度等等。这些函数可在编译器内置编译时识别调用并映射到特殊指令或VM内置实现。
总之,语言层面的扩展应遵循**优先基础、逐步提高**的策略。先把现有语法真正落地可用然后在此基础上添加新类型、新结构。在设计每个新特性时要考虑与现有部分的兼容和交互尽量保持语言的一致性。例如引入类后函数如何作为方法处理、作用域如何影响等都要有清晰规则。通过不断完善SCompiler可以从一个小型脚本语言成长为具有现代语言基本特征的系统。
### 2. 优化编译器架构的模块化与可扩展性
* **采用设计模式提高扩展性**:可以运用更加模块化的设计模式来改进编译器结构。例如:
* 在AST到IR的遍历中引入**访问者模式(visitor)**为AST节点增加接受访问者的方法IR生成器实现一个IRVisitor根据节点类型生成IR。这样新增节点类型时只需添加相应visitor处理而不用修改通用遍历逻辑。当前的`StatementBuilder/ExpressionBuilder`有些类似visitor但不是严格模式可重构为visitor模式以提升一致性。
* 语法和语义分析已用了工厂和注册表模式如StatementParserFactory, AnalyzerRegistry这很好地支持了通过注册添加新解析器/分析器。可以继续发扬:将关键字与解析器的绑定配置化,甚至通过读取配置文件/注解实现自动注册,这样添加新语法时减少改动集中在一处。
* 对于指令和操作码映射,同样可以使用**工厂+反射**来减少硬编码。例如VM指令的操作码和Command类对应关系可在一个配置表中新增指令时只需添加表项不必修改大量代码。这在编译器后端(IROpCodeMapper)和虚拟机CommandFactory中都适用。
* **划清各阶段接口**:为每个编译阶段定义清晰的输入输出接口,有助于模块化和调试。例如:
* Lexer输出Token流对象Parser输入Token流输出AST对象Semantic输入AST输出注释过的AST或符号表。IR生成输入AST输出IRProgram后端输入IRProgram输出VMProgram。若能把这些接口抽象出来甚至可以方便地插入或替换某个阶段实现比如以后想支持不同前端语法只要产出相同IR即可
* 通过明确定义阶段边界,也可以插入**优化器**例如在IR生成后增加IR优化阶段输入IRProgram输出优化后的IRProgram然后再交给后端。因为接口稳定所以对其他部分无影响。
* **增强错误处理和调试机制**:编译器应提供统一的错误报告系统。可以引入一个`CompilationErrorReporter`收集各阶段的错误和警告信息,并在最终输出。取代目前抛异常中断的做法,让编译尽可能检测所有问题一次性反馈给用户。对于调试编译过程,可加入**日志选项**比如提供verbose模式打印Lexer生成的token列表、Parser生成的AST树可以借助现有`ASTJsonSerializer`将AST输出JSON、语义分析的符号表、IR指令列表等。这对开发者理解编译器行为和定位问题很有帮助。
此外,考虑未来多人协作或用户自定义扩展,可设计错误码和文档,方便扩展者遵循统一规范添加新的错误类型。
* **提高复用性**让编译器的一些组件更独立可移植到其他项目或用途。例如Lexer可以设计成无需依赖整个编译器框架就能工作输入源码输出tokens。这样一来如果将来语言有IDE插件或其他工具可直接使用编译器组件而不必启动完整编译流程。再如符号表、类型系统等也可作为独立模块提供API。这种松耦合设计能让扩展和重构更加从容。
* **配置与脚本化**目前所有规则都硬编码在Java类中可以考虑引入外部配置或脚本语言描述部分编译逻辑。例如用一种元语言定义文法和生成解析器而不是手写所有Parselet或者用配置文件列出关键字和TokenScanner、StatementParser映射关系。这种改动前期成本较高但对以后拓展新语法非常有利——可能只要修改配置就能支持简单的新关键字。此外若有精力还可以尝试**生成器**模式比如根据BuiltinType列表自动生成各类型指令的代码框架以减少手工重复。
* **面向未来的优化**:如果后续打算提升生成代码质量,可以将架构调整为便于插入优化算法:
* IR可以考虑使用\*\*控制流图(CFG)\*\*形式,以更好地做数据流分析、死代码消除等优化。
* 模块间优化(比如内联、常量传播)也需要编译器能够跨函数或模块看待,这会影响编译流程的架构。提前设计扩展点(如预留优化阶段钩子)可避免以后大改。
* 另一个方向是支持多目标后端输出。也许未来不仅运行在自制VM上还可以输出LLVM IR或Java字节码等。为了这个可能性前端和后端的解耦要做好。可以设计接口`CodeGenerator`当前实现一个生成Snow VM字节码将来可以有生成其它平台代码的实现。
总之,优化编译器架构的关键词是\*\*“解耦、扩展、可配置”\*\*。通过更好的分层和约定,使得添加功能不需要大改现有代码,尽可能遵循开闭原则。同时,让编译器核心更加通用,以适应语言演进的需求。这一过程可以逐步进行,每次重构一种机制,使整体变得健壮灵活。
### 3. 后续功能实现规划
为了使 SCompiler 逐步具备现代编程语言的基本特性,建议按照由易到难、由基础到高级的顺序进行后续开发。以下是一个可能的规划路线:
1. **修复现有缺失**:首先解决当前明显的不完整之处,使语言最基本构造可正常工作。这一步重点是完成 if/loop 的IR和执行支持以及添加Boolean类型和逻辑运算符。如前所述这将显著提高语言可用性让用户能编写条件和循环逻辑。
2. **增强类型系统**:在基本类型齐备后,可考虑引入更复杂类型如数组、结构体。数组相对独立,可以较快实现一维数组支持并提供基本库函数。结构体/类较复杂,可以留到稍后。同时,完善类型检查,比如实现自动类型提升规则(`Type.widen`方法已经定义用于比如把Short提升为Int以参与运算【14†】保证表达式类型推导符合期望。
3. **模块和作用域**:完善模块导入链接机制,实现多文件编译。确保符号表正确管理不同模块作用域和命名空间。同时,实现块级作用域:修改 Parser/Semantic在进入新的缩进块时如函数体、if/else块、loop块创建子符号表离开时销毁。测试在嵌套块中声明同名变量会屏蔽外层变量等行为是否正确。这一步完成后作用域管理将更严谨模块化也真正可用。
4. **异常处理和标准库**:加入基本的`throw/try/catch`机制实现运行时错误捕获。可先支持在函数内捕获自己抛出的异常不跨函数传播逐步再实现跨栈传播。并增加一些标准库功能如I/O可以在VM里实现System.out风格输出指令或提供标准库函数由底层IO支持、字符串操作函数子串、拼接等数学函数库等。这些丰富程序功能同时为后面支持更复杂应用打基础。
5. **面向对象支持**如果计划支持类在上述过程稳定后着手。先引入class定义语法和对象创建可能用`new`关键字。实现成员变量、方法的解析和符号管理。虚拟机需要重大升级支持对象引用和堆分配这可能是最艰巨的一步。可考虑借鉴JVM或其他简易OO语言的运行时模型。逐步实现方法调用、`this`引用最后考虑继承和多态。如果过于困难也可以选择只支持简单类结构、不涉及继承类似C的struct加函数指针风格
6. **优化和完善**当语言主要特性都有后可以回头优化编译器和VM性能。例如
* 编译器增加优化步骤如常量折叠在IR生成时把常量表达式直接计算减少运行时开销。
* 虚拟机可以考虑实现字节码直接线程化或JIT例如把指令序列翻译为本地代码段执行。这需要深入的专业知识可根据需要决定。
* 增强调试支持:比如实现源码级调试功能,需要在编译时保留行号映射信息,在虚拟机执行时跟踪当前源码行,允许断点、单步。这对一个成熟语言实现很重要。
* 补充更多标准库或与宿主系统交互功能,让语言更有用(比如文件操作、网络库等)。
7. **持续测试和文档**:在每个阶段,都需要编写大量测试用例验证新特性的正确性,并完善语言文档供用户参考。例如,当添加数组后,要在文档中说明其用法和限制。保持文档与实现同步能避免用户困惑,也方便开源协作时吸引他人参与。

View File

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

View File

@ -11,27 +11,30 @@ import org.jcnc.snow.compiler.semantic.type.BuiltinType;
import org.jcnc.snow.compiler.semantic.type.Type;
/**
* {@code IfAnalyzer} 是用于分析 {@link IfNode} 条件语句的语义分析器
* {@code IfAnalyzer} 用于分析 if 语句的语义正确性
* <p>
* 它负责验证 if 条件表达式的类型合法性并递归分析 then 分支和 else 分支中的所有子语句
* 分析规则如下
* 主要职责如下
* <ul>
* <li>if 条件必须为 {@link BuiltinType#INT} 类型用于表示布尔真值</li>
* <li>若条件类型不为 int将报告语义错误并继续分析</li>
* <li>then else 分支中的语句将分别递归进行语义分析</li>
* <li>若某个子语句无匹配分析器将回退到默认处理器或报告不支持</li>
* <li>条件表达式类型检查确认 if 的条件表达式类型为 int用于真假判断否则记录语义错误</li>
* <li>块级作用域分别为 then 分支和 else 分支创建独立的符号表SymbolTable
* 支持分支内变量的块级作用域防止分支内声明的变量污染外部或互相干扰允许分支内变量同名遮蔽</li>
* <li>分支递归分析 then else 分支的每条语句递归调用对应的语义分析器进行语义检查</li>
* <li>错误记录若遇到条件类型不符不支持的语句类型或分支内部其他语义问题均通过 {@link SemanticError} 记录详细错误信息并附带代码位置信息</li>
* <li>健壮性不会因一处错误立即终止而是尽量分析全部分支收集所有能发现的错误一次性输出</li>
* </ul>
* <p>
* 该分析器提升了语言的健壮性与可维护性是支持 SCompiler 块级作用域及全局错误收集能力的关键一环
*/
public class IfAnalyzer implements StatementAnalyzer<IfNode> {
/**
* 执行 if 语句的语义分析包括条件类型检查和分支分析
* 分析 if 语句的语义合法性包括条件表达式类型分支作用域及分支语句检查
*
* @param ctx 当前语义分析上下文提供注册表错误收集日志等服务
* @param mi 当前模块信息本实现未直接使用
* @param fn 当前所在函数节点本实现未直接使用
* @param locals 当前作用域符号表提供变量查找支持
* @param ifn 要分析的 {@link IfNode} 语法节点包含条件表达式及两个分支语句块
* @param ctx 语义分析上下文记录全局符号表和错误
* @param mi 当前模块信息
* @param fn 当前所在函数
* @param locals 当前作用域符号表
* @param ifn if 语句 AST 节点
*/
@Override
public void analyze(Context ctx,
@ -40,31 +43,45 @@ public class IfAnalyzer implements StatementAnalyzer<IfNode> {
SymbolTable locals,
IfNode ifn) {
// 1. 分析并校验条件表达式类型
ctx.log("检查 if 条件");
// 1. 检查 if 条件表达式类型
// 获取对应条件表达式的表达式分析器
var exprAnalyzer = ctx.getRegistry().getExpressionAnalyzer(ifn.condition());
Type cond = exprAnalyzer.analyze(ctx, mi, fn, locals, ifn.condition());
if (cond != BuiltinType.INT) {
ctx.getErrors().add(new SemanticError(
ifn,
"if 条件必须为 int 类型(表示真假)"
));
ctx.log("错误: if 条件类型不为 int");
// 对条件表达式执行类型分析
Type condType = exprAnalyzer.analyze(ctx, mi, fn, locals, ifn.condition());
// 判断条件类型是否为 intSCompiler 约定 int 表示真假否则报错
if (condType != BuiltinType.INT) {
ctx.getErrors().add(new SemanticError(ifn, "if 条件必须为 int 类型(表示真假)"));
}
// 2. 递归分析 then 分支语句
// 2. 分析 then 分支
// 创建 then 分支的块级作用域以当前 locals 为父作用域
SymbolTable thenScope = new SymbolTable(locals);
// 遍历 then 分支下的每一条语句
for (var stmt : ifn.thenBranch()) {
// 获取对应语句类型的分析器
var stAnalyzer = ctx.getRegistry().getStatementAnalyzer(stmt);
if (stAnalyzer != null) {
stAnalyzer.analyze(ctx, mi, fn, locals, stmt);
// 对当前语句执行语义分析作用域为 thenScope
stAnalyzer.analyze(ctx, mi, fn, thenScope, stmt);
} else {
// 若找不到对应的分析器记录错误
ctx.getErrors().add(new SemanticError(stmt, "不支持的语句类型: " + stmt));
}
}
// 3. 递归分析 else 分支语句若存在
// 3. 分析 else 分支可选
if (!ifn.elseBranch().isEmpty()) {
// 创建 else 分支的块级作用域同样以 locals 为父作用域
SymbolTable elseScope = new SymbolTable(locals);
// 遍历 else 分支下的每一条语句
for (var stmt : ifn.elseBranch()) {
var stAnalyzer = ctx.getRegistry().getStatementAnalyzer(stmt);
if (stAnalyzer != null) {
stAnalyzer.analyze(ctx, mi, fn, locals, stmt);
// 对当前语句执行语义分析作用域为 elseScope
stAnalyzer.analyze(ctx, mi, fn, elseScope, stmt);
} else {
ctx.getErrors().add(new SemanticError(stmt, "不支持的语句类型: " + stmt));
}
}
}
}

View File

@ -11,29 +11,27 @@ import org.jcnc.snow.compiler.semantic.type.BuiltinType;
import org.jcnc.snow.compiler.semantic.type.Type;
/**
* {@code LoopAnalyzer} 是用于分析 {@link LoopNode} 循环结构的语义分析器
* {@code LoopAnalyzer} 用于分析 for/while 等循环结构的语义正确性
* <p>
* 支持的循环结构包括典型的 C 风格 for-loop由初始化语句条件表达式更新语句和循环体组成
* <p>
* 分析流程如下
* <ol>
* <li>分析初始化语句 {@code initializer}</li>
* <li>分析并验证条件表达式 {@code condition} 类型是否为 {@link BuiltinType#INT}</li>
* <li>分析更新语句 {@code update}</li>
* <li>递归分析循环体 {@code body} 中的每个子语句</li>
* <li>所有阶段若发现语义错误均记录至 {@link Context#getErrors()}并写入日志</li>
* </ol>
* 主要职责如下
* <ul>
* <li>为整个循环体包括初始化条件更新循环体本身创建独立的块级符号表作用域保证循环内变量与外部隔离</li>
* <li>依次分析初始化语句条件表达式更新语句和循环体各语句并递归检查嵌套的语法结构</li>
* <li>检查条件表达式的类型必须为 int布尔条件否则记录语义错误</li>
* <li>支持所有错误的收集而不中断流程便于一次性输出全部问题</li>
* </ul>
* 该分析器实现了 SCompiler 语言的块级作用域循环与类型健壮性是健全语义分析的基础部分
*/
public class LoopAnalyzer implements StatementAnalyzer<LoopNode> {
/**
* 执行循环结构的语义分析
* 分析循环结构 forwhile的语义合法性
*
* @param ctx 当前语义分析上下文提供模块信息错误记录日志输出和分析器注册表等服务
* @param mi 当前模块信息本方法中未直接使用保留用于接口一致性
* @param fn 当前函数节点本方法中未直接使用保留用于可能的上下文分析扩展
* @param locals 当前作用域的符号表记录变量类型及其可见性
* @param ln 待分析的 {@link LoopNode} 节点包含初始化语句条件表达式更新语句和循环体
* @param ctx 语义分析上下文错误收集等
* @param mi 当前模块信息
* @param fn 当前所在函数
* @param locals 外部传入的符号表本地作用域
* @param ln 当前循环节点
*/
@Override
public void analyze(Context ctx,
@ -42,35 +40,39 @@ public class LoopAnalyzer implements StatementAnalyzer<LoopNode> {
SymbolTable locals,
LoopNode ln) {
// 1. 分析初始化语句
ctx.log("检查 loop 循环");
// 1. 创建整个循环结构的块级作用域
// 新建 loopScope以支持循环内部变量声明与外部隔离
SymbolTable loopScope = new SymbolTable(locals);
// 2. 分析初始化语句 for(i=0)使用 loopScope 作为作用域
var initAnalyzer = ctx.getRegistry().getStatementAnalyzer(ln.initializer());
if (initAnalyzer != null) {
initAnalyzer.analyze(ctx, mi, fn, locals, ln.initializer());
initAnalyzer.analyze(ctx, mi, fn, loopScope, ln.initializer());
}
// 2. 分析条件表达式并检查类型
// 3. 分析条件表达式 for(...; cond; ...) while(cond)
var condAnalyzer = ctx.getRegistry().getExpressionAnalyzer(ln.condition());
Type cond = condAnalyzer.analyze(ctx, mi, fn, locals, ln.condition());
if (cond != BuiltinType.INT) {
ctx.getErrors().add(new SemanticError(
ln,
"loop 条件必须为 int 类型(表示真假)"
));
ctx.log("错误: loop 条件类型不为 int");
Type condType = condAnalyzer.analyze(ctx, mi, fn, loopScope, ln.condition());
// 条件类型必须为 int bool否则记录错误
if (condType != BuiltinType.INT) {
ctx.getErrors().add(new SemanticError(ln, "loop 条件必须为 int 类型(表示真假)"));
}
// 3. 分析更新语句
// 4. 分析更新语句 for(...; ...; update)
var updateAnalyzer = ctx.getRegistry().getStatementAnalyzer(ln.update());
if (updateAnalyzer != null) {
updateAnalyzer.analyze(ctx, mi, fn, locals, ln.update());
updateAnalyzer.analyze(ctx, mi, fn, loopScope, ln.update());
}
// 4. 递归分析循环体中的每个语句
// 5. 分析循环体内的每一条语句
for (var stmt : ln.body()) {
var stAnalyzer = ctx.getRegistry().getStatementAnalyzer(stmt);
if (stAnalyzer != null) {
stAnalyzer.analyze(ctx, mi, fn, locals, stmt);
// 递归分析循环体语句作用域同样为 loopScope
stAnalyzer.analyze(ctx, mi, fn, loopScope, stmt);
} else {
// 不支持的语句类型记录错误
ctx.getErrors().add(new SemanticError(stmt, "不支持的语句类型: " + stmt));
}
}
}

View File

@ -9,46 +9,41 @@ import java.util.List;
import java.util.stream.Collectors;
/**
* {@code SemanticAnalyzerRunner} 是语义分析的统一入口
* {@code SemanticAnalyzerRunner} 是语义分析阶段的统一入口与调度器
* <p>
* 负责从原始 AST 中提取模块节点调用语义分析主流程
* 并在出现语义错误时统一报告并终止编译流程
* <p>
* 使用方式
* <pre>{@code
* SemanticAnalyzerRunner.runSemanticAnalysis(ast, true);
* }</pre>
* <p>
* 功能概述
* 功能职责
* <ul>
* <li>筛选出所有 {@link ModuleNode} 节点模块级入口</li>
* <li>调用 {@link SemanticAnalyzer} 执行完整语义分析流程</li>
* <li>将收集到的 {@link SemanticError} 列表交由报告器处理</li>
* <li>若存在语义错误调用 {@link SemanticAnalysisReporter#reportAndExitIfNecessary(List)} 自动中止流程</li>
* <li>从原始 AST 列表中过滤并收集所有 {@link ModuleNode} 节点作为模块分析的起点</li>
* <li>调用 {@link SemanticAnalyzer} 对所有模块节点执行完整语义分析流程</li>
* <li>汇总并报告所有 {@link SemanticError}如有语义错误自动中止编译流程防止后续崩溃</li>
* </ul>
* <p>
* 推荐使用方式
* <pre>
* SemanticAnalyzerRunner.runSemanticAnalysis(ast, true);
* </pre>
* <p>
* 该类是实现 SCompiler 所有错误一次性输出且错误即终止 语义分析约束的关键
*/
public class SemanticAnalyzerRunner {
/**
* 对输入的语法树执行语义分析
* <p>
* 语法树应为编译前阶段如解析器产出的 AST 列表
* 本方法会自动筛选其中的 {@link ModuleNode} 节点并调用语义分析器执行完整分析
* 对输入的语法树执行语义分析并自动报告
*
* @param ast 根节点列表应包含一个或多个 {@link ModuleNode}
* @param verbose 是否启用详细日志将控制内部 {@link Context#log(String)} 的行为
*/
public static void runSemanticAnalysis(List<Node> ast, boolean verbose) {
// 1. 提取模块节点
// 1. AST 列表中过滤所有模块节点 ModuleNode
List<ModuleNode> modules = ast.stream()
.filter(ModuleNode.class::isInstance)
.map(ModuleNode.class::cast)
.collect(Collectors.toList());
.filter(ModuleNode.class::isInstance) // 保留类型为 ModuleNode 的节点
.map(ModuleNode.class::cast) // 转换为 ModuleNode
.collect(Collectors.toList()); // 收集为 List<ModuleNode>
// 2. 执行语义分析
// 2. 调用语义分析器对所有模块进行全流程语义分析返回错误列表
List<SemanticError> errors = new SemanticAnalyzer(verbose).analyze(modules);
// 3. 报告并在必要时终止
// 3. 统一报告全部语义错误如有错误则自动终止编译System.exit
SemanticAnalysisReporter.reportAndExitIfNecessary(errors);
}
}

View File

@ -1,44 +1,32 @@
package org.jcnc.snow.compiler.semantic.error;
import org.jcnc.snow.compiler.parser.ast.base.Node;
/**
* {@code SemanticError} 表示语义分析过程中发现的错误信息
* <p>
* 语义错误是编译器无法接受的程序逻辑问题例如
* 表示一次语义错误<br/>
* <ul>
* <li>使用了未声明的变量</li>
* <li>类型不兼容的赋值</li>
* <li>函数返回类型与实际返回值不一致</li>
* <li>调用了不存在的函数或模块</li>
* <li>记录对应 {@link Node} 及出错信息</li>
* <li>重写 {@link #toString()} <code> X, Y: message</code> 格式输出</li>
* <li>避免默认的 <code>Node@hash</code> 形式</li>
* </ul>
* <p>
* 访问器方法{@code node()} / {@code message()}相等性判断等功能
*
* <p>主要字段说明
* <ul>
* <li>{@code node}发生语义错误的 AST 节点可用于定位源代码位置</li>
* <li>{@code message}具体的错误描述适合用于报错提示日志输出IDE 集成等</li>
* </ul>
*
* @param node 发生错误的抽象语法树节点 {@link Node}
* @param message 错误描述信息
*/
public record SemanticError(Node node, String message) {
/**
* 返回格式化后的语义错误信息字符串
* <p>
* 输出格式
* <pre>
* Semantic error at [节点]: [错误信息]
* </pre>
* 适用于命令行编译器输出调试日志或错误收集器展示
*
* @return 格式化的错误信息字符串
*/
@Override
public String toString() {
return "Semantic error at " + node + ": " + message;
// Node 假定提供 line() / column() 方法如无则返回 -1
int line = -1;
int col = -1;
if (node != null) {
try {
line = (int) node.getClass().getMethod("line").invoke(node);
col = (int) node.getClass().getMethod("column").invoke(node);
} catch (ReflectiveOperationException ignored) {
// Node 未提供 line/column 方法则保持 -1
}
}
String pos = (line >= 0 && col >= 0) ? ("" + line + ", 列 " + col) : "未知位置";
return pos + ": " + message;
}
}

View File

@ -5,51 +5,47 @@ import org.jcnc.snow.compiler.semantic.error.SemanticError;
import java.util.List;
/**
* {@code SemanticAnalysisReporter} 是语义分析阶段的结果报告工具类
* <p>
* 用于统一处理语义分析阶段产生的错误信息输出与流程终止逻辑
* 通常作为语义分析器的收尾阶段调用
*
* <p>主要职责包括
* {@code SemanticAnalysisReporter} 用于在语义分析结束后汇总并打印所有收集到的
* {@link SemanticError}为了同时满足完整错误收集按需快速失败两种使用场景
* 现在提供两个公共 API
* <ul>
* <li>打印所有收集到的 {@link SemanticError}</li>
* <li>若存在错误使用 {@code System.exit(1)} 终止编译流程</li>
* <li>若无错误输出分析通过提示</li>
* <li>{@link #report(List)} 仅打印不终止</li>
* <li>{@link #reportAndExitIfNecessary(List)} 若存在错误则 <b>打印并退出</b></li>
* </ul>
*
* <p>该类为工具类禁止实例化方法均为静态调用
* 调用方可根据需求选择合适方法
*/
public final class SemanticAnalysisReporter {
// 禁止实例化
private SemanticAnalysisReporter() {
}
private SemanticAnalysisReporter() { }
/**
* 打印语义分析结果并在必要时终止程序
* <p>
* 如果错误列表非空
* <ul>
* <li>逐条打印错误信息含位置与描述</li>
* <li>使用 {@code System.exit(1)} 退出表示语义分析失败</li>
* </ul>
* 如果错误列表为空
* <ul>
* <li>打印语义分析通过提示</li>
* </ul>
* 打印语义分析结果<b>不会</b>退出进程
*
* @param errors 语义分析阶段收集到的错误列表允许为 null
* @param errors 语义分析阶段收集到的错误列表允许为 {@code null}
*/
public static void reportAndExitIfNecessary(List<SemanticError> errors) {
if (errors != null && !errors.isEmpty()) {
System.err.println("语义分析发现错误:");
for (SemanticError error : errors) {
System.err.println(" " + error);
}
System.exit(1); // 非正常退出阻止后续编译流程
public static void report(List<SemanticError> errors) {
if (hasErrors(errors)) {
System.err.println("语义分析发现 " + errors.size() + " 个错误:");
errors.forEach(err -> System.err.println(" " + err));
} else {
System.out.println("语义分析通过,没有发现错误。");
}
}
/**
* 打印语义分析结果如有错误立即以状态码 <code>1</code> 结束进程
* 适用于 CLI 工具需要立即中止后续编译阶段的场景
*
* @param errors 语义分析阶段收集到的错误列表允许为 {@code null}
*/
public static void reportAndExitIfNecessary(List<SemanticError> errors) {
report(errors);
if (hasErrors(errors)) {
System.exit(1);
}
}
private static boolean hasErrors(List<SemanticError> errors) {
return errors != null && !errors.isEmpty();
}
}