书接上回,在《 Hulo 语言架构:从源代码到目标代码的完整流程》一文中,我们介绍了Hulo编程语言的整体架构和编译流程。今天,让我们深入探讨编译流程中的第一个关键环节——解析器。
解析器可以说是源代码到目标语言最重要的基础,它负责将结构化的文本实例化为抽象语法树(AST),这个过程也被称之为编译前端。解析器通过词法分析器(Lexer)将源代码分解为标记流(Token Stream),再通过语法分析器(Parser)将标记流转换为抽象语法树,最终将人类可读的源代码转换为机器可处理的树形数据结构。这个树形结构保留了源代码的语法结构信息,为后续的语义分析、类型检查、优化和代码生成等编译后端阶段提供了必要的数据基础。
听起来好像云里雾里是吧,别急,接下来我们举一个简单的例子来说明:
假设我们现在有这样一段代码:print("Hello, World!")
Token (标记)
Token 是词法分析的最小单位,每个 Token 都包含类型和值信息。对于上面的代码,词法分析器会将其分解为以下 Token 序列:
| 类型 | 值 |
|---|---|
| IDENT | |
| LPAREN | ( |
| STRING | “Hello, World!” |
| RPAREN | ) |
- IDENT 是标识符(Identifier)的缩写,一般变量名、函数名、类名、类型这些都归为标识符。
- LPAREN 、RPAREN 分别是 Left 和 Right 与 Paren 单词组合,就是简单的左括号和右括号。
- STRING 则很显而易见,Hello, World 整体是一个字符串的字面量。
Ps. 字面量是一种很常见的说法,比如说 3.14 、10 、0644 这些数字就可以被成为 NUMBER 类型的字面量,而 true 和 false 则是 BOOL 类型的字面量。
也就是说,Token 的作用就是将结构化的语法每个部分进行细分,细分到不可再分为止。我们可以在看一个稍微复杂的例子:
class User {
name: str
age: bool
}
| 类型 | 值 | 说明 |
|---|---|---|
| CLASS | class | 类声明关键字 |
| IDENT | User | 类名标识符 |
| LBRACE | { | 左大括号,类体开始 |
| IDENT | name | 字段名标识符 |
| COLON | : | 类型声明分隔符 |
| IDENT | str | 类型名标识符 |
| IDENT | age | 字段名标识符 |
| COLON | : | 类型声明分隔符 |
| IDENT | bool | 类型名标识符 |
| RBRACE | } | 右大括号,类体结束 |
Lexer (词法分析器)
词法分析器负责将源代码字符串分解为 Token 流。它的工作过程如下:
- 字符扫描:从左到右逐个扫描源代码字符
- 模式匹配:根据预定义的规则识别不同类型的 Token
- Token 生成:为每个识别出的模式生成对应的 Token
例如,对于print("Hello, World!"):
- 扫描到
print→ 识别为标识符(IDENT) - 扫描到
(→ 识别为左括号(LPAREN) - 扫描到
"Hello, World!"→ 识别为字符串字面量(STRING) - 扫描到
)→ 识别为右括号(RPAREN)
经过词法分析器的处理,源代码被分解为 Token[] 数组,每个 Token 都包含了类型和值信息。
Parser (语法分析器)
语法分析器负责将 Token 流转换为抽象语法树(AST)。它根据语言的语法规则,将 Token 组织成有意义的语法结构。
对于print("Hello, World!"),语法分析器会构建如下 AST:
CallExpr
├── Fun: Ident("print")
└── Args: [StringLiteral("Hello, World!")]
这个 AST 表示:
- 这是一个函数调用表达式(CallExpr)
- 函数名是”print”
- 参数是一个字符串字面量”Hello, World!”
看到这里,是不是感觉有点熟悉了?在大部分现代化语言的标准库中,往往都包含着解析成该语言 AST 的库。例如:
- Golang:
go/ast– 提供 Go 语言的 AST 定义和解析功能 - TypeScript:
@typescript-eslint/parser– TypeScript 的官方解析器 - Python:
ast模块 – Python 标准库中的抽象语法树模块 - JavaScript:
@babel/parser– Babel 生态中的 JavaScript 解析器 - Rust:
syn库 – Rust 的语法解析库 - Java:
javac编译器内置的 AST 处理 - C#: Roslyn 编译器平台提供的语法树 API
这些库不仅为语言本身提供了强大的代码分析能力,也为开发者构建工具链、代码格式化、静态分析、代码生成等提供了基础支持。通过使用这些标准化的 AST 库,开发者可以更容易地实现代码转换、优化和工具开发。
回到分析器本身,我们已经完成了从源代码到结构化实例的转换,是的,编译前端就是在做这样的工作,将难以操作的字符串转换成一个个对象,例如 CallExpr 表达式对象、IfStmt 语句对象、ClassDecl 声明对象… 这些转换将代码变得可操作了起来,它不再是只能靠正则表达式或者字符串处理的语法。
在 AST 中,节点通常分为三大类:
-
Expr (Expression): 表达式节点,表示会产生值的代码片段。例如:
CallExpr: 函数调用表达式,如print("hello")BinaryExpr: 二元运算表达式,如a + bIdent: 标识符表达式,如变量名xStringLiteral: 字符串字面量,如"hello"
-
Stmt (Statement): 语句节点,表示执行动作的代码片段。例如:
IfStmt: if 条件语句,如if (x > 0) { ... }WhileStmt: while 循环语句,如while (i < 10) { ... }AssignStmt: 赋值语句,如x = 10ReturnStmt: 返回语句,如return result
-
Decl (Declaration): 声明节点,表示定义新实体的代码片段。例如:
ClassDecl: 类声明,如class User { ... }FuncDecl: 函数声明,如function add(a, b) { ... }VarDecl: 变量声明,如var x = 10
这种分类方式使得 AST 具有清晰的层次结构,便于后续的语义分析、类型检查和代码生成。
Ps. 当然这都是人为划定的,你也可以都把他们当成同样的节点也是可以的。不过,合理的分类能够帮助我们更好地理解代码结构,并为后续的编译阶段提供更清晰的语义信息。
Leave a Reply Cancel reply