feat:LexerEngine 实现
This commit is contained in:
commit
ab7604f9d5
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
/.idea/.gitignore
|
||||
/.idea/encodings.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/vcs.xml
|
||||
/.idea/
|
||||
69
opcode
Normal file
69
opcode
Normal file
@ -0,0 +1,69 @@
|
||||
module: CommonTasks
|
||||
function: add_numbers
|
||||
parameter:
|
||||
declare num1: int
|
||||
declare num2: int
|
||||
return_type: int
|
||||
body:
|
||||
return num1 + num2
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
|
||||
module: MathUtils
|
||||
function: square_number
|
||||
parameter:
|
||||
declare number: int
|
||||
return_type: int
|
||||
body:
|
||||
return number * number
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
|
||||
module: StringUtils
|
||||
function: concatenate
|
||||
parameter:
|
||||
declare first_str: string
|
||||
declare second_str: string
|
||||
return_type: string
|
||||
body:
|
||||
return first_str + second_str
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
|
||||
module: MainModule
|
||||
import: CommonTasks, MathUtils, StringUtils, BuiltinUtils
|
||||
|
||||
function: main
|
||||
parameter:
|
||||
args: string
|
||||
return_type: void
|
||||
body:
|
||||
loop:
|
||||
initializer:
|
||||
declare counter: int = 5
|
||||
condition:
|
||||
counter > 0
|
||||
update:
|
||||
counter = counter - 1
|
||||
body:
|
||||
BuiltinUtils.print(counter)
|
||||
end body
|
||||
end loop
|
||||
|
||||
declare input_number: int = BuiltinUtils.to_int(args)
|
||||
if input_number == 0 then
|
||||
input_number = 999
|
||||
end if
|
||||
|
||||
declare sum_result: int = CommonTasks.add_numbers(input_number, 20)
|
||||
|
||||
declare squared_result: int = MathUtils.square_number(sum_result)
|
||||
|
||||
declare final_message: string = StringUtils.concatenate("Result:",BuiltinUtils.to_string(squared_result))
|
||||
BuiltinUtils.print(final_message)
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
17
pom.xml
Normal file
17
pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.jcnc.snow.compiler</groupId>
|
||||
<artifactId>SCompiler</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
354
readme.md
Normal file
354
readme.md
Normal file
@ -0,0 +1,354 @@
|
||||
# 编译器架构设计方案
|
||||
|
||||
## 编译器主要模块划分
|
||||
|
||||
编译器可以划分为多个相对独立的模块,每个模块各司其职,按照顺序将源代码逐步翻译为目标虚拟机指令集。主要模块包括:
|
||||
|
||||
### 词法分析器(Lexical Analyzer)
|
||||
|
||||
词法分析器负责从源码文本中识别最小的语法单元**单词符号**(Token)。它逐字符读取源代码,跳过无用的空白和注释,根据定义的词法规则将字符序列切分为有意义的Token序列。词法分析器的职责包括:
|
||||
|
||||
- **符号识别**:识别标识符、关键字、数字字面量、字符串字面量、运算符(如`+`、`-`、`=`等)、标点符号(如逗号、括号等)以及缩进或换行等控制符号(如果语法基于缩进)。
|
||||
- **过滤无关字符**:忽略空格、换行(在非显式有意义场景下)和注释内容,不生成相应Token。
|
||||
- **错误检测**:当出现无法识别的字符或不合法的序列时,报告词法错误(例如不支持的字符,或字符串未正确闭合等)。
|
||||
|
||||
词法分析的输出是按顺序排列的Token列表,提供给下游的语法分析器使用。
|
||||
|
||||
### 语法分析器(Syntax Parser)
|
||||
|
||||
语法分析器根据语言的语法规则,将Token序列解析为抽象语法树(AST,Abstract Syntax Tree)。语法分析器的职责包括:
|
||||
|
||||
- **语法结构识别**:按照语言文法,将Token序列组织成语法结构。例如识别表达式、语句、函数定义、条件块、循环块、模块块等结构。对于自定义DSL类似YAML的语法,如果采用缩进表示层级,语法分析器需要结合缩进Token来确定层次结构(类似Python语法的块级缩进)。
|
||||
- **AST构建**:创建对应的AST节点对象,构造出反映源代码层次结构的树形表示。每个节点表示源码中的一个构造(如二元表达式、赋值语句、函数声明等)。
|
||||
- **语法错误处理**:如果Token序列不符合语法规则(如遇到意外的Token或缺少某个符号),报告语法错误。语法分析器可以尝试**错误恢复**(如跳过一定Tokens到语句末尾)以继续解析后续部分,从而发现更多错误。
|
||||
|
||||
语法分析的输出是编译器的中间表示——AST,它以树形结构表示整个源代码的语法结构。
|
||||
|
||||
### 语义分析器(Semantic Analyzer)
|
||||
|
||||
语义分析器在AST的基础上检查程序的语义正确性,并进行必要的语义处理。其主要职责包括:
|
||||
|
||||
- **符号解析**:建立**符号表**(Symbol Table),登记各种标识符的信息,包括变量、函数、模块等。分模块、分作用域检查每个标识符的**声明和定义**:例如变量在使用前是否已声明,函数调用时对应函数是否存在等。对于支持模块的语言,语义分析需要处理跨模块引用,如`CommonTasks.add_numbers`应解析到`CommonTasks`模块下名为`add_numbers`的函数定义。
|
||||
- **类型检查**(如果语言是静态类型或计划支持类型检查):检查表达式、赋值、函数参数/返回值等类型匹配是否正确。例如将一个函数返回值赋给变量时,类型是否兼容;算术运算的操作数类型是否可用等。如果当前语言未强制类型,可暂跳过严格类型检查,但设计上保留接口以便未来加入类型系统。
|
||||
- **语义规则验证**:包括检查控制流语义(如`return`语句是否在函数内、循环控制语句如`break/continue`是否在循环内使用)、常量表达式求值(必要时进行常量折叠优化)等。
|
||||
- **注解AST**:在AST节点上附加语义信息,例如每个表达式节点注入类型信息,每个变量/函数节点链接到其符号表条目。为后续代码生成做好准备。
|
||||
|
||||
语义分析阶段如发现语义错误(未声明的变量、函数不存在、类型不匹配等),将报告错误并阻止后续正确的代码生成。语义分析完成后,输出可以是**注释丰富的AST**(带有类型和符号引用),同时维护好符号表,供后续阶段使用。
|
||||
|
||||
### 指令生成器(Code Generator)
|
||||
|
||||
指令生成器将经过检查的AST转化为目标虚拟机的指令序列(即字节码或中间代码)。它的主要职责包括:
|
||||
|
||||
- **代码选择**:为AST中每种结构生成等价的虚拟指令序列。例如表达式运算生成对应的算术或位运算指令,条件和循环结构生成相应的分支跳转指令,函数调用生成调用和返回指令等。
|
||||
- **资源分配**:为变量、常量、临时值分配虚拟机运行时所需的资源。例如给每个变量分配一个内存槽位或栈位置,函数则分配栈桢空间。指令生成阶段会利用符号表信息确定每个变量/函数地址或索引,用于生成`LOAD/STORE`等内存指令的操作数。
|
||||
- **指令序列输出**:以适当的格式输出虚拟机指令列表。可以选择直接构建一个指令对象列表(在内存中运行),或者生成汇编文本/二进制文件供自定义虚拟机加载执行。**例如**,遇到函数定义时,产生相应的函数入口标签和终止的返回指令;遇到函数调用时,按照调用约定生成参数入栈指令和调用指令;遇到算术表达式则按操作次序生成`LOAD`和算术计算指令等。
|
||||
- **优化(可选)**:在生成指令时做简单优化,如删除无效代码、常量折叠、简单的代数化简等(更复杂的优化可在将来引入独立优化模块,这里只考虑基础的直接翻译)。
|
||||
|
||||
指令生成器输出的结果是**虚拟指令列表**,它完成了从高级源语言到自定义虚拟机指令集的翻译。这些指令可以交由虚拟机解释执行,或者进一步封装成可加载的模块文件。
|
||||
|
||||
## 各模块的输入/输出格式
|
||||
|
||||
为了让各模块解耦并协同工作,需要定义清晰的接口,即各阶段输入和输出的数据格式:
|
||||
|
||||
- **词法分析器**:输入是源码的原始文本字符串;输出是**Token列表**(`List<Token>`)。每个Token包含类型、字面量值及位置信息。
|
||||
- **语法分析器**:输入是词法分析器生成的Token列表;输出是**抽象语法树(AST)**的根节点(表示整个源码的结构)。AST由各类AST节点按树形组织。
|
||||
- **语义分析器**:输入是初步的AST(以及初始的符号表);输出是**注解过的AST**和填充完整的**符号表**。如果有语义错误,则输出错误信息而不产生有效AST。注解AST指在节点上附加类型、作用域等语义信息。
|
||||
- **指令生成器**:输入是通过语义检查的AST(以及符号表);输出是**虚拟机指令列表**,可以是一个Java内的数据结构(如`List<Instruction>`)或者文本形式的指令序列。每条指令包含操作码和操作数,构成最终可执行的字节码序列。
|
||||
|
||||
(在设计上,也可以在语义分析和指令生成之间增加**中间代码表示(IR)**和**优化模块**的输入输出,但当前方案下暂未引入独立的IR阶段。)
|
||||
|
||||
## 数据结构与类设计建议
|
||||
|
||||
为了实现上述各模块,需要设计合理的数据结构和类。以下是主要的Java类和结构建议:
|
||||
|
||||
### Token类
|
||||
|
||||
**Token**表示词法分析得到的最小语法单元。可以设计一个`Token`类包含以下属性:
|
||||
|
||||
- `type`:Token类型(例如标识符、关键字、数字、字符串、运算符等,可用枚举表示)。
|
||||
- `lexeme`:Token在源代码中的字面文本。例如标识符的名字,数字的文本形式,符号本身等。
|
||||
- `value`:Token的值(可选,用于存储已解析的数值、字符串字面量的实际值等)。例如数字`"20.2"`解析成一个Number类型的值20.2。
|
||||
- `line`和`column`:位置信息,记录Token在源码中的行号和列号,用于错误报告和调试。
|
||||
|
||||
例如,可以定义枚举`TokenType`包括:IDENTIFIER、NUMBER、STRING、KEYWORD、OPERATOR、INDENT、DEDENT、NEWLINE等,根据DSL具体情况扩展。词法分析时,对每个识别出的符号创建一个Token实例,设置其类型和lexeme等。
|
||||
|
||||
### AST节点类层次
|
||||
|
||||
**抽象语法树(AST)**可以用面向对象的类层次来表示。设计时通常有一个AST节点的基类,以及按语法元素分类的子类。建议的类设计:
|
||||
|
||||
- 抽象基类 `ASTNode`:包含所有AST节点共有的属性和方法。例如:
|
||||
- `sourceLocation`:源代码位置(行列范围)便于错误提示。
|
||||
- `accept(ASTVisitor v)`:如果考虑使用访问者模式进行遍历,可以预留accept方法。
|
||||
- (如果需要类型系统)`type`:该节点表达式的计算类型。
|
||||
- **表达式类**:派生自ASTNode,用于表示表达式的节点。例如:
|
||||
- `LiteralNode` 字面量节点:包含字面量的值和类型(如数值、字符串等)。
|
||||
- `VariableNode` 变量节点:包含变量名,可能还有引用的符号表条目用于指代其声明。
|
||||
- `BinaryOpNode` 二元运算节点:包含运算符类型以及左、右子表达式AST节点。
|
||||
- `UnaryOpNode` 一元运算节点(如负号、逻辑非),包含运算符和子表达式。
|
||||
- `CallNode` 函数调用节点:包含被调用函数的标识(函数名或引用)和实参表达式列表。若DSL支持模块命名空间,可以在节点中区分函数所属模块或在语义分析时将其解析为函数Symbol引用。
|
||||
- **语句类**:派生自ASTNode,用于表示可执行的语句结构。例如:
|
||||
- `AssignNode` 赋值语句节点:包含左侧的目标(一般是VariableNode)和右侧的表达式(AST节点),表示形如`变量 = 表达式`的赋值操作。
|
||||
- `IfNode` 条件语句节点:包含条件表达式,以及then分支和else分支的语句块(可以是AST节点列表或一个BlockNode封装)。
|
||||
- `WhileNode` 或 `ForNode` 循环节点:包含循环条件表达式,以及循环体语句块节点。根据DSL具体语法选择命名,若DSL有特定循环关键字就对应命名(如`LoopNode`泛指循环)。
|
||||
- `ReturnNode` 返回语句节点:可能包含返回值表达式节点(如果函数有返回值),或无子节点表示空返回。
|
||||
- `FunctionCallStmtNode` 函数调用语句节点:用于表示独立的一条调用语句(当调用的返回值不被使用时也可视作一条语句),内部可复用`CallNode`来表示调用的结构。
|
||||
- `PrintNode` 打印语句节点:如果DSL支持`print`等输出功能,可以有专门节点,或者也可以视为特殊的函数调用节点。
|
||||
- `BlockNode`(可选)代码块节点:包含一系列顺序执行的子语句节点列表。对于缩进结构的DSL,可以用BlockNode表示缩进构成的语句序列,从而AST层次清晰地反映作用域。
|
||||
|
||||
- **模块与函数**:
|
||||
- `ModuleNode` 模块节点:表示一个模块/命名空间,包含模块名和该模块内定义的函数列表。方便组织多模块代码,以及支持模块间调用。
|
||||
- `FunctionNode` 函数定义节点:包含函数名、形参列表、返回类型(如果有)、函数体(通常是一个BlockNode)等属性。形参列表可为`List<ParamNode>`,其中ParamNode包含参数名及类型等信息。FunctionNode也可记录所在模块,方便区分同名函数。
|
||||
|
||||
AST类需要体现出源码的结构和层次。以上只是建议,可根据DSL具体特性调整或增加节点类型。例如,若DSL采用YAML风格缩进而无显式`{}`块,则BlockNode的生成可依据缩进Token产生。
|
||||
|
||||
### 符号表与Symbol类
|
||||
|
||||
**符号表(Symbol Table)**用于在语义分析阶段记录和查询标识符的语义信息。可以设计一个符号表类(或接口)以及符号类来封装信息:
|
||||
|
||||
- 符号表通常可以按**作用域**分层。典型做法是在每进入一个新作用域(如进入一个函数、进入一个代码块(如if/while内部))时,创建一个新的符号表或符号表的嵌套结构。符号查找时先查当前作用域,找不到再向上层作用域查找。这可以通过符号表类中保存对父级符号表的引用实现。
|
||||
- 每个符号表可以使用一个哈希表(如`Map<String, Symbol>`)存储名字到Symbol对象的映射,以便快速查找。
|
||||
|
||||
**Symbol类**则代表一个符号(标识符)的信息。可能有多种符号类型,例如变量符号、函数符号、模块符号等,可以用继承或字段区分:
|
||||
|
||||
- 公共字段:`name`(符号名称),`kind`(符号种类枚举:变量、函数、模块、参数等),`scope`(所属作用域或符号表引用)。
|
||||
- **变量符号**:包含变量名、类型、在运行时的存储分配信息(比如相对于栈帧起始的偏移量、所在寄存器或全局内存地址)。当代码生成时会用到偏移量或索引来定位变量的位置。
|
||||
- **函数符号**:包含函数名、返回类型、参数列表(参数也是符号,有名称和类型)、以及指向函数AST节点或其指令起始位置的引用。还可以包含该函数所属模块的信息。
|
||||
- **模块符号**:包含模块名以及模块内符号表(用于存储该模块下的函数和全局符号)。模块本身也可视为一种符号,便于在跨模块调用时通过模块符号找到其内部内容。
|
||||
|
||||
符号表在语义分析时填充:在遇到变量声明时将Symbol加入当前符号表;函数定义时将Symbol加入模块符号表;模块定义时在全局注册模块符号等等。在语义分析阶段进行标识符解析时,通过符号表来验证标识符是否存在以及获取附加信息(比如变量类型用于类型检查)。
|
||||
|
||||
### 虚拟机指令表示
|
||||
|
||||
最终生成的**虚拟机指令**可以用适当的数据结构表示,以方便Java代码输出和操作。例如:
|
||||
|
||||
- 定义一个指令类 `Instruction`,包含操作码和操作数字段:
|
||||
- `opcode`:操作码,可用枚举类型定义全部指令集指令,如ADD、SUB、AND、OR、LOAD、STORE、JMP、JMP_IF、CALL、RET、PUSH、POP等。
|
||||
- `operands`:操作数列表,根据指令不同存储整数、浮点数、字符串(用于符号引用)等。例如 `LOAD_CONST 20.2` 的操作数是一个浮点常量20.2;`LOAD_VAR x` 操作数可以是变量在符号表中的索引或内存地址;`CALL f, n` 可能有操作数表示被调函数标识符或地址,以及参数个数n。
|
||||
- 可以为不同指令定义子类,或者令Instruction包含所有可能字段(如 int operand1, double operand2 等)但只使用与该opcode相关的字段。
|
||||
- 另一种方式是简单使用字符串或枚举加参数直接拼装指令文本,例如 `String codeLine = "LOAD_VAR " + index;` 最终组合成可供虚拟机解析执行的文本。但使用类结构可以更安全、结构化地生成和优化指令序列。
|
||||
|
||||
此外,可以设计一个容器类如`Program`或`ModuleCode`来存放指令列表和相关信息。例如一个Program类包含:模块名、指令列表、常量池、调试符号信息等。这样可以方便地序列化、保存到文件或加载执行。
|
||||
|
||||
通过上述数据结构设计,编译器各阶段可以清晰地交付和操作相应的数据:词法分析产生Token对象,语法分析组装AST节点,语义分析利用Symbol对象检查语义,代码生成产生Instruction列表。这些类在Java中应尽量设计为**不可变或只在构造时修改**(如Token、AST节点),以保证编译过程的稳定性。
|
||||
|
||||
## 开发顺序建议
|
||||
|
||||
开发应遵循编译流程顺序,分阶段实现和测试每个模块,逐步集成。建议的实施顺序:
|
||||
|
||||
1. **实现词法分析器**:首先编写Lexer模块。根据语言定义列出需要识别的Token类型和模式,编写解析逻辑。可采用手写状态机或正则表达式匹配的方式逐字符扫描。实现完成后,通过简单源码片段测试:输入一段源代码字符串,输出Token列表,检查是否正确识别了标识符、关键字、数字等,忽略了空白和注释。例如测试单行赋值语句、函数头等,看Token序列是否符合预期。必要时设计单元测试用例,比如给定字符串`"x = 5"`,期望输出Tokens:IDENTIFIER("x"), OPERATOR("="), NUMBER(5)。如果DSL使用缩进语法,也需测试不同行和缩进输出正确的INDENT/DEDENT/NEWLINE Token序列。
|
||||
|
||||
2. **实现语法分析器**:在Lexer正确工作的前提下,编写Parser模块。可采用**递归下降**算法手写解析器(为每个语法规则编写一个方法),或其他解析算法(如LL(1)预测分析)。首先设计或参考文法规则,明确语法层次。例如:
|
||||
- 程序 = 模块列表
|
||||
- 模块 = `module 模块名:` 缩进的函数定义列表 (假设DSL用冒号+缩进表示从属关系)
|
||||
- 函数定义 = `function 函数名(参数列表):` 缩进的语句列表
|
||||
- 语句 = 赋值语句 | 条件语句 | 循环语句 | 返回语句 | 函数调用语句 | 打印语句 ...
|
||||
- 表达式 = 算术表达式 | 函数调用 | 字面量 | 变量 等
|
||||
根据文法编写相应的解析函数。例如parseExpression处理表达式优先级,parseStatement根据下一个Token判断语句类型并调用相应子解析。逐步实现各子模块,组装AST节点。
|
||||
|
||||
语法分析阶段需要经常测试:可以从简单的语句开始测试AST生成是否正确,然后逐步增加复杂结构。比如:
|
||||
- 测试表达式解析:输入如`a + b * 2`,检查生成的AST节点树形结构(BinaryOpNodes嵌套)符合期望的优先级。
|
||||
- 测试简单语句:如赋值`x = 1`是否构造出AssignNode,子节点正确。
|
||||
- 测试控制结构:如`if`和`while`语句的AST,包含条件和子块节点。
|
||||
- 测试函数:定义一个简单函数,解析后AST的FunctionNode有正确的参数列表和函数体子树。
|
||||
可以编写调试输出函数,将AST以缩进形式打印,便于人工校验结构。持续迭代,发现解析错误及时修正文法规则或解析实现。
|
||||
|
||||
3. **实现语义分析器**:在能够成功构造AST后,开发Semantic Analyzer。首先设计符号表数据结构,然后遍历AST进行检查:
|
||||
- **符号表建立**:比如先遍历所有模块和函数声明,将函数Symbol加入模块符号表(支持前向引用的语言需要先收集声明)。然后再遍历函数内部,将参数和局部变量声明加入对应符号表。
|
||||
- **语义检查**:第二轮或在同一遍历中,检查每个AST节点的语义:
|
||||
* 对每个变量引用节点(VariableNode),在符号表中查找其声明Symbol,不存在则报错;存在则可以在该节点上附加符号信息(方便代码生成)。
|
||||
* 对函数调用节点(CallNode),检查函数名解析:首先根据是否有模块前缀解析模块符号,再查找函数Symbol。校验参数个数和类型是否匹配函数签名。
|
||||
* 检查控制流:如ReturnNode是否在函数内,返回类型与函数声明的返回类型匹配;循环中的Break/Continue(如果支持)是否在循环体内。
|
||||
* 类型检查(如需要):表达式节点,根据子节点类型推导结果类型,检查赋值两边类型兼容等等。
|
||||
- **注解/标注**:将解析出的符号和类型信息记录回AST或Symbol表。例如每个VariableNode关联它所引用的Symbol,CallNode关联目标函数的Symbol,方便后续直接使用已经解析的地址或类型信息。
|
||||
测试语义分析需要构造各种场景:
|
||||
- 正确的代码应通过检查,符号表内容正确。
|
||||
- 刻意制作错误代码,如引用未声明变量、函数调用参数错误等,验证是否能捕获并输出适当错误信息(包含行号和错误原因)。
|
||||
对于通过的代码,可打印符号表或在AST节点附加的信息核对是否准确(如变量Symbol的偏移、类型,函数Symbol的参数列表等)。
|
||||
|
||||
4. **实现指令生成器**:语义检查正确后,实现将AST转换为虚拟机指令。可使用**AST遍历**(如递归或访问者模式)生成代码:
|
||||
- 考虑函数为最基本的编译单位,对每个函数的AST生成一段指令序列。可以在函数Symbol中记录起始指令的索引或地址。
|
||||
- **表达式代码生成**:采用后序遍历,对每个表达式节点生成指令:
|
||||
* 例如BinaryOpNode:先生成左子表达式的指令序列,把结果压入栈,再生成右子表达式指令,结果压栈,最后根据运算符opcode生成对应算术指令(如ADD)。如果是短路逻辑运算符,还需特殊处理跳转。
|
||||
* VariableNode(变量取值):根据符号表偏移,生成`LOAD_VAR offset`指令,将变量值压栈。
|
||||
* LiteralNode:生成`LOAD_CONST value`指令。
|
||||
* CallNode:先计算每个实参表达式(将值按顺序压栈),然后生成调用指令`CALL func`。调用指令的操作数可以是被调用函数在全局的地址/索引,或者符号名由虚拟机在运行时链接。调用完毕后,约定返回值会压入栈顶。
|
||||
- **语句代码生成**:依照语句类型生成:
|
||||
* AssignNode:对右侧表达式生成指令(结果在栈顶),然后根据左侧变量偏移生成`STORE_VAR offset`指令将值弹栈存入变量。
|
||||
* IfNode:生成条件表达式指令,接着是一条条件跳转指令(如`JMP_IF_FALSE`)跳过then块;then块语句生成;如果有else块,则再插入无条件跳转跳过else块的指令以及相应的标签处理;else块语句生成。需要适当设置跳转目标地址,在生成过程中可能需要回填(先生成占位跳转,再知道目标位置后回填)。
|
||||
* WhileNode:需要创建循环起始标签,生成条件表达式,接条件跳转指令跳出循环(跳转到循环后位置); 循环体指令; 最后插入跳转回循环开头的指令形成闭环; 回填跳出循环的目标位置到之前的条件跳转指令。
|
||||
* ReturnNode:如果有返回值表达式则先生成其代码(将值压栈),然后生成`RET`指令(操作数可能用于返回值或直接弹栈返回)。约定RET执行时虚拟机会将栈顶值作为返回值处理。
|
||||
* PrintNode(或调用printf等):生成参数表达式后,调用输出函数的指令(可视作特殊的函数调用或特定的I/O指令)。
|
||||
- **汇编/输出**:将生成的指令序列输出。在测试阶段,可以先输出人类可读的指令文本(模拟汇编形式),以验证逻辑。例如:
|
||||
```
|
||||
LOAD_VAR 0 ; 将局部变量0 (input_number) 压栈
|
||||
LOAD_CONST 20.2 ; 将常数20.2 压栈
|
||||
CALL CommonTasks.add_numbers, 2 ; 调用函数,加两个参数
|
||||
STORE_VAR 1 ; 弹栈将结果存入局部变量1 (sum_result)
|
||||
```
|
||||
通过编译简单函数并打印指令序列,人工核对指令逻辑是否正确,尤其关注跳转指令的目标是否正确计算,函数调用前后栈平衡是否正确等。如果有自己的虚拟机执行环境,进一步可以将生成的指令送入虚拟机运行,看输出结果是否符合源代码预期。
|
||||
|
||||
5. **集成和整体测试**:各模块分别验证后,整合完整的编译流程。从词法分析到代码生成编写一个总控程序,给定源码文件,依次调用lexer、parser、semantic、codegen,最终产出可执行指令。编写多个样例程序做集成测试:
|
||||
- 简单算术运算、条件、循环、小函数调用等,编译后在虚拟机上运行,核对运行结果。
|
||||
- 包含一些语法/语义错误的代码,看是否在编译期得到合理的错误输出。
|
||||
- 多模块调用的程序,验证跨模块函数调用的正确性。
|
||||
通过循序渐进的测试与调试,保证各阶段正确衔接。
|
||||
|
||||
这一开发顺序遵循由浅入深、分模块实现的原则,每完成一阶段都验证其输出,再进入下一阶段,从而快速定位和修复问题,确保最终构建出正确的编译器。
|
||||
|
||||
## 错误处理建议
|
||||
|
||||
良好的错误处理对编译器尤为重要,应提供清晰的错误信息且在可能情况下尽量恢复继续分析,以发现更多错误。以下是各阶段的错误处理建议:
|
||||
|
||||
- **词法分析错误**:如果Lexer遇到非法字符或不匹配任何已知Token规则的文本片段,应报告词法错误。可以抛出自定义异常`LexicalException`携带位置信息和错误原因,或者返回一个特殊的ERROR Token。由于词法错误往往会导致无法继续解析,该错误一般是不可恢复的(停止进一步编译)。对于可跳过的简单错误(如不支持的转义符),Lexer可选择跳过字符继续,但总体上应中止并报告,让用户修改源码。
|
||||
|
||||
- **语法分析错误**:当Parser检测到Token序列不符合语法期望(例如缺少右括号、遇到不合法的关键字顺序),应抛出`SyntaxException`或记录错误消息。错误信息应包括出现错误的源码位置和预期的语法构造。例如:“Syntax Error: 在第10行: 缺少右括号 ')', 在`func(a, b`处”。
|
||||
为了提高用户体验,Parser可以实现**简单的错误恢复**策略:
|
||||
- **Panic模式**:跳过一定范围的Token直到可能的语句边界(例如跳到下一行换行或分号后)再继续解析。这允许找到后续语句的错误,而不被前一处错误完全阻断。
|
||||
- **插入或删除**:在容错模式下,Parser可假设缺失某符号并继续(如遇到缺少的右括号,可以假装看到一个`)`),同时记录一条错误。
|
||||
基于实现复杂度考虑,初期实现可在报错后终止解析。但设计上应使Parser易于扩展错误恢复功能。所有语法错误都应汇总报告,错误信息应当语句清晰、指明问题和位置。
|
||||
|
||||
- **语义分析错误**:在AST检查过程中,如发现语义不一致,应记录**语义错误**并标记编译失败。典型语义错误及处理:
|
||||
- **未声明标识符**:如变量或函数使用前未声明。在VariableNode或CallNode分析时检测,报告错误:“Semantic Error: 未定义的变量x,位于第N行第M列” 或 “函数Foo未定义” 等。
|
||||
- **重复定义**:如同一作用域内变量重名,函数重复定义。应在插入符号表时检查,如果已存在同名符号则报告错误以及位置。
|
||||
- **类型不匹配**:如赋值语句左右类型不兼容,函数调用实参类型与形参类型不符等。报告应指出期望类型和实际类型。
|
||||
- **其他语义约束**:如`return`语句不在函数内使用,`break`不在循环中使用。这些情况在检测到时,也应报告错误和发生位置。
|
||||
语义错误通常可以在一次遍历中收集多个而不马上终止编译。例如可以将所有错误信息存入一个列表,语义分析遍历结束后,如果列表非空,则一起输出所有错误。但是,对于某些致命错误(比如后续分析依赖的信息缺失),也可以选择中止。
|
||||
错误消息应避免晦涩技术术语,尽量友好说明问题。例如:“类型错误:尝试将字符串赋值给数值变量 'count' (第20行)”等。
|
||||
|
||||
- **错误报告统一与模型**:建议定义一个通用的错误表示结构,例如`CompileError`类,包含错误类型(词法/语法/语义)、错误消息、源代码位置等。编译过程中捕获到的错误都转换为CompileError对象并收集。在编译结束时,若有错误列表,则按照出现顺序输出所有错误信息,并终止代码生成。
|
||||
在IDE或命令行环境中,错误输出格式可以统一为“文件名:行:列: 错误类型: 描述”。这种模型便于将来拓展(如引入更多错误类型、警告类型等)。
|
||||
|
||||
总之,错误处理应贯彻“**早发现、早报告**”和“**清晰定位**”的原则。前端检测(词法/语法/语义)的错误比运行期错误更容易理解,因此编译器应尽量完善前端检查,让用户在编译阶段就修复问题。设计良好的错误处理模块能显著提升编译器的可用性和健壮性。
|
||||
|
||||
## 可扩展性建议
|
||||
|
||||
在编译器架构设计中,应考虑未来可能的功能扩展,例如优化器、调试支持、类型检查的加强等。通过模块化和面向对象设计,可以使编译器易于扩充而不需推倒重来。以下是几个方面的扩展建议:
|
||||
|
||||
### 优化扩展
|
||||
|
||||
目前的编译流程直接从AST生成虚拟机指令。为了将来支持代码优化,可以在架构中预留**中间表示(IR)**或优化阶段:
|
||||
- **中间表示**:可在语义分析之后,引入一个中间代码生成阶段,将AST转换为一种更低级但易于优化的表示形式(例如三地址码、控制流图、四元组等)。然后在此IR上进行优化处理,如常量折叠、死代码消除、公共子表达式消除等,再将优化后的IR翻译为最终指令。由于当前直接生成虚拟机指令也能工作,中间表示阶段可以以后按需添加。确保AST结构设计和代码生成模块松耦合,有助于将来插入IR阶段而影响最小。
|
||||
- **模块化优化**:如果暂不采用中间表示,也可以在现有AST基础上进行优化。设计Visitor模式方便新增遍历。例如可以新增`OptimizationVisitor`遍历AST,对某些模式进行变换(如乘2替换为左移1位指令等)。通过访问者,不需修改原有Parser或AST结构即可插入优化流程。
|
||||
- **可配置开关**:架构上可以让优化阶段可选启用,如提供编译选项控制是否运行优化。当扩展优化功能后,不影响原有正常编译,只在需要时插入额外步骤。
|
||||
|
||||
总的来说,通过引入清晰的中间层和访问者模式,优化器可以作为一个插件式模块加入,提高代码运行效率。
|
||||
|
||||
### 调试支持扩展
|
||||
|
||||
为了支持将来的调试和分析,需要在编译时生成必要的调试符号信息,并设计编译器输出使之便于调试:
|
||||
- **调试符号信息**:在代码生成阶段,可以收集源代码与指令的对应关系。例如维护一个映射,记录每条虚拟机指令对应的源代码行号、所在函数。当运行在调试模式的虚拟机遇到断点或异常时,可以通过该映射还原源码位置。这要求编译器在输出指令时附带这些元数据(可以存在一个专门的调试信息区段或者一张表)。
|
||||
- **变量和符号信息**:为了在调试时查看变量值,编译器应保留变量的符号表或名称到运行时存储位置的映射。可以在输出中包含局部变量表结构,使调试器在检查内存或栈时知道哪个槽位对应哪个变量名。同样地,函数的符号也可保留用于更友好的堆栈调用信息。
|
||||
- **调试选项**:编译器设计中可以加入调试选项,比如`-g`开启调试符号生成。当此选项启用时,上述额外的符号/行号信息会打包进输出;未启用时则省略这些信息以优化体积和速度。
|
||||
- **友好的错误和日志**:在非运行时的调试,例如编译错误场景,编译器本身也可提供**日志**或**可视化**AST的功能,帮助调试编译器或DSL程序。例如在调试模式下输出详细的解析过程、符号表状态等。这些都可通过配置开关实现,不影响正常编译路径。
|
||||
|
||||
通过这些设计,将来可以实现单步执行、断点、查看变量等调试功能,使自定义语言的开发体验更接近成熟语言环境。
|
||||
|
||||
### 类型检查扩展
|
||||
|
||||
如果当前DSL类型系统简单(比如可能只有数值类型或弱类型),未来可能引入更丰富的**类型检查**,包括自定义数据类型、类型推导、函数重载等。为此,在现有设计中可以做好准备:
|
||||
|
||||
- **类型信息在AST和Symbol中的存储**:即使暂时只有基本类型,也可以在AST节点和Symbol类中加入类型字段。例如LiteralNode记载其类型(整型、浮点型等),Variable Symbol记录声明类型或推导类型。这样将来增加类型时,不需大改数据结构。
|
||||
- **类型检查模块化**:目前语义分析或代码生成可能对类型未严格要求,但将来可以把类型检查作为语义分析中的独立步骤。例如引入一个`TypeChecker`访问AST,根据上下文推导类型、检测不一致。通过将类型检查逻辑集中,未来扩展如增加类型转换规则、泛型等会更容易管理。
|
||||
- **错误和警告机制**:随着类型系统复杂化,可能出现类型相关的警告(例如隐式类型转换的精度丢失)等。因此错误报告机制应能区分严重错误和一般警告,允许编译器在遇到非致命问题时提示但继续编译。这可以通过扩展CompileError类加入级别属性实现。
|
||||
- **标准库类型支持**:未来或许增加更多内建类型(字符串、布尔、集合等)和相应操作。编译器符号表可设计为易于注册内建类型和函数,比如启动时预先载入一些核心库Symbol。如果有类型检查,需要让这些内建符号携带正确的类型签名。
|
||||
|
||||
通过以上扩展措施,编译器能够从当前的简单类型逐步演进到强类型系统,而不用推翻既有架构。
|
||||
|
||||
### 其它设计扩展考虑
|
||||
|
||||
- **模块和多文件编译**:如果将来需要支持多文件编译或模块单独编译,编译器架构应考虑**分步编译和链接**。例如可以实现每个模块单独编译成中间表示或字节码文件,然后通过链接器组合。这要求符号表设计支持全局符号解析、未定义符号留待链接解决等。当前实现可以简单假设所有源码一次性编译,未来可逐步转为更灵活的编译流程。
|
||||
- **后端移植**:当前后端目标是自定义虚拟机指令集。若未来考虑输出其他形式(例如直接生成Java字节码或机器码),可以在后端模块中采用接口/抽象工厂模式,使得不同的**CodeEmitter**实现对应不同目标。当增加新后端时,复用前端,只需实现新的指令选择和输出模块。这也是扩展性的一环。
|
||||
- **性能与并行**:对于大型源码,将来可考虑词法和语法分析的性能(比如引入多线程编译不同模块)。设计时保持模块边界清晰有助于并行化,例如每个模块AST可并行语义分析,再汇总符号表。虽然目前可能不需要,但良好的模块划分为将来性能优化留出空间。
|
||||
|
||||
综上,编译器架构在初始设计时就应秉承**高内聚、低耦合**原则,各阶段通过清晰的数据接口衔接。这样在需要添加新功能时,可以在原有框架基础上拓展新模块或替换某模块的实现,而不影响其他部分。例如,可以方便地插入一个优化器、启用调试信息生成、增强类型系统。这种可扩展性设计确保编译器能够随着用户需求的演进而演化,而无需推倒重写。
|
||||
|
||||
## 编译流程示例:代码片段完整编译过程
|
||||
|
||||
下面通过一个简单示例展示从源代码到虚拟机指令的完整编译过程。示例源代码片段为:
|
||||
|
||||
```plaintext
|
||||
sum_result = CommonTasks.add_numbers(input_number, 20.2)
|
||||
```
|
||||
|
||||
假定这行代码位于某个函数体内,作用是调用模块`CommonTasks`中的函数`add_numbers`,传入参数`input_number`和字面量`20.2`,将返回值赋给本地变量`sum_result`。编译过程各阶段处理如下:
|
||||
|
||||
1. **源代码**:编译器读取到这一行源代码字符串:
|
||||
```plaintext
|
||||
sum_result = CommonTasks.add_numbers(input_number, 20.2)
|
||||
```
|
||||
(在完整源码中,这可能缩进于一个函数定义下。)
|
||||
|
||||
2. **词法分析(Token序列)**:Lexer按规则将该行拆解成Token序列:
|
||||
- 标识符`sum_result` → Token(TYPE=IDENTIFIER, lexeme="sum_result")
|
||||
- 运算符`=` → Token(TYPE=OPERATOR, lexeme="=")
|
||||
- 标识符`CommonTasks` → Token(TYPE=IDENTIFIER, lexeme="CommonTasks")
|
||||
- 分隔符`.` → Token(TYPE=DOT, lexeme=".")
|
||||
- 标识符`add_numbers` → Token(TYPE=IDENTIFIER, lexeme="add_numbers")
|
||||
- 分隔符`(` → Token(TYPE=LPAREN, lexeme="(")
|
||||
- 标识符`input_number` → Token(TYPE=IDENTIFIER, lexeme="input_number")
|
||||
- 分隔符`,` → Token(TYPE=COMMA, lexeme=",")
|
||||
- 数字字面量`20.2` → Token(TYPE=NUMBER, lexeme="20.2", value=20.2)
|
||||
- 分隔符`)` → Token(TYPE=RPAREN, lexeme=")")
|
||||
- (行结束)换行或缩进减少 → Token(TYPE=NEWLINE, lexeme="\n") 等(具体取决于语法对换行的处理)
|
||||
|
||||
词法分析输出的Token列表按顺序传递给Parser。对于本行,无非法字符,词法分析顺利完成。
|
||||
|
||||
3. **语法分析(AST)**:Parser根据语法规则识别出这是一个**赋值语句**,带有一个函数调用表达式。构造出的AST(抽象语法树)结构可以表示为:
|
||||
|
||||
```plaintext
|
||||
AssignNode
|
||||
├── target: VariableNode("sum_result")
|
||||
└── value: CallNode
|
||||
├── func: "CommonTasks.add_numbers" (可能拆分为 Module="CommonTasks", FuncName="add_numbers")
|
||||
└── args:
|
||||
├── VariableNode("input_number")
|
||||
└── LiteralNode(20.2)
|
||||
```
|
||||
|
||||
解释:
|
||||
- 根节点是一个AssignNode,表示赋值操作。
|
||||
- AssignNode的`target`子节点是VariableNode("sum_result"),表示将值存入的目标变量。
|
||||
- AssignNode的`value`子树是一棵函数调用表达式。CallNode表示调用`CommonTasks.add_numbers`,它包含对被调用函数的引用以及参数列表。
|
||||
- 在CallNode下,`args`列表有两个元素:VariableNode("input_number")表示第一个实参引用了变量`input_number`,LiteralNode(20.2)表示第二个实参是20.2这个字面量常数。
|
||||
|
||||
AST很好地反映了语句的层次结构:赋值语句的右侧本身是一个复合的函数调用表达式。此阶段Parser已确保语法正确,例如括号配对、逗号分隔等都符合规则。如果有语法错误,此时Parser会报错而无法生成上述AST。
|
||||
|
||||
4. **语义分析**:Semantic Analyzer对AST进行符号和类型检查:
|
||||
- 符号表在进入当前函数时已包含其参数列表以及局部变量声明。我们假设符号表中已存在符号:`input_number`(函数参数),`sum_result`(事先声明的本地变量)。如果`sum_result`尚未声明,那么在此赋值前应报语义错误“未声明变量sum_result”。
|
||||
- 解析函数调用符号:`CommonTasks.add_numbers`应拆解为模块名和函数名。首先在全局/模块符号表中查找模块`CommonTasks`,如果找到,再在该模块的符号表中查找函数`add_numbers`。假设在其他源码中有对CommonTasks模块及其函数的声明,那么符号表能解析成功,得到函数符号(包含其参数类型列表、返回类型等)。
|
||||
- 检查函数参数匹配:假设`add_numbers`函数期望两个参数,第一个类型与`input_number`类型匹配(例如都是数值型),第二个参数应接受一个数值常量20.2(浮点型)。如果类型不符(比如`input_number`是整型但函数需要浮点型),可以在此报类型警告或错误。如果类型系统不严格,也可以允许自动转换。
|
||||
- 将符号信息注入AST:VariableNode("input_number")绑定到符号表中`input_number`的Symbol实例(包含其类型、偏移等);VariableNode("sum_result")绑定到`sum_result`的Symbol。CallNode可能存储解析到的函数Symbol引用(包括其在虚拟机中的入口地址或一个符号ID,参数类型签名等)。
|
||||
- 其他检查:确认`CommonTasks.add_numbers`函数是否有返回值。如果函数声明有返回,赋值接收返回是合理的;如果函数为void却被用在赋值,将报告语义错误。这里假设add_numbers有返回值。
|
||||
|
||||
经过语义分析,这行代码没有发现错误,AST节点被注解上了相关符号和类型信息,符号表也准备好供代码生成使用。例如,符号表可能提供:`input_number`在当前函数栈帧偏移0的位置,`sum_result`偏移1的位置,函数`CommonTasks.add_numbers`对应全局函数索引或入口地址等信息。
|
||||
|
||||
5. **指令生成**:Code Generator遍历AST生成目标虚拟机指令序列。按照前述AST:
|
||||
- 对AssignNode进行代码生成时,先处理右侧的`CallNode`(因为需要计算出值再赋给左侧)。
|
||||
- 生成函数调用代码:
|
||||
1. **参数1** (`VariableNode("input_number")): 生成加载变量指令,将`input_number`值压入栈。例如:
|
||||
`LOAD_VAR 0 ; 将偏移0处的变量 input_number 压入栈`
|
||||
(注:操作数0是`input_number`在当前作用域的偏移,由符号表提供。LOAD_VAR指令属于内存操作,将局部变量读取到计算栈)
|
||||
2. **参数2** (`LiteralNode(20.2)`): 生成加载常量指令,将20.2压入栈,例如:
|
||||
`LOAD_CONST 20.2 ; 将常数20.2 压入栈`
|
||||
(编译器可能在常量池维护20.2并引用之,这里为简化直接写值)
|
||||
3. **调用** (`CallNode`本身): 现在实参已按顺序在栈上,生成函数调用指令。比如:
|
||||
`CALL CommonTasks.add_numbers, 2 ; 调用CommonTasks模块的add_numbers函数,传入2个参数`
|
||||
该指令会跳转到指定函数执行,完成后将返回值压入栈顶。这里操作数标识了被调用函数,可以是一个引用索引、地址,或模块+函数名称组合,由虚拟机解析。参数个数2可能用于调用约定(虚拟机根据此清理参数等)。
|
||||
- 函数调用完成后,虚拟机栈顶留下了`add_numbers`的返回值。接下来生成AssignNode左侧赋值的代码:
|
||||
4. **赋值目标** (`VariableNode("sum_result")`): 赋值将把栈顶值存入目标变量。根据符号表,`sum_result`是当前函数偏移1的局部变量,于是生成:
|
||||
`STORE_VAR 1 ; 将栈顶值弹出并存入偏移1处的局部变量 sum_result`
|
||||
STORE_VAR指令弹出一个值存入指定位置,实现赋值操作。完成后栈顶返回值被清除且写入变量。
|
||||
|
||||
汇总以上步骤,最终的指令序列(伪代码形式)如下:
|
||||
```plaintext
|
||||
LOAD_VAR 0 ; 载入 input_number (参数1)
|
||||
LOAD_CONST 20.2 ; 载入常数20.2 (参数2)
|
||||
CALL CommonTasks.add_numbers, 2 ; 调用函数,计算 add_numbers(input_number, 20.2)
|
||||
STORE_VAR 1 ; 将返回值存入 sum_result
|
||||
```
|
||||
这段指令完成了源代码要求的功能:调用函数并获取结果到目标变量。若在函数末尾,还可能需要相应的`RET`指令,但这里仅示范单条语句的翻译。
|
||||
|
||||
6. **虚拟机执行**(编译输出的利用):编译器将上述指令输出到目标格式(内存或二进制文件)。之后用户的自定义虚拟机可以载入并解释执行这段指令。例如,在运行时当执行到`CALL CommonTasks.add_numbers`时,虚拟机跳转到事先编译好的`CommonTasks.add_numbers`函数指令序列执行,完成后将返回值压栈,再继续后续`STORE_VAR 1`。执行完毕后,虚拟机的栈帧中偏移1处(sum_result的位置)就存放了计算结果。由此可见,源代码中的意图已经通过编译后的指令得到实现。
|
||||
|
||||
这个示例展示了编译器如何分阶段处理源代码:词法分析获得Token流,语法分析形成AST,语义分析确保符号合法并注解信息,最后代码生成转化为低级指令。整个过程中,各模块各尽其职,将高级DSL逐步翻译为自定义虚拟机可以理解和执行的形式。
|
||||
|
||||
25
src/main/java/org/jcnc/snow/compiler/Main.java
Normal file
25
src/main/java/org/jcnc/snow/compiler/Main.java
Normal file
@ -0,0 +1,25 @@
|
||||
// File: org/jcnc/snow/compiler/Main.java
|
||||
package org.jcnc.snow.compiler;
|
||||
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerEngine;
|
||||
import org.jcnc.snow.compiler.lexer.utils.TokenPrinter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) throws IOException {
|
||||
// 如果命令行中提供了文件名,就读取该文件;否则默认读取当前目录下的 "opcode" 文件
|
||||
String filePath = args.length > 0 ? args[0] : "test";
|
||||
|
||||
String source = Files.readString(Path.of(filePath), StandardCharsets.UTF_8);
|
||||
// 词法分析
|
||||
LexerEngine lexerEngine = new LexerEngine(source);
|
||||
// 打印所有 Token
|
||||
TokenPrinter.printTokens(lexerEngine.getAllTokens());
|
||||
}
|
||||
|
||||
}
|
||||
121
src/main/java/org/jcnc/snow/compiler/lexer/LexerContext.java
Normal file
121
src/main/java/org/jcnc/snow/compiler/lexer/LexerContext.java
Normal file
@ -0,0 +1,121 @@
|
||||
package org.jcnc.snow.compiler.lexer;
|
||||
|
||||
/**
|
||||
* {@code LexerContext} 是词法分析中的状态管理器,负责追踪当前字符位置、
|
||||
* 行号、列号,并提供字符读取、匹配等功能。
|
||||
* <p>
|
||||
* 它是 {@link LexerEngine} 和 {@link org.jcnc.snow.compiler.lexer.TokenScanner}
|
||||
* 之间共享的上下文对象,用于读取字符并记录位置信息。
|
||||
*/
|
||||
public class LexerContext {
|
||||
/** 源代码文本,换行符统一替换为 \n */
|
||||
private final String source;
|
||||
|
||||
/** 当前在源代码中的偏移位置 */
|
||||
private int pos = 0;
|
||||
|
||||
/** 当前行号,从 1 开始计数 */
|
||||
private int line = 1;
|
||||
|
||||
/** 当前列号,从 1 开始计数 */
|
||||
private int col = 1;
|
||||
|
||||
/** 上一个字符的列号(用于生成准确的 Token 位置信息) */
|
||||
private int lastCol = 1;
|
||||
|
||||
/**
|
||||
* 创建一个新的 {@code LexerContext} 实例,并初始化源代码内容。
|
||||
* 所有 \r\n(Windows 换行)会被标准化为 \n。
|
||||
*
|
||||
* @param source 原始源代码字符串
|
||||
*/
|
||||
public LexerContext(String source) {
|
||||
this.source = source.replace("\r\n", "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否已到达源代码末尾。
|
||||
*
|
||||
* @return 如果读取完毕返回 true,否则返回 false
|
||||
*/
|
||||
public boolean isAtEnd() {
|
||||
return pos >= source.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取当前字符,并将指针移动到下一个字符位置。
|
||||
* 同时更新行号与列号。
|
||||
*
|
||||
* @return 当前字符,若已结束返回 '\0'
|
||||
*/
|
||||
public char advance() {
|
||||
if (isAtEnd()) return '\0';
|
||||
char c = source.charAt(pos++);
|
||||
lastCol = col;
|
||||
if (c == '\n') {
|
||||
line++;
|
||||
col = 1;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看当前字符,但不会前进指针。
|
||||
*
|
||||
* @return 当前字符,若已结束返回 '\0'
|
||||
*/
|
||||
public char peek() {
|
||||
return isAtEnd() ? '\0' : source.charAt(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看下一个字符,但不会前进指针。
|
||||
*
|
||||
* @return 下一个字符,若已结束返回 '\0'
|
||||
*/
|
||||
public char peekNext() {
|
||||
return pos + 1 >= source.length() ? '\0' : source.charAt(pos + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果当前字符是指定字符,则前进指针并返回 true。
|
||||
*
|
||||
* @param expected 要匹配的字符
|
||||
* @return 是否成功匹配并前进
|
||||
*/
|
||||
public boolean match(char expected) {
|
||||
if (isAtEnd() || source.charAt(pos) != expected) return false;
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前行号。
|
||||
*
|
||||
* @return 当前行,从 1 开始计数
|
||||
*/
|
||||
public int getLine() {
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前列号。
|
||||
*
|
||||
* @return 当前列,从 1 开始计数
|
||||
*/
|
||||
public int getCol() {
|
||||
return col;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一个字符所在的列号。
|
||||
* 主要用于生成换行符等 Token 的精确列号。
|
||||
*
|
||||
* @return 上一个字符的列号
|
||||
*/
|
||||
public int getLastCol() {
|
||||
return lastCol;
|
||||
}
|
||||
}
|
||||
84
src/main/java/org/jcnc/snow/compiler/lexer/LexerEngine.java
Normal file
84
src/main/java/org/jcnc/snow/compiler/lexer/LexerEngine.java
Normal file
@ -0,0 +1,84 @@
|
||||
package org.jcnc.snow.compiler.lexer;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.scanners.*;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 词法分析器(LexerEngine),用于将源代码字符串转换为一系列语法标记(Token)。
|
||||
* <p>
|
||||
* 它使用多个 {@link TokenScanner} 实现类处理不同类型的词法单元(数字、标识符、运算符、注释等),
|
||||
* 并追踪位置信息(行号、列号)以便后续语法分析或错误报告。
|
||||
*/
|
||||
public class LexerEngine {
|
||||
/** 所有已识别的 Token(含 EOF) */
|
||||
private final List<Token> tokens = new ArrayList<>();
|
||||
|
||||
/** 扫描上下文,记录当前扫描位置、行列号等 */
|
||||
private final LexerContext context;
|
||||
|
||||
/** Token 扫描器列表,按优先级顺序处理不同类型的 Token */
|
||||
private final List<TokenScanner> scanners;
|
||||
|
||||
/**
|
||||
* 创建并初始化 LexerEngine,构建扫描器列表并立即开始扫描源代码。
|
||||
*
|
||||
* @param source 输入的源代码字符串
|
||||
*/
|
||||
public LexerEngine(String source) {
|
||||
this.context = new LexerContext(source);
|
||||
|
||||
// 注册所有 Token 扫描器(按优先级顺序)
|
||||
this.scanners = List.of(
|
||||
new WhitespaceTokenScanner(), // 跳过空格、制表符等
|
||||
new NewlineTokenScanner(), // 处理换行符,生成 NEWLINE Token
|
||||
new CommentTokenScanner(), // 处理行注释与块注释
|
||||
new NumberTokenScanner(), // 处理数字字面量(整数、小数)
|
||||
new IdentifierTokenScanner(), // 处理标识符及关键字
|
||||
new StringTokenScanner(), // 处理字符串字面量
|
||||
new OperatorTokenScanner(), // 处理运算符(==、!=、&& 等)
|
||||
new SymbolTokenScanner(), // 处理符号(括号、逗号等)
|
||||
new UnknownTokenScanner() // 默认处理器,捕捉未知字符
|
||||
);
|
||||
|
||||
scanAllTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描整个源代码,识别并构建 Token 序列。
|
||||
* <p>
|
||||
* 每次迭代:
|
||||
* <ul>
|
||||
* <li>获取当前位置字符</li>
|
||||
* <li>尝试交由扫描器处理</li>
|
||||
* <li>若无扫描器可处理,则标记为 UNKNOWN</li>
|
||||
* </ul>
|
||||
* 最后追加 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokens.add(Token.eof(context.getLine()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回所有扫描到的 Token(包含 EOF)。
|
||||
*
|
||||
* @return Token 的不可变副本列表
|
||||
*/
|
||||
public List<Token> getAllTokens() {
|
||||
return List.copyOf(tokens);
|
||||
}
|
||||
}
|
||||
32
src/main/java/org/jcnc/snow/compiler/lexer/TokenScanner.java
Normal file
32
src/main/java/org/jcnc/snow/compiler/lexer/TokenScanner.java
Normal file
@ -0,0 +1,32 @@
|
||||
package org.jcnc.snow.compiler.lexer;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TokenScanner 接口:定义所有词法扫描器的通用规范。
|
||||
* 每种 Token 类型(如标识符、数字、字符串等)都应该实现此接口。
|
||||
*/
|
||||
public interface TokenScanner {
|
||||
|
||||
/**
|
||||
* 判断当前字符是否可以由该扫描器处理。
|
||||
* 词法分析器会遍历所有已注册的 TokenScanner,
|
||||
* 用此方法来找到合适的处理器。
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 词法分析上下文,提供字符流访问和辅助方法
|
||||
* @return 如果该扫描器能处理当前字符,则返回 true
|
||||
*/
|
||||
boolean canHandle(char c, LexerContext ctx);
|
||||
|
||||
/**
|
||||
* 处理当前字符起始的 token,并将生成的 Token 添加到列表中。
|
||||
* 实现中通常会读取多个字符,直到确定该 token 的结束。
|
||||
*
|
||||
* @param ctx 词法分析上下文,用于读取字符和获取位置信息
|
||||
* @param tokens Token 列表,扫描出的 token 应添加到该列表中
|
||||
*/
|
||||
void handle(LexerContext ctx, List<Token> tokens);
|
||||
}
|
||||
42
src/main/java/org/jcnc/snow/compiler/lexer/doc/README.md
Normal file
42
src/main/java/org/jcnc/snow/compiler/lexer/doc/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Snow Compiler - Lexer 模块
|
||||
|
||||
🎯 这是 `Snow Compiler` 项目的词法分析模块,负责将源代码解析为一系列结构化的 Token,为后续的语法分析(Parser)或解释器(Interpreter)做准备。
|
||||
|
||||
---
|
||||
|
||||
## 🧠 核心模块职责
|
||||
|
||||
| 模块 | 说明 |
|
||||
|----------------------|-----------------------------------------------------|
|
||||
| `LexerEngine` | 主引擎,接收字符流,分发给各个 `TokenScanner` 处理 |
|
||||
| `LexerContext` | 封装源码字符流和当前扫描状态(如 `peek()`、`advance()`) |
|
||||
| `TokenScanner` | 扫描器接口,所有扫描器需实现该接口进行识别与处理 |
|
||||
| `scanners/` | 各类扫描器实现 |
|
||||
| `token/` | Token 结构、类型定义、工厂方法 |
|
||||
| `utils/TokenPrinter` | Token 输出辅助类,格式化打印 Token 序列 |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 支持的 Token 类型
|
||||
|
||||
- 标识符(变量名、函数名)
|
||||
- 数字(整数、小数)
|
||||
- 字符串(支持转义)
|
||||
- 注释(单行、多行)
|
||||
- 运算符(==, !=, &&, ||, <, >= 等)
|
||||
- 符号(: , . + - * ( ))
|
||||
- 换行符
|
||||
- 空白符(自动跳过)
|
||||
- 未知字符(捕获非法或暂不支持的符号)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 示例:使用 LexerEngine 分析源码
|
||||
|
||||
```java
|
||||
String code = "let x = 42;\nprint(\"hello\")";
|
||||
LexerEngine lexer = new LexerEngine();
|
||||
List<Token> tokens = lexer.tokenize(code);
|
||||
|
||||
// 输出 token 流
|
||||
TokenPrinter.print(tokens);
|
||||
@ -0,0 +1,51 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.TokenScanner;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* 抽象 TokenScanner 实现,封装常用的扫描行为和模板方法。
|
||||
* 子类只需实现 {@code scanToken} 方法,关注 token 的核心逻辑,
|
||||
* 行列追踪和 token 添加交由此类统一处理。
|
||||
*/
|
||||
public abstract class AbstractTokenScanner implements TokenScanner {
|
||||
|
||||
@Override
|
||||
public void handle(LexerContext ctx, List<Token> tokens) {
|
||||
int line = ctx.getLine();
|
||||
int col = ctx.getCol();
|
||||
Token token = scanToken(ctx, line, col);
|
||||
if (token != null) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类实现的具体扫描逻辑。
|
||||
*
|
||||
* @param ctx 扫描上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 构建的 Token,如果为 null 表示无需添加
|
||||
*/
|
||||
protected abstract Token scanToken(LexerContext ctx, int line, int col);
|
||||
|
||||
/**
|
||||
* 连续读取字符直到不满足条件。
|
||||
*
|
||||
* @param ctx 扫描上下文
|
||||
* @param predicate 字符判断条件
|
||||
* @return 匹配的字符串
|
||||
*/
|
||||
protected String readWhile(LexerContext ctx, Predicate<Character> predicate) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (!ctx.isAtEnd() && predicate.test(ctx.peek())) {
|
||||
sb.append(ctx.advance());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 注释扫描器:处理源代码中的注释部分,包括:
|
||||
* <ul>
|
||||
* <li>单行注释(以 "//" 开头,直到行尾)</li>
|
||||
* <li>多行注释(以 "/*" 开头,以 "*/" 结尾)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 本扫描器会识别注释并生成 {@code TokenType.COMMENT} 类型的 Token,
|
||||
* 不会丢弃注释内容,而是将完整注释文本保留在 Token 中,便于后续分析(如文档提取、保留注释等场景)。
|
||||
* </p>
|
||||
*/
|
||||
public class CommentTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当当前位置字符为 '/' 且下一个字符为 '/' 或 '*' 时,表示可能是注释的起始。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果是注释的起始符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return c == '/' && (ctx.peekNext() == '/' || ctx.peekNext() == '*');
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现注释的扫描逻辑。
|
||||
* <p>支持两种注释格式:</p>
|
||||
* <ul>
|
||||
* <li><b>单行注释:</b> 以 "//" 开头,直到遇到换行符</li>
|
||||
* <li><b>多行注释:</b> 以 "/*" 开头,直到遇到 "*/" 结束</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号(用于 Token 位置信息)
|
||||
* @param col 当前列号(用于 Token 位置信息)
|
||||
* @return 包含完整注释内容的 COMMENT 类型 Token
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
// 消费第一个 '/' 字符
|
||||
ctx.advance();
|
||||
StringBuilder sb = new StringBuilder("/");
|
||||
|
||||
// 处理单行注释 //
|
||||
if (ctx.match('/')) {
|
||||
sb.append('/');
|
||||
while (!ctx.isAtEnd() && ctx.peek() != '\n') {
|
||||
sb.append(ctx.advance());
|
||||
}
|
||||
}
|
||||
// 处理多行注释 /* ... */
|
||||
else if (ctx.match('*')) {
|
||||
sb.append('*');
|
||||
while (!ctx.isAtEnd()) {
|
||||
char ch = ctx.advance();
|
||||
sb.append(ch);
|
||||
if (ch == '*' && ctx.peek() == '/') {
|
||||
sb.append(ctx.advance()); // 消费 '/'
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Token(TokenType.COMMENT, sb.toString(), line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenFactory;
|
||||
|
||||
/**
|
||||
* 标识符扫描器:处理标识符的识别,如变量名、函数名等。
|
||||
* <p>
|
||||
* 识别规则如下:
|
||||
* <ul>
|
||||
* <li>必须以字母或下划线(_)开头</li>
|
||||
* <li>后续字符可以是字母、数字或下划线</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 扫描完成后会调用 {@link TokenFactory} 自动判断是否为关键字,
|
||||
* 并返回对应类型的 {@link Token}。
|
||||
*/
|
||||
public class IdentifierTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>如果字符为字母或下划线,则认为是标识符的起始。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果是标识符起始字符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return Character.isLetter(c) || c == '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行标识符的扫描逻辑。
|
||||
* <p>连续读取满足标识符规则的字符序列,交由 {@code TokenFactory} 创建对应的 Token。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 标识符或关键字类型的 Token
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
String lexeme = readWhile(ctx, ch -> Character.isLetterOrDigit(ch) || ch == '_');
|
||||
return TokenFactory.create(lexeme, line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 换行符扫描器:将源代码中的换行符(\n)识别为 {@code NEWLINE} 类型的 Token。
|
||||
* <p>
|
||||
* 通常用于记录行的分界,辅助语法分析阶段进行行敏感的判断或保持结构清晰。
|
||||
*/
|
||||
public class NewlineTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当字符为换行符(\n)时返回 true。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果为换行符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return c == '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行换行符的扫描逻辑。
|
||||
* <p>读取一个换行符并生成对应的 {@code NEWLINE} 类型 Token。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 表示换行的 Token
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
ctx.advance();
|
||||
return new Token(TokenType.NEWLINE, "\n", line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 数字扫描器:识别整数与小数(仅支持十进制,不支持负号与科学计数法)。
|
||||
* <p>
|
||||
* 支持的格式示例:
|
||||
* <ul>
|
||||
* <li>整数:123、0、45678</li>
|
||||
* <li>小数:3.14、0.5、12.0</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 生成的 Token 类型为 {@code NUMBER_LITERAL}。
|
||||
*/
|
||||
public class NumberTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当字符为数字时,表示可能是数字字面量的起始。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果为数字字符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return Character.isDigit(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数字扫描逻辑。
|
||||
* <p>连续读取数字字符,允许出现一个小数点,用于识别整数或小数。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 表示数字字面量的 Token
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean hasDot = false;
|
||||
|
||||
while (!ctx.isAtEnd()) {
|
||||
char c = ctx.peek();
|
||||
if (c == '.' && !hasDot) {
|
||||
hasDot = true;
|
||||
sb.append(ctx.advance());
|
||||
} else if (Character.isDigit(c)) {
|
||||
sb.append(ctx.advance());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Token(TokenType.NUMBER_LITERAL, sb.toString(), line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 运算符扫描器:识别逻辑与比较运算符,包括单字符和双字符组合。
|
||||
* <p>
|
||||
* 支持的运算符包括:
|
||||
* <ul>
|
||||
* <li>赋值与比较:=、==、!=</li>
|
||||
* <li>关系运算符:>、>=、<、<=</li>
|
||||
* <li>逻辑运算符:&&、||</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 不符合上述组合的字符会返回 {@code UNKNOWN} 类型的 Token。
|
||||
*/
|
||||
public class OperatorTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>运算符扫描器关注的起始字符包括:=、!、<、>、|、&</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果是潜在的运算符起始字符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return "=!<>|&".indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描并识别运算符 Token。
|
||||
* <p>支持组合运算符判断,如 ==、!=、>= 等,
|
||||
* 若无法匹配组合形式则退回单字符形式。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 对应的运算符 Token,无法识别的运算符返回 {@code UNKNOWN}
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
char c = ctx.advance();
|
||||
String lexeme;
|
||||
TokenType type;
|
||||
|
||||
switch (c) {
|
||||
case '=':
|
||||
if (ctx.match('=')) {
|
||||
lexeme = "==";
|
||||
type = TokenType.DOUBLE_EQUALS;
|
||||
} else {
|
||||
lexeme = "=";
|
||||
type = TokenType.EQUALS;
|
||||
}
|
||||
break;
|
||||
case '!':
|
||||
if (ctx.match('=')) {
|
||||
lexeme = "!=";
|
||||
type = TokenType.NOT_EQUALS;
|
||||
} else {
|
||||
lexeme = "!";
|
||||
type = TokenType.UNKNOWN;
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
if (ctx.match('=')) {
|
||||
lexeme = ">=";
|
||||
type = TokenType.GREATER_EQUAL;
|
||||
} else {
|
||||
lexeme = ">";
|
||||
type = TokenType.GREATER_THAN;
|
||||
}
|
||||
break;
|
||||
case '<':
|
||||
if (ctx.match('=')) {
|
||||
lexeme = "<=";
|
||||
type = TokenType.LESS_EQUAL;
|
||||
} else {
|
||||
lexeme = "<";
|
||||
type = TokenType.LESS_THAN;
|
||||
}
|
||||
break;
|
||||
case '&':
|
||||
if (ctx.match('&')) {
|
||||
lexeme = "&&";
|
||||
type = TokenType.AND;
|
||||
} else {
|
||||
lexeme = "&";
|
||||
type = TokenType.UNKNOWN;
|
||||
}
|
||||
break;
|
||||
case '|':
|
||||
if (ctx.match('|')) {
|
||||
lexeme = "||";
|
||||
type = TokenType.OR;
|
||||
} else {
|
||||
lexeme = "|";
|
||||
type = TokenType.UNKNOWN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
lexeme = String.valueOf(c);
|
||||
type = TokenType.UNKNOWN;
|
||||
}
|
||||
|
||||
return new Token(type, lexeme, line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 字符串扫描器:处理双引号包裹的字符串字面量,支持基本的转义字符。
|
||||
* <p>
|
||||
* 支持格式示例:
|
||||
* <ul>
|
||||
* <li>"hello"</li>
|
||||
* <li>"line\\nbreak"</li>
|
||||
* <li>"escaped \\\" quote"</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 扫描器会保留原始字符串的形式(包含双引号和转义符),
|
||||
* 并生成 {@code STRING_LITERAL} 类型的 Token。
|
||||
*/
|
||||
public class StringTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当字符为双引号(")时,认为是字符串字面量的开始。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果为字符串起始符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return c == '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行字符串的扫描逻辑。
|
||||
* <p>从当前位置开始,读取直到匹配结束的双引号。
|
||||
* 支持转义字符(如 \"、\\n 等),不会中断字符串扫描。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 字符串字面量类型的 Token
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(ctx.advance()); // 起始双引号
|
||||
|
||||
while (!ctx.isAtEnd()) {
|
||||
char c = ctx.advance();
|
||||
sb.append(c);
|
||||
|
||||
if (c == '\\') {
|
||||
sb.append(ctx.advance()); // 添加转义字符后的实际字符
|
||||
} else if (c == '"') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Token(TokenType.STRING_LITERAL, sb.toString(), line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 符号扫描器:识别常见的单字符符号,如冒号、逗号、括号和算术符号。
|
||||
* <p>
|
||||
* 支持的符号包括:
|
||||
* <ul>
|
||||
* <li>标点符号:: , .</li>
|
||||
* <li>括号:( )</li>
|
||||
* <li>算术运算符:+ - *</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 生成的 Token 类型根据字符分别对应 {@link TokenType} 枚举中的定义。
|
||||
*/
|
||||
public class SymbolTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>本扫描器处理的符号包括 : , ( ) . + - *</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果是支持的符号字符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return ":,().+-*".indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行符号的扫描逻辑。
|
||||
* <p>根据字符匹配对应的 {@code TokenType} 类型,构造并返回 Token。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 表示符号的 Token
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
char c = ctx.advance();
|
||||
TokenType type = switch (c) {
|
||||
case ':' -> TokenType.COLON;
|
||||
case ',' -> TokenType.COMMA;
|
||||
case '.' -> TokenType.DOT;
|
||||
case '+' -> TokenType.PLUS;
|
||||
case '-' -> TokenType.MINUS;
|
||||
case '*' -> TokenType.MULTIPLY;
|
||||
case '(' -> TokenType.LPAREN;
|
||||
case ')' -> TokenType.RPAREN;
|
||||
default -> TokenType.UNKNOWN;
|
||||
};
|
||||
return new Token(type, String.valueOf(c), line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
/**
|
||||
* 未知符号扫描器:兜底处理无法被其他扫描器识别的字符。
|
||||
* <p>
|
||||
* 用于捕捉非法或未定义的符号序列,生成 {@code UNKNOWN} 类型的 Token。
|
||||
* <p>
|
||||
* 它会连续读取一段既不是字母、数字、空白符,也不属于常规符号(如 ;、{、"、:、,、(、)、.、+、-、*)的字符序列。
|
||||
*/
|
||||
public class UnknownTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 始终返回 true,作为所有扫描器中的兜底处理器。
|
||||
* <p>当没有其他扫描器能够处理当前字符时,使用本扫描器。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 总是返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描未知或非法的字符序列。
|
||||
* <p>跳过字母、数字、空白和已知符号,仅捕获无法识别的符号块。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return {@code UNKNOWN} 类型的 Token,包含无法识别的字符内容
|
||||
*/
|
||||
@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
|
||||
);
|
||||
|
||||
return new Token(TokenType.UNKNOWN, lexeme, line, col);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package org.jcnc.snow.compiler.lexer.scanners;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.LexerContext;
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
|
||||
/**
|
||||
* 空白符扫描器:跳过非换行的空白字符,不生成任何 Token。
|
||||
* <p>
|
||||
* 支持的空白字符包括空格、制表符(Tab)等,但不包括换行符(由 {@link NewlineTokenScanner} 处理)。
|
||||
* <p>
|
||||
* 此扫描器仅用于忽略多余的空白内容,保持 Token 流的紧凑性。
|
||||
*/
|
||||
public class WhitespaceTokenScanner extends AbstractTokenScanner {
|
||||
|
||||
/**
|
||||
* 判断是否可以处理当前位置的字符。
|
||||
* <p>当字符为空白符但不是换行符(\n)时返回 true。</p>
|
||||
*
|
||||
* @param c 当前字符
|
||||
* @param ctx 当前词法上下文
|
||||
* @return 如果为非换行的空白字符,则返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean canHandle(char c, LexerContext ctx) {
|
||||
return Character.isWhitespace(c) && c != '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过空白字符,不生成 Token。
|
||||
* <p>直接推进上下文位置,返回 null。</p>
|
||||
*
|
||||
* @param ctx 词法上下文
|
||||
* @param line 当前行号
|
||||
* @param col 当前列号
|
||||
* @return 始终返回 null(表示无 Token 产生)
|
||||
*/
|
||||
@Override
|
||||
protected Token scanToken(LexerContext ctx, int line, int col) {
|
||||
ctx.advance();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
125
src/main/java/org/jcnc/snow/compiler/lexer/token/Token.java
Normal file
125
src/main/java/org/jcnc/snow/compiler/lexer/token/Token.java
Normal file
@ -0,0 +1,125 @@
|
||||
package org.jcnc.snow.compiler.lexer.token;
|
||||
|
||||
/**
|
||||
* 表示词法分析过程中生成的一个 Token 实例,包含其类型、清洗后的词素、原始片段及在源文件中的位置信息。
|
||||
*/
|
||||
public class Token {
|
||||
/**
|
||||
* Token 的类型,如 KEYWORD、IDENTIFIER、TYPE 等。
|
||||
*/
|
||||
private final TokenType type;
|
||||
/**
|
||||
* 清洗后的词素内容,例如去掉引号的字符串正文或注释正文。
|
||||
*/
|
||||
private final String lexeme;
|
||||
/**
|
||||
* 源代码中对应的原始片段,可能包含引号或注释符号等。
|
||||
*/
|
||||
private final String raw;
|
||||
/**
|
||||
* Token 在源文件中的行号,从 1 开始计算。
|
||||
*/
|
||||
private final int line;
|
||||
/**
|
||||
* Token 在源文件行中的列号,从 1 开始计算。
|
||||
*/
|
||||
private final int col;
|
||||
|
||||
/**
|
||||
* 构造一个完整信息的 Token 实例。
|
||||
*
|
||||
* @param type Token 类型
|
||||
* @param lexeme 清洗后的词素内容
|
||||
* @param raw 源代码中的原始片段
|
||||
* @param line 所在源文件的行号,从 1 开始
|
||||
* @param col 所在源文件行的列号,从 1 开始
|
||||
*/
|
||||
public Token(TokenType type, String lexeme, String raw, int line, int col) {
|
||||
this.type = type;
|
||||
this.lexeme = lexeme;
|
||||
this.raw = raw;
|
||||
this.line = line;
|
||||
this.col = col;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个简单的 Token 实例,清洗后的词素与原始片段相同。
|
||||
* <p>适用于非注释、非字符串等无需区分 raw 与 lexeme 的场景。</p>
|
||||
*
|
||||
* @param type Token 类型
|
||||
* @param lexeme 词素内容(即原始片段)
|
||||
* @param line 所在源文件的行号,从 1 开始
|
||||
* @param col 所在源文件行的列号,从 1 开始
|
||||
*/
|
||||
public Token(TokenType type, String lexeme, int line, int col) {
|
||||
this(type, lexeme, lexeme, line, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表示文件结束(EOF)的 Token,列号固定为 1,词素与原始片段均为空字符串。
|
||||
*
|
||||
* @param line EOF Token 所在的行号,通常为文件末尾的下一行
|
||||
* @return 表示 EOF 的 Token 实例
|
||||
*/
|
||||
public static Token eof(int line) {
|
||||
return new Token(TokenType.EOF, "", "", line, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取此 Token 的类型。
|
||||
*
|
||||
* @return Token 类型枚举
|
||||
*/
|
||||
public TokenType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取清洗后的词素内容。
|
||||
*
|
||||
* @return 词素(lexeme)
|
||||
*/
|
||||
public String getLexeme() {
|
||||
return lexeme;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源代码中的原始片段。
|
||||
*
|
||||
* @return 原始片段(raw)
|
||||
*/
|
||||
public String getRaw() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Token 在源文件中的行号。
|
||||
*
|
||||
* @return 行号,从 1 开始
|
||||
*/
|
||||
public int getLine() {
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Token 在源文件行中的列号。
|
||||
*
|
||||
* @return 列号,从 1 开始
|
||||
*/
|
||||
public int getCol() {
|
||||
return col;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Token 的字符串表示,包含类型、词素、行号和列号,便于调试输出。
|
||||
*
|
||||
* @return 格式如 Token(type=TYPE, lexeme='value', line=1, col=5)
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"Token(type=%s, lexeme='%s', line=%d, col=%d)",
|
||||
type, lexeme, line, col
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package org.jcnc.snow.compiler.lexer.token;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* TokenFactory 用于根据原始的词法单元字符串(raw string),
|
||||
* 判断其类型(关键字、类型名、标识符等),并创建对应的 Token 实例。
|
||||
*/
|
||||
public class TokenFactory {
|
||||
|
||||
// 关键字集合:包含语言中所有保留关键字
|
||||
private static final Set<String> KEYWORDS = Set.of(
|
||||
"module", "function", "parameter", "return_type",
|
||||
"body", "end", "if", "then", "else", "loop",
|
||||
"declare", "return", "import",
|
||||
"initializer", "condition", "update"
|
||||
);
|
||||
|
||||
// 类型集合:包含语言中所有内置类型名称
|
||||
private static final Set<String> TYPES = Set.of(
|
||||
"int", "string", "float", "bool", "void", "double"
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建一个 Token 实例。
|
||||
* 根据原始字符串判断其类型,并结合位置信息生成 Token。
|
||||
*
|
||||
* @param raw 原始字符串(词法单元文本)
|
||||
* @param line 所在行号
|
||||
* @param col 所在列号
|
||||
* @return 对应类型的 Token 实例
|
||||
*/
|
||||
public static Token create(String raw, int line, int col) {
|
||||
TokenType type = determineTokenType(raw); // 判断类型
|
||||
return new Token(type, raw, line, col); // 创建 Token
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断原始字符串所对应的 Token 类型。
|
||||
* 优先级顺序:类型 -> 关键字 -> 标识符 -> 未知。
|
||||
*
|
||||
* @param raw 原始词法文本
|
||||
* @return 对应的 TokenType
|
||||
*/
|
||||
private static TokenType determineTokenType(String raw) {
|
||||
if (isType(raw)) return TokenType.TYPE;
|
||||
if (isKeyword(raw)) return TokenType.KEYWORD;
|
||||
if (isIdentifier(raw)) return TokenType.IDENTIFIER;
|
||||
return TokenType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为内置类型名称。
|
||||
*
|
||||
* @param raw 字符串
|
||||
* @return 如果是内置类型,则返回 true
|
||||
*/
|
||||
private static boolean isType(String raw) {
|
||||
return TYPES.contains(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为语言关键字。
|
||||
*
|
||||
* @param raw 字符串
|
||||
* @return 如果是关键字,则返回 true
|
||||
*/
|
||||
private static boolean isKeyword(String raw) {
|
||||
return KEYWORDS.contains(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为合法标识符。
|
||||
* 标识符必须以字母或下划线开头,其后可接字母、数字或下划线。
|
||||
*
|
||||
* @param raw 字符串
|
||||
* @return 如果是合法标识符,则返回 true
|
||||
*/
|
||||
private static boolean isIdentifier(String raw) {
|
||||
return raw.matches("[a-zA-Z_][a-zA-Z0-9_]*");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package org.jcnc.snow.compiler.lexer.token;
|
||||
|
||||
/**
|
||||
* 表示 Snow 编程语言中的所有词法单元(Token)类型。
|
||||
* <p>
|
||||
* 这些类型用于标识词法分析阶段识别出的各种语法构成要素。
|
||||
*/
|
||||
public enum TokenType {
|
||||
|
||||
/** 普通标识符,如变量名、函数名等 */
|
||||
IDENTIFIER,
|
||||
|
||||
/** 语言保留关键字(如 if、return、module 等) */
|
||||
KEYWORD,
|
||||
|
||||
/** 类型名(如 int、string、bool 等) */
|
||||
TYPE,
|
||||
|
||||
/** 字符串字面量(如 "hello") */
|
||||
STRING_LITERAL,
|
||||
|
||||
/** 数字字面量(整数或浮点数) */
|
||||
NUMBER_LITERAL,
|
||||
|
||||
/** 冒号 ":" */
|
||||
COLON,
|
||||
|
||||
/** 逗号 "," */
|
||||
COMMA,
|
||||
|
||||
/** 点号 "." */
|
||||
DOT,
|
||||
|
||||
/** 赋值符号 "=" */
|
||||
EQUALS,
|
||||
|
||||
/** 加号 "+" */
|
||||
PLUS,
|
||||
|
||||
/** 乘号 "+" */
|
||||
MULTIPLY,
|
||||
|
||||
/** 减号 "-" */
|
||||
MINUS,
|
||||
|
||||
/** 左括号 "(" */
|
||||
LPAREN,
|
||||
|
||||
/** 右括号 ")" */
|
||||
RPAREN,
|
||||
|
||||
/** 相等比较 "==" */
|
||||
DOUBLE_EQUALS,
|
||||
|
||||
/** 不等比较 "!=" */
|
||||
NOT_EQUALS,
|
||||
|
||||
/** 大于符号 ">" */
|
||||
GREATER_THAN,
|
||||
|
||||
/** 大于等于符号 ">=" */
|
||||
GREATER_EQUAL,
|
||||
|
||||
/** 小于符号 "<" */
|
||||
LESS_THAN,
|
||||
|
||||
/** 小于等于符号 "<=" */
|
||||
LESS_EQUAL,
|
||||
|
||||
/** 逻辑与符号 "&&" */
|
||||
AND,
|
||||
|
||||
/** 逻辑或符号 "||" */
|
||||
OR,
|
||||
|
||||
/** 换行符,表示逻辑上的新行 */
|
||||
NEWLINE,
|
||||
|
||||
/** 单行或多行注释 */
|
||||
COMMENT,
|
||||
|
||||
/** 文件结束符(End of File) */
|
||||
EOF,
|
||||
|
||||
/** 无法识别的非法或未知符号 */
|
||||
UNKNOWN,
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package org.jcnc.snow.compiler.lexer.utils;
|
||||
|
||||
import org.jcnc.snow.compiler.lexer.token.Token;
|
||||
import org.jcnc.snow.compiler.lexer.token.TokenType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TokenPrinter 用于以表格形式将词法分析生成的 Token 列表输出到控制台。
|
||||
* <p>
|
||||
* 输出包含每个 Token 的行号、列号、类型以及词素(lexeme),
|
||||
* 并对换行、制表符等特殊字符进行转义显示。
|
||||
* </p>
|
||||
*/
|
||||
public class TokenPrinter {
|
||||
|
||||
/**
|
||||
* 将给定的 Token 列表打印到标准输出(控制台)。
|
||||
* <p>
|
||||
* 输出格式:
|
||||
* <pre>
|
||||
* line col type lexeme
|
||||
* ----------------------------------------------------
|
||||
* 1 1 KEYWORD module
|
||||
* 1 7 IDENTIFIER MyModule
|
||||
* ...
|
||||
* </pre>
|
||||
* 并且对 lexeme 中的换行符、制表符、回车符等进行转义(\n、\t、\r),
|
||||
* 以便在表格中保持排版整齐。如果遇到类型为 {@link TokenType#NEWLINE} 的 Token,
|
||||
* 会在该行后额外插入一个空行以增强可读性。
|
||||
*
|
||||
* @param tokens 要打印的 Token 列表,不应为 null;列表中的每个元素
|
||||
* 都应包含有效的行号、列号、类型和词素信息
|
||||
*/
|
||||
public static void printTokens(List<Token> tokens) {
|
||||
// 打印表头:列名对齐,宽度分别为 6、6、16
|
||||
System.out.printf("%-6s %-6s %-16s %s%n", "line", "col", "type", "lexeme");
|
||||
System.out.println("----------------------------------------------------");
|
||||
|
||||
// 逐个 Token 输出对应信息
|
||||
for (Token token : tokens) {
|
||||
// 对 lexeme 中的特殊字符进行转义,避免表格错位
|
||||
String lexeme = token.getLexeme()
|
||||
.replace("\n", "\\n")
|
||||
.replace("\t", "\\t")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
// 按照固定格式输出:行号、列号、类型、词素
|
||||
System.out.printf("%-6d %-6d %-16s %s%n",
|
||||
token.getLine(),
|
||||
token.getCol(),
|
||||
token.getType(),
|
||||
lexeme
|
||||
);
|
||||
|
||||
// 如果当前 Token 是换行符类型,则额外打印一行空白行
|
||||
if (token.getType() == TokenType.NEWLINE) {
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
69
test
Normal file
69
test
Normal file
@ -0,0 +1,69 @@
|
||||
module: CommonTasks
|
||||
function: add_numbers
|
||||
parameter:
|
||||
declare num1: int
|
||||
declare num2: int
|
||||
return_type: int
|
||||
body:
|
||||
return num1 + num2
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
|
||||
module: MathUtils
|
||||
function: square_number
|
||||
parameter:
|
||||
declare number: int
|
||||
return_type: int
|
||||
body:
|
||||
return number * number
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
|
||||
module: StringUtils
|
||||
function: concatenate
|
||||
parameter:
|
||||
declare first_str: string
|
||||
declare second_str: string
|
||||
return_type: string
|
||||
body:
|
||||
return first_str + second_str
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
|
||||
module: MainModule
|
||||
import: CommonTasks, MathUtils, StringUtils, BuiltinUtils
|
||||
|
||||
function: main
|
||||
parameter:
|
||||
args: string
|
||||
return_type: void
|
||||
body:
|
||||
loop:
|
||||
initializer:
|
||||
declare counter: int = 5
|
||||
condition:
|
||||
counter > 0
|
||||
update:
|
||||
counter = counter - 1
|
||||
body:
|
||||
BuiltinUtils.print(counter)
|
||||
end body
|
||||
end loop
|
||||
|
||||
declare input_number: int = BuiltinUtils.to_int(args)
|
||||
if input_number == 0 then
|
||||
input_number = 999
|
||||
end if
|
||||
|
||||
declare sum_result: int = CommonTasks.add_numbers(input_number, 20)
|
||||
|
||||
declare squared_result: int = MathUtils.square_number(sum_result)
|
||||
|
||||
declare final_message: string = StringUtils.concatenate("Result:",BuiltinUtils.to_string(squared_result))
|
||||
BuiltinUtils.print(final_message)
|
||||
end body
|
||||
end function
|
||||
end module
|
||||
Loading…
x
Reference in New Issue
Block a user