Compare commits

...

16 Commits
master ... dev

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
d4d2b3b87c fix:改进语法分析器支持顶层语句 2025-05-16 15:52:58 +08:00
e023b576c1 fix:token修复 2025-05-16 13:12:33 +08:00
dc81131add 修改文档 2025-05-16 12:21:29 +08:00
93d9d87ad8 更新文档 2025-05-14 19:58:31 +08:00
ffd1aefa22 测试 2025-05-14 15:51:10 +08:00
6d3a2d3c5b 增加文档 2025-05-14 15:48:54 +08:00
17 changed files with 716 additions and 286 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. **持续测试和文档**:在每个阶段,都需要编写大量测试用例验证新特性的正确性,并完善语言文档供用户参考。例如,当添加数组后,要在文档中说明其用法和限制。保持文档与实现同步能避免用户困惑,也方便开源协作时吸引他人参与。

BIN
doc/SCompiler.pdf Normal file

Binary file not shown.

View File

@ -1,15 +1,19 @@
package org.jcnc.snow.compiler.cli;
import org.jcnc.snow.compiler.backend.*;
import org.jcnc.snow.compiler.backend.RegisterAllocator;
import org.jcnc.snow.compiler.backend.VMCodeGenerator;
import org.jcnc.snow.compiler.backend.VMProgramBuilder;
import org.jcnc.snow.compiler.ir.builder.IRProgramBuilder;
import org.jcnc.snow.compiler.ir.core.*;
import org.jcnc.snow.compiler.ir.core.IRFunction;
import org.jcnc.snow.compiler.ir.core.IRProgram;
import org.jcnc.snow.compiler.lexer.core.LexerEngine;
import org.jcnc.snow.compiler.lexer.token.Token;
import org.jcnc.snow.compiler.parser.ast.base.Node;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.core.ParserEngine;
import org.jcnc.snow.compiler.semantic.core.SemanticAnalyzerRunner;
import org.jcnc.snow.vm.engine.*;
import org.jcnc.snow.vm.engine.VMMode;
import org.jcnc.snow.vm.engine.VirtualMachineEngine;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -36,12 +40,12 @@ public class SnowCompiler {
String source = Files.readString(srcPath, StandardCharsets.UTF_8);
/* 1. 词法分析 */
LexerEngine lexer = new LexerEngine(source);
LexerEngine lexer = new LexerEngine(source, srcPath.toString());
List<Token> tokens = lexer.getAllTokens();
/* 2. 语法分析 */
ParserContext ctx = new ParserContext(tokens);
List<Node> ast = new ParserEngine(ctx).parse();
List<Node> ast = new ParserEngine(ctx).parse();
/* 3. 语义分析 */
SemanticAnalyzerRunner.runSemanticAnalysis(ast, false);

View File

@ -10,78 +10,102 @@ import java.util.List;
/**
* {@code LexerEngine} 是编译器前端的词法分析器核心实现
* <p>
* 负责将源代码字符串按顺序扫描并转换为一系列 {@link Token} 实例
* 负责将源代码字符串按顺序扫描并转换为一系列 {@link Token} 实例
* 每个 Token 表示语法上可识别的最小单位如标识符关键字常量运算符等
* <p>
* 分析流程通过注册多个 {@link TokenScanner} 扫描器实现类型识别
* 并由 {@link LexerContext} 提供字符流与位置信息支持
* 支持文件名传递遇到非法字符时会以文件名:::错误信息输出简洁诊断
* </p>
*/
public class LexerEngine {
/** 扫描生成的 Token 序列(含 EOF */
/**
* 扫描生成的 Token 序列包含文件结束符 EOF
*/
private final List<Token> tokens = new ArrayList<>();
/** 词法上下文,提供字符流读取与位置信息 */
/**
* 词法上下文提供字符流读取与位置信息
*/
private final LexerContext context;
/** Token 扫描器集合,按优先级顺序组织,用于识别不同类别的 Token */
/**
* Token 扫描器集合按优先级顺序组织用于识别不同类别的 Token
*/
private final List<TokenScanner> scanners;
/**
* 构造一个 {@code LexerEngine} 实例并初始化内部扫描器与上下文
* 调用构造函数时即开始词法扫描生成完整 Token 序列
* 构造词法分析器假定输入源自标准输入文件名默认为 <stdin>
*
* @param source 原始源代码文本
* @param source 源代码文本
*/
public LexerEngine(String source) {
this.context = new LexerContext(source);
this(source, "<stdin>");
}
// 按优先级注册所有支持的 Token 扫描器
/**
* 构造词法分析器并指定源文件名用于诊断信息
* 构造时立即进行全量扫描
*
* @param source 源代码文本
* @param sourceName 文件名或来源描述"main.snow"
*/
public LexerEngine(String source, String sourceName) {
this.context = new LexerContext(source);
this.scanners = List.of(
new WhitespaceTokenScanner(), // 跳过空格制表符等
new NewlineTokenScanner(), // 处理换行符生成 NEWLINE Token
new CommentTokenScanner(), // 处理单行/多行注释
new NumberTokenScanner(), // 识别整数与浮点数
new IdentifierTokenScanner(), // 识别标识符与关键字
new NumberTokenScanner(), // 识别整数与浮点数字面量
new IdentifierTokenScanner(), // 识别标识符关键字
new StringTokenScanner(), // 处理字符串常量
new OperatorTokenScanner(), // 处理运算符
new SymbolTokenScanner(), // 处理括号分号等符号
new UnknownTokenScanner() // 捕捉无法识别的字符
new OperatorTokenScanner(), // 识别运算符
new SymbolTokenScanner(), // 识别括号分号等符号
new UnknownTokenScanner() // 捕捉无法识别的字符最后兜底
);
scanAllTokens();
// 主扫描流程遇到非法字符立即输出错误并终止进程
try {
scanAllTokens();
} catch (LexicalException le) {
// 输出文件名::: 错误信息简洁明了
System.err.printf(
"%s:%d:%d: %s%n",
sourceName,
le.getLine(), // 获取出错行号
le.getColumn(), // 获取出错列号
le.getMessage() // 错误描述
);
System.exit(65); // 65 = EX_DATAERR标准数据错误退出码
}
}
/**
* 执行主扫描流程将整个源代码转换为 Token 序列
* <p>
* 每次扫描尝试依次使用各个 {@link TokenScanner}直到某一扫描器能够处理当前字符
* 若无匹配扫描器交由 {@code UnknownTokenScanner} 处理
* 扫描结束后自动附加一个 EOF文件结束Token
* </p>
* 主扫描循环将源代码转为 Token 序列
* 依次尝试每个扫描器直到找到可处理当前字符的扫描器为止
* 扫描到结尾后补充 EOF Token
*/
private void scanAllTokens() {
while (!context.isAtEnd()) {
char currentChar = context.peek();
// 依次查找能处理当前字符的扫描器
for (TokenScanner scanner : scanners) {
if (scanner.canHandle(currentChar, context)) {
scanner.handle(context, tokens);
break;
break; // 已处理跳到下一个字符
}
}
}
// 末尾补一个 EOF 标记
tokens.add(Token.eof(context.getLine()));
}
/**
* 返回词法分析生成的所有 Token EOF
* 获取全部 Token包含 EOF返回只读列表
*
* @return Token 的不可变副本列表
* @return 词法分析结果 Token 列表
*/
public List<Token> getAllTokens() {
return List.copyOf(tokens);
}
}
}

View File

@ -0,0 +1,54 @@
package org.jcnc.snow.compiler.lexer.core;
/**
* 词法异常LexicalException
* <p>
* {@link org.jcnc.snow.compiler.lexer.core.LexerEngine} 在扫描过程中遇到
* 非法或无法识别的字符序列时抛出该异常
* <ul>
* <li>异常消息仅包含一行简明错误信息包含行号与列号</li>
* <li>完全禁止 Java 堆栈信息输出使命令行输出保持整洁</li>
* </ul>
* <pre>
*
* main.s:2:19: Lexical error: Illegal character sequence '@' at 2:19
* </pre>
*/
public class LexicalException extends RuntimeException {
/** 错误发生的行号从1开始 */
private final int line;
/** 错误发生的列号从1开始 */
private final int column;
/**
* 构造词法异常
* @param reason 错误原因非法字符描述
* @param line 出错行号
* @param column 出错列号
*/
public LexicalException(String reason, int line, int column) {
// 构造出错消息并禁止异常堆栈打印
super(String.format("Lexical error: %s at %d:%d", reason, line, column),
null, false, false);
this.line = line;
this.column = column;
}
/**
* 屏蔽异常堆栈填充始终不打印堆栈信息
*/
@Override
public synchronized Throwable fillInStackTrace() { return this; }
/**
* 获取出错的行号
* @return 行号
*/
public int getLine() { return line; }
/**
* 获取出错的列号
* @return 列号
*/
public int getColumn() { return column; }
}

View File

@ -1,25 +1,28 @@
package org.jcnc.snow.compiler.lexer.scanners;
import org.jcnc.snow.compiler.lexer.core.LexerContext;
import org.jcnc.snow.compiler.lexer.core.LexicalException;
import org.jcnc.snow.compiler.lexer.token.Token;
import org.jcnc.snow.compiler.lexer.token.TokenType;
/**
* 未知符号扫描器兜底处理无法被其他扫描器识别的字符
* 未知 Token 扫描器UnknownTokenScanner
* <p>
* 用于捕捉非法或未定义的符号序列生成 {@code UNKNOWN} 类型的 Token
* 作为所有扫描器的兜底处理器当前字符若不被任何其他扫描器识别
* 由本类处理并抛出 {@link LexicalException}终止词法分析流程
* </p>
* <p>
* 它会连续读取一段既不是字母数字空白符也不属于常规符号 ;{"、:、,、(、)、.、+、-、*)的字符序列。
* 主要作用保证所有非法不可识别的字符@$等不会被静默跳过或误当作合法 Token
* 而是在词法阶段立刻定位并报错有助于尽早发现源代码问题
* </p>
*/
public class UnknownTokenScanner extends AbstractTokenScanner {
/**
* 始终返回 true作为所有扫描器中的兜底处理器
* <p>当没有其他扫描器能够处理当前字符时使用本扫描器</p>
*
* @param c 当前字符
* @param ctx 当前词法上下文
* @return 总是返回 true
* 判断是否可以处理当前字符
* 对于 UnknownTokenScanner始终返回 true兜底扫描器必须排在扫描器链末尾
* @param c 当前待处理字符
* @param ctx 词法上下文
* @return 是否处理该字符始终为 true
*/
@Override
public boolean canHandle(char c, LexerContext ctx) {
@ -27,23 +30,26 @@ public class UnknownTokenScanner extends AbstractTokenScanner {
}
/**
* 扫描未知或非法的字符序列
* <p>跳过字母数字空白和已知符号仅捕获无法识别的符号块</p>
*
* @param ctx 词法上下文
* @param line 当前行
* @param col 当前列号
* @return {@code UNKNOWN} 类型的 Token包含无法识别的字符内容
* 实际处理非法字符序列的方法
* 连续读取所有无法被其他扫描器识别的字符组成错误片段并抛出异常
* @param ctx 词法上下文
* @param line 错误发生行号
* @param col 错误发生列
* @return 不会返回Token始终抛异常
* @throws LexicalException 非法字符导致的词法错误
*/
@Override
protected Token scanToken(LexerContext ctx, int line, int col) {
String lexeme = readWhile(ctx, c ->
!Character.isLetterOrDigit(c) &&
!Character.isWhitespace(c) &&
c != ';' && c != '{' && c != '"' &&
":,().+-*".indexOf(c) < 0
// 读取一段非法字符既不是字母数字也不是常见符号
String lexeme = readWhile(ctx, ch ->
!Character.isLetterOrDigit(ch) &&
!Character.isWhitespace(ch) &&
":,().+-*{};\"".indexOf(ch) < 0
);
return new Token(TokenType.UNKNOWN, lexeme, line, col);
// 如果没读到任何字符则把当前字符单独作为非法片段
if (lexeme.isEmpty())
lexeme = String.valueOf(ctx.advance());
// 抛出词法异常并带上错误片段与具体位置
throw new LexicalException("Illegal character sequence '" + lexeme + "'", line, col);
}
}

View File

@ -115,11 +115,13 @@ public class TokenStream {
}
/**
* 判断是否尚未到达 EOF
* 判断是否已经到达 EOF
*
* @return 若当前位置 Token EOF则返回 true
* @return 若当前位置 Token EOF则返回 true否则 false
*/
public boolean isAtEnd() {
return peek().getType() != TokenType.EOF;
return peek().getType() == TokenType.EOF; // 修正逻辑
}
}

View File

@ -10,62 +10,59 @@ import org.jcnc.snow.compiler.parser.ast.base.Node;
import java.util.ArrayList;
import java.util.List;
/**
* {@code ParserEngine} 是语法分析阶段的主控引擎
* 负责驱动顶层语法结构如模块定义导入语句函数定义等的解析流程
* <p>
* 它通过不断读取 TokenStream 中的标记动态选择合适的解析器由工厂提供
* 并构建对应的抽象语法树AST节点
* 同时具备跳过空行处理非法标记等基本容错能力
* </p>
*/
public class ParserEngine {
/** 解析上下文,封装 TokenStream 与语法状态 */
private final ParserContext ctx;
/**
* 构造一个 {@code ParserEngine} 实例
*
* @param ctx 提供语法分析所需的上下文信息 Token
*/
public ParserEngine(ParserContext ctx) {
this.ctx = ctx;
}
/**
* 启动语法解析流程提取所有顶层 AST 节点
* <p>
* 该方法会循环调用 {@link TopLevelParserFactory} 选择合适解析器
* 并将解析结果存入返回列表直到遇到文件结束符EOF
* </p>
*
* @return 顶层 AST 节点列表如模块导入函数等
* @throws IllegalStateException 若遇到无法识别的顶层标记
*/
public List<Node> parse() {
List<Node> nodes = new ArrayList<>();
TokenStream ts = ctx.getTokens();
List<Node> nodes = new ArrayList<>();
List<String> errs = new ArrayList<>();
TokenStream ts = ctx.getTokens();
while (ts.isAtEnd()) {
// 跳过 NEWLINE 以增强容错性
while (!ts.isAtEnd()) { // 取反
// 跳过空行
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
continue;
}
// 当前词素作为解析关键字 module, import
String lex = ts.peek().getLexeme();
TopLevelParser parser = TopLevelParserFactory.get(ts.peek().getLexeme());
TopLevelParser parser = TopLevelParserFactory.get(lex);
if (parser == null) {
throw new IllegalStateException("意外的顶级标记: " + lex);
try {
nodes.add(parser.parse(ctx));
} catch (Exception ex) {
errs.add(ex.getMessage());
synchronize(ts); // 错误恢复
}
// 执行对应解析器
nodes.add(parser.parse(ctx));
}
if (!errs.isEmpty()) {
throw new IllegalStateException("解析过程中检测到 "
+ errs.size() + " 处错误:\n - "
+ String.join("\n - ", errs));
}
return nodes;
}
}
/** 错误同步:跳到下一行或下一个已注册顶层关键字 */
private void synchronize(TokenStream ts) {
while (!ts.isAtEnd()) {
if (ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
break;
}
if (TopLevelParserFactory.get(ts.peek().getLexeme()) != null) {
break;
}
ts.next();
}
// 连续空行全部吃掉
while (!ts.isAtEnd() && ts.peek().getType() == TokenType.NEWLINE) {
ts.next();
}
}
}

View File

@ -27,33 +27,37 @@ import java.util.Map;
*/
public class PrattExpressionParser implements ExpressionParser {
/** 前缀解析器注册表:按 Token 类型映射 */
/**
* 前缀解析器注册表 Token 类型映射
*/
private static final Map<String, PrefixParselet> prefixes = new HashMap<>();
/** 中缀解析器注册表:按运算符词素映射 */
private static final Map<String, InfixParselet> infixes = new HashMap<>();
/**
* 中缀解析器注册表按运算符词素映射
*/
private static final Map<String, InfixParselet> infixes = new HashMap<>();
static {
// 注册前缀解析器
prefixes.put(TokenType.NUMBER_LITERAL.name(), new NumberLiteralParselet());
prefixes.put(TokenType.IDENTIFIER.name(), new IdentifierParselet());
prefixes.put(TokenType.LPAREN.name(), new GroupingParselet());
prefixes.put(TokenType.IDENTIFIER.name(), new IdentifierParselet());
prefixes.put(TokenType.LPAREN.name(), new GroupingParselet());
prefixes.put(TokenType.STRING_LITERAL.name(), new StringLiteralParselet());
// 注册中缀解析器
infixes.put("+", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("-", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("*", new BinaryOperatorParselet(Precedence.PRODUCT, true));
infixes.put("/", new BinaryOperatorParselet(Precedence.PRODUCT, true));
infixes.put("%", new BinaryOperatorParselet(Precedence.PRODUCT, true));
infixes.put(">", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("<", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("+", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("-", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("*", new BinaryOperatorParselet(Precedence.PRODUCT, true));
infixes.put("/", new BinaryOperatorParselet(Precedence.PRODUCT, true));
infixes.put("%", new BinaryOperatorParselet(Precedence.PRODUCT, true));
infixes.put(">", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("<", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("==", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("!=", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put(">=", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("<=", new BinaryOperatorParselet(Precedence.SUM, true));
infixes.put("(", new CallParselet());
infixes.put(".", new MemberParselet());
infixes.put("(", new CallParselet());
infixes.put(".", new MemberParselet());
}
/**
@ -83,14 +87,13 @@ public class PrattExpressionParser implements ExpressionParser {
ExpressionNode left = prefix.parse(ctx, token);
while (ctx.getTokens().isAtEnd() && prec.ordinal() < nextPrecedence(ctx)) {
while (!ctx.getTokens().isAtEnd()
&& prec.ordinal() < nextPrecedence(ctx)) {
String lex = ctx.getTokens().peek().getLexeme();
InfixParselet infix = infixes.get(lex);
if (infix == null) break;
left = infix.parse(ctx, left);
}
return left;
}

View File

@ -1,42 +1,29 @@
package org.jcnc.snow.compiler.parser.factory;
import org.jcnc.snow.compiler.parser.base.TopLevelParser;
import org.jcnc.snow.compiler.parser.core.ParserEngine;
import org.jcnc.snow.compiler.parser.module.ModuleParser;
import org.jcnc.snow.compiler.parser.function.FunctionParser;
import org.jcnc.snow.compiler.parser.top.ScriptTopLevelParser;
import java.util.Map;
import java.util.HashMap;
/**
* {@code TopLevelParserFactory} 是一个顶层结构解析器工厂类
* 用于根据源文件起始关键字 {@code module}动态选择相应的 {@link TopLevelParser} 实现
*
* <p>
* 每种顶层结构如模块导入等应实现 {@link TopLevelParser} 接口并在本工厂中静态注册
* 从而在语法分析阶段由 {@link ParserEngine} 根据关键字调用对应的解析器
* </p>
*
* <p>
* 本类采用静态注册表机制在类加载时将支持的关键字与其解析器一一映射并缓存于内部 Map
* 若调用时传入的关键字未注册则返回 {@code null}调用方应自行处理此情况
* </p>
*/
public class TopLevelParserFactory {
/** 顶层关键字 -> 顶层结构解析器 的映射表 */
private static final Map<String, TopLevelParser> registry = new HashMap<>();
private static final TopLevelParser DEFAULT = new ScriptTopLevelParser(); // 默认解析器
static {
// 注册顶层结构解析器
registry.put("module", new ModuleParser());
// 顶层结构解析器
registry.put("module", new ModuleParser());
registry.put("function", new FunctionParser());
// 也可按需继续注册其它关键字
}
/**
* 根据给定关键字获取对应的顶层结构解析器
*
* @param keyword 顶层结构关键字例如 "module"
* @return 对应的 {@link TopLevelParser} 实例若关键字未注册则返回 {@code null}
* 根据关键字获取解析器若未注册回退到脚本语句解析
*/
public static TopLevelParser get(String keyword) {
return registry.get(keyword);
return registry.getOrDefault(keyword, DEFAULT);
}
}

View File

@ -0,0 +1,25 @@
package org.jcnc.snow.compiler.parser.top;
import org.jcnc.snow.compiler.parser.base.TopLevelParser;
import org.jcnc.snow.compiler.parser.ast.base.Node;
import org.jcnc.snow.compiler.parser.ast.base.StatementNode;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.factory.StatementParserFactory;
import org.jcnc.snow.compiler.parser.statement.StatementParser;
/**
* {@code ScriptTopLevelParser} 允许在无 module 包裹的情况下
* 直接解析单条顶层语句脚本模式
*
* 解析得到的 {@link StatementNode} 将在 IR 阶段被封装成 _start 函数
*/
public class ScriptTopLevelParser implements TopLevelParser {
@Override
public Node parse(ParserContext ctx) {
String first = ctx.getTokens().peek().getLexeme();
StatementParser sp = StatementParserFactory.get(first);
StatementNode stmt = sp.parse(ctx);
return stmt; // StatementNode 亦是 Node
}
}

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 分支语句若存在
for (var stmt : ifn.elseBranch()) {
var stAnalyzer = ctx.getRegistry().getStatementAnalyzer(stmt);
if (stAnalyzer != null) {
stAnalyzer.analyze(ctx, mi, fn, locals, stmt);
// 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) {
// 对当前语句执行语义分析作用域为 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,49 +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() { }
/**
* 打印语义分析结果并在必要时终止程序
* <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();
}
}

2
test
View File

@ -6,7 +6,7 @@ module: CommonTasks
body:
return CommonTasks.test(3,1)
return CommonTasks.test(3,1)
end body
end function