commit ab7604f9d5c17dca93eeb31738782e04169633bf Author: Luke Date: Tue Apr 22 16:54:33 2025 +0800 feat:LexerEngine 实现 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71ff660 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/opcode b/opcode new file mode 100644 index 0000000..1c72dcd --- /dev/null +++ b/opcode @@ -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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d7ca88b --- /dev/null +++ b/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + org.jcnc.snow.compiler + SCompiler + 1.0-SNAPSHOT + + + 23 + 23 + UTF-8 + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5dff7f7 --- /dev/null +++ b/readme.md @@ -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列表;输出是**抽象语法树(AST)**的根节点(表示整个源码的结构)。AST由各类AST节点按树形组织。 +- **语义分析器**:输入是初步的AST(以及初始的符号表);输出是**注解过的AST**和填充完整的**符号表**。如果有语义错误,则输出错误信息而不产生有效AST。注解AST指在节点上附加类型、作用域等语义信息。 +- **指令生成器**:输入是通过语义检查的AST(以及符号表);输出是**虚拟机指令列表**,可以是一个Java内的数据结构(如`List`)或者文本形式的指令序列。每条指令包含操作码和操作数,构成最终可执行的字节码序列。 + +(在设计上,也可以在语义分析和指令生成之间增加**中间代码表示(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包含参数名及类型等信息。FunctionNode也可记录所在模块,方便区分同名函数。 + +AST类需要体现出源码的结构和层次。以上只是建议,可根据DSL具体特性调整或增加节点类型。例如,若DSL采用YAML风格缩进而无显式`{}`块,则BlockNode的生成可依据缩进Token产生。 + +### 符号表与Symbol类 + +**符号表(Symbol Table)**用于在语义分析阶段记录和查询标识符的语义信息。可以设计一个符号表类(或接口)以及符号类来封装信息: + +- 符号表通常可以按**作用域**分层。典型做法是在每进入一个新作用域(如进入一个函数、进入一个代码块(如if/while内部))时,创建一个新的符号表或符号表的嵌套结构。符号查找时先查当前作用域,找不到再向上层作用域查找。这可以通过符号表类中保存对父级符号表的引用实现。 +- 每个符号表可以使用一个哈希表(如`Map`)存储名字到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逐步翻译为自定义虚拟机可以理解和执行的形式。 + diff --git a/src/main/java/org/jcnc/snow/compiler/Main.java b/src/main/java/org/jcnc/snow/compiler/Main.java new file mode 100644 index 0000000..2faec3c --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/Main.java @@ -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()); + } + +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/LexerContext.java b/src/main/java/org/jcnc/snow/compiler/lexer/LexerContext.java new file mode 100644 index 0000000..2f3356b --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/LexerContext.java @@ -0,0 +1,121 @@ +package org.jcnc.snow.compiler.lexer; + +/** + * {@code LexerContext} 是词法分析中的状态管理器,负责追踪当前字符位置、 + * 行号、列号,并提供字符读取、匹配等功能。 + *

+ * 它是 {@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; + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/LexerEngine.java b/src/main/java/org/jcnc/snow/compiler/lexer/LexerEngine.java new file mode 100644 index 0000000..f39d564 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/LexerEngine.java @@ -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)。 + *

+ * 它使用多个 {@link TokenScanner} 实现类处理不同类型的词法单元(数字、标识符、运算符、注释等), + * 并追踪位置信息(行号、列号)以便后续语法分析或错误报告。 + */ +public class LexerEngine { + /** 所有已识别的 Token(含 EOF) */ + private final List tokens = new ArrayList<>(); + + /** 扫描上下文,记录当前扫描位置、行列号等 */ + private final LexerContext context; + + /** Token 扫描器列表,按优先级顺序处理不同类型的 Token */ + private final List 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 序列。 + *

+ * 每次迭代: + *

    + *
  • 获取当前位置字符
  • + *
  • 尝试交由扫描器处理
  • + *
  • 若无扫描器可处理,则标记为 UNKNOWN
  • + *
+ * 最后追加 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 getAllTokens() { + return List.copyOf(tokens); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/TokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/TokenScanner.java new file mode 100644 index 0000000..913afad --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/TokenScanner.java @@ -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 tokens); +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/doc/README.md b/src/main/java/org/jcnc/snow/compiler/lexer/doc/README.md new file mode 100644 index 0000000..0fa4261 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/doc/README.md @@ -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 tokens = lexer.tokenize(code); + +// 输出 token 流 +TokenPrinter.print(tokens); diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/AbstractTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/AbstractTokenScanner.java new file mode 100644 index 0000000..10c2c04 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/AbstractTokenScanner.java @@ -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 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 predicate) { + StringBuilder sb = new StringBuilder(); + while (!ctx.isAtEnd() && predicate.test(ctx.peek())) { + sb.append(ctx.advance()); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java new file mode 100644 index 0000000..5933313 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/CommentTokenScanner.java @@ -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; + +/** + * 注释扫描器:处理源代码中的注释部分,包括: + *
    + *
  • 单行注释(以 "//" 开头,直到行尾)
  • + *
  • 多行注释(以 "/*" 开头,以 "*/" 结尾)
  • + *
+ *

+ * 本扫描器会识别注释并生成 {@code TokenType.COMMENT} 类型的 Token, + * 不会丢弃注释内容,而是将完整注释文本保留在 Token 中,便于后续分析(如文档提取、保留注释等场景)。 + *

+ */ +public class CommentTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

当当前位置字符为 '/' 且下一个字符为 '/' 或 '*' 时,表示可能是注释的起始。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果是注释的起始符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return c == '/' && (ctx.peekNext() == '/' || ctx.peekNext() == '*'); + } + + /** + * 实现注释的扫描逻辑。 + *

支持两种注释格式:

+ *
    + *
  • 单行注释: 以 "//" 开头,直到遇到换行符
  • + *
  • 多行注释: 以 "/*" 开头,直到遇到 "*/" 结束
  • + *
+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java new file mode 100644 index 0000000..4eb119a --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/IdentifierTokenScanner.java @@ -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; + +/** + * 标识符扫描器:处理标识符的识别,如变量名、函数名等。 + *

+ * 识别规则如下: + *

    + *
  • 必须以字母或下划线(_)开头
  • + *
  • 后续字符可以是字母、数字或下划线
  • + *
+ *

+ * 扫描完成后会调用 {@link TokenFactory} 自动判断是否为关键字, + * 并返回对应类型的 {@link Token}。 + */ +public class IdentifierTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

如果字符为字母或下划线,则认为是标识符的起始。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果是标识符起始字符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return Character.isLetter(c) || c == '_'; + } + + /** + * 执行标识符的扫描逻辑。 + *

连续读取满足标识符规则的字符序列,交由 {@code TokenFactory} 创建对应的 Token。

+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java new file mode 100644 index 0000000..1481c9f --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NewlineTokenScanner.java @@ -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。 + *

+ * 通常用于记录行的分界,辅助语法分析阶段进行行敏感的判断或保持结构清晰。 + */ +public class NewlineTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

当字符为换行符(\n)时返回 true。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果为换行符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return c == '\n'; + } + + /** + * 执行换行符的扫描逻辑。 + *

读取一个换行符并生成对应的 {@code NEWLINE} 类型 Token。

+ * + * @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); + } +} \ No newline at end of file diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java new file mode 100644 index 0000000..17d9b6d --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/NumberTokenScanner.java @@ -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; + +/** + * 数字扫描器:识别整数与小数(仅支持十进制,不支持负号与科学计数法)。 + *

+ * 支持的格式示例: + *

    + *
  • 整数:123、0、45678
  • + *
  • 小数:3.14、0.5、12.0
  • + *
+ *

+ * 生成的 Token 类型为 {@code NUMBER_LITERAL}。 + */ +public class NumberTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

当字符为数字时,表示可能是数字字面量的起始。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果为数字字符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return Character.isDigit(c); + } + + /** + * 执行数字扫描逻辑。 + *

连续读取数字字符,允许出现一个小数点,用于识别整数或小数。

+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java new file mode 100644 index 0000000..75f08df --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/OperatorTokenScanner.java @@ -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; + +/** + * 运算符扫描器:识别逻辑与比较运算符,包括单字符和双字符组合。 + *

+ * 支持的运算符包括: + *

    + *
  • 赋值与比较:=、==、!=
  • + *
  • 关系运算符:>、>=、<、<=
  • + *
  • 逻辑运算符:&&、||
  • + *
+ *

+ * 不符合上述组合的字符会返回 {@code UNKNOWN} 类型的 Token。 + */ +public class OperatorTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

运算符扫描器关注的起始字符包括:=、!、<、>、|、&

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果是潜在的运算符起始字符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return "=!<>|&".indexOf(c) >= 0; + } + + /** + * 扫描并识别运算符 Token。 + *

支持组合运算符判断,如 ==、!=、>= 等, + * 若无法匹配组合形式则退回单字符形式。

+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java new file mode 100644 index 0000000..5a80e57 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/StringTokenScanner.java @@ -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; + +/** + * 字符串扫描器:处理双引号包裹的字符串字面量,支持基本的转义字符。 + *

+ * 支持格式示例: + *

    + *
  • "hello"
  • + *
  • "line\\nbreak"
  • + *
  • "escaped \\\" quote"
  • + *
+ *

+ * 扫描器会保留原始字符串的形式(包含双引号和转义符), + * 并生成 {@code STRING_LITERAL} 类型的 Token。 + */ +public class StringTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

当字符为双引号(")时,认为是字符串字面量的开始。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果为字符串起始符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return c == '"'; + } + + /** + * 执行字符串的扫描逻辑。 + *

从当前位置开始,读取直到匹配结束的双引号。 + * 支持转义字符(如 \"、\\n 等),不会中断字符串扫描。

+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/SymbolTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/SymbolTokenScanner.java new file mode 100644 index 0000000..27a0516 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/SymbolTokenScanner.java @@ -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; + +/** + * 符号扫描器:识别常见的单字符符号,如冒号、逗号、括号和算术符号。 + *

+ * 支持的符号包括: + *

    + *
  • 标点符号:: , .
  • + *
  • 括号:( )
  • + *
  • 算术运算符:+ - *
  • + *
+ *

+ * 生成的 Token 类型根据字符分别对应 {@link TokenType} 枚举中的定义。 + */ +public class SymbolTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

本扫描器处理的符号包括 : , ( ) . + - *

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果是支持的符号字符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return ":,().+-*".indexOf(c) >= 0; + } + + /** + * 执行符号的扫描逻辑。 + *

根据字符匹配对应的 {@code TokenType} 类型,构造并返回 Token。

+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java new file mode 100644 index 0000000..2f4e5cd --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/UnknownTokenScanner.java @@ -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; + +/** + * 未知符号扫描器:兜底处理无法被其他扫描器识别的字符。 + *

+ * 用于捕捉非法或未定义的符号序列,生成 {@code UNKNOWN} 类型的 Token。 + *

+ * 它会连续读取一段既不是字母、数字、空白符,也不属于常规符号(如 ;、{、"、:、,、(、)、.、+、-、*)的字符序列。 + */ +public class UnknownTokenScanner extends AbstractTokenScanner { + + /** + * 始终返回 true,作为所有扫描器中的兜底处理器。 + *

当没有其他扫描器能够处理当前字符时,使用本扫描器。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 总是返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return true; + } + + /** + * 扫描未知或非法的字符序列。 + *

跳过字母、数字、空白和已知符号,仅捕获无法识别的符号块。

+ * + * @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); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/scanners/WhitespaceTokenScanner.java b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/WhitespaceTokenScanner.java new file mode 100644 index 0000000..9b6e318 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/scanners/WhitespaceTokenScanner.java @@ -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。 + *

+ * 支持的空白字符包括空格、制表符(Tab)等,但不包括换行符(由 {@link NewlineTokenScanner} 处理)。 + *

+ * 此扫描器仅用于忽略多余的空白内容,保持 Token 流的紧凑性。 + */ +public class WhitespaceTokenScanner extends AbstractTokenScanner { + + /** + * 判断是否可以处理当前位置的字符。 + *

当字符为空白符但不是换行符(\n)时返回 true。

+ * + * @param c 当前字符 + * @param ctx 当前词法上下文 + * @return 如果为非换行的空白字符,则返回 true + */ + @Override + public boolean canHandle(char c, LexerContext ctx) { + return Character.isWhitespace(c) && c != '\n'; + } + + /** + * 跳过空白字符,不生成 Token。 + *

直接推进上下文位置,返回 null。

+ * + * @param ctx 词法上下文 + * @param line 当前行号 + * @param col 当前列号 + * @return 始终返回 null(表示无 Token 产生) + */ + @Override + protected Token scanToken(LexerContext ctx, int line, int col) { + ctx.advance(); + return null; + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/token/Token.java b/src/main/java/org/jcnc/snow/compiler/lexer/token/Token.java new file mode 100644 index 0000000..55deaa3 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/token/Token.java @@ -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 实例,清洗后的词素与原始片段相同。 + *

适用于非注释、非字符串等无需区分 raw 与 lexeme 的场景。

+ * + * @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 + ); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/token/TokenFactory.java b/src/main/java/org/jcnc/snow/compiler/lexer/token/TokenFactory.java new file mode 100644 index 0000000..3d3874f --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/token/TokenFactory.java @@ -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 KEYWORDS = Set.of( + "module", "function", "parameter", "return_type", + "body", "end", "if", "then", "else", "loop", + "declare", "return", "import", + "initializer", "condition", "update" + ); + + // 类型集合:包含语言中所有内置类型名称 + private static final Set 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_]*"); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/token/TokenType.java b/src/main/java/org/jcnc/snow/compiler/lexer/token/TokenType.java new file mode 100644 index 0000000..891561d --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/token/TokenType.java @@ -0,0 +1,89 @@ +package org.jcnc.snow.compiler.lexer.token; + +/** + * 表示 Snow 编程语言中的所有词法单元(Token)类型。 + *

+ * 这些类型用于标识词法分析阶段识别出的各种语法构成要素。 + */ +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, + + +} diff --git a/src/main/java/org/jcnc/snow/compiler/lexer/utils/TokenPrinter.java b/src/main/java/org/jcnc/snow/compiler/lexer/utils/TokenPrinter.java new file mode 100644 index 0000000..50e8aaa --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/lexer/utils/TokenPrinter.java @@ -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 列表输出到控制台。 + *

+ * 输出包含每个 Token 的行号、列号、类型以及词素(lexeme), + * 并对换行、制表符等特殊字符进行转义显示。 + *

+ */ +public class TokenPrinter { + + /** + * 将给定的 Token 列表打印到标准输出(控制台)。 + *

+ * 输出格式: + *

+     * line   col    type             lexeme
+     * ----------------------------------------------------
+     * 1      1      KEYWORD          module
+     * 1      7      IDENTIFIER       MyModule
+     * ...
+     * 
+ * 并且对 lexeme 中的换行符、制表符、回车符等进行转义(\n、\t、\r), + * 以便在表格中保持排版整齐。如果遇到类型为 {@link TokenType#NEWLINE} 的 Token, + * 会在该行后额外插入一个空行以增强可读性。 + * + * @param tokens 要打印的 Token 列表,不应为 null;列表中的每个元素 + * 都应包含有效的行号、列号、类型和词素信息 + */ + public static void printTokens(List 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(); + } + } + } + +} diff --git a/test b/test new file mode 100644 index 0000000..1c72dcd --- /dev/null +++ b/test @@ -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