Skip to content

Commit cfeeddc

Browse files
chore: new article written
1 parent 2e2066a commit cfeeddc

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
---
2+
title: "深入理解并实现基本的脚本语言解释器"
3+
author: "杨子凡"
4+
date: "Oct 26, 2025"
5+
description: "构建迷你脚本语言解释器指南"
6+
latex: true
7+
pdf: true
8+
---
9+
10+
## 从零开始,用几百行代码构建你的迷你「Python」或「JavaScript」
11+
12+
在编程世界中,我们经常使用像 Python 或 JavaScript 这样的脚本语言来执行动态任务。想象一下,当我们写下 `a = 10 + 2 * 3; print(a);` 这样一行简单的代码时,计算机是如何一步步理解并执行它的呢?这背后隐藏着解释器的神奇工作原理。本文将带你从零开始,深入探讨并亲手实现一个基本的脚本语言解释器。通过这个过程,你不仅能深化对编程语言内部机制的理解,还能掌握构建一个支持变量、算术运算、条件语句、循环和函数的动态类型语言的核心技能。我们称之为 `MiniScript`,并使用 Python 作为实现语言,因为其语法简洁,易于表达思想,但所涉及的原理是通用的。
13+
14+
15+
造一个解释器可能听起来像在重复发明轮子,但这个过程却能极大地提升我们对编程语言本质的认识。以一个简单的代码片段为例:`a = 10 + 2 * 3; print(a);`。这行代码涉及赋值、算术运算和输出,计算机需要将其从文本转化为实际行动。通过实现解释器,我们可以直观地看到代码是如何被解析和执行的,这就像学习一门新语言时,理解其语法规则一样重要。解释器无处不在,从 Python 到 JavaScript,许多流行语言都依赖解释执行。更重要的是,一个基础的解释器并不复杂,其核心逻辑可以用几百行代码实现,这让它成为学习编译器设计和语言实现的绝佳起点。本文的目标是构建一个支持变量、算术运算、比较运算、条件语句、循环语句和函数的动态类型语言 `MiniScript`。最终,你将获得一个可运行的迷你解释器,并能通过动手实践加深对关键概念的理解。
16+
17+
## 背景知识:解释器与编译器的区别
18+
19+
在深入实现之前,我们需要先理解解释器和编译器的核心区别。编译器将源代码一次性翻译成另一种语言,通常是机器码,生成独立的可执行文件。这就像将一本中文书完整翻译成英文出版,整个过程是离线的。而解释器则直接读取源代码,边解析边执行,类似于同声传译,一边听一边现场翻译。对于 `MiniScript`,我们将采用解释器的方式,其工作流程可以概括为:源代码首先经过词法分析,被拆分成令牌流;然后通过语法分析构建抽象语法树;最后解释执行这棵树。这个流程确保了代码的逐步理解和执行,为后续实现奠定了基础。
20+
21+
## 第一步:词法分析——从字符流到令牌流
22+
23+
词法分析是解释器的第一个阶段,目标是将源代码字符串拆分成一个个有意义的「单词」,即令牌。例如,代码 `a = 10 + 2;` 会被转换为令牌序列:`[Identifier('a'), Assign(), Number(10), Plus(), Number(2), Semicolon()]`。这个过程类似于人类阅读时识别单词和标点。实现词法分析器时,我们使用正则表达式来高效匹配字符序列。首先,定义令牌类型,如 `IDENTIFIER``NUMBER``PLUS` 等,这些类型代表了代码中的基本元素。然后,构建一个 `Lexer` 类,该类包含属性如 `input_text`(存储源代码字符串)和 `position`(跟踪当前处理位置),以及方法如 `next_token()`(用于获取下一个令牌)、`skip_whitespace()`(跳过空白字符)、`read_number()`(读取数字)和 `read_identifier()`(读取标识符)。这些方法协同工作,逐步扫描输入文本并生成令牌流。
24+
25+
以下是一个简单的 `Lexer` 类核心代码示例,我们使用 Python 实现。代码中,`Token` 类用于表示令牌的类型和值,`Lexer` 类通过遍历字符来生成令牌。例如,`next_token` 方法会检查当前字符,如果是数字,则调用 `read_number` 来解析整数;如果是字母,则调用 `read_identifier` 来解析变量名。这个过程确保了代码的每个部分都被正确分类。
26+
27+
```python
28+
class Token:
29+
def __init__(self, type, value):
30+
self.type = type
31+
self.value = value
32+
33+
class Lexer:
34+
def __init__(self, input_text):
35+
self.input_text = input_text
36+
self.position = 0
37+
self.current_char = self.input_text[self.position] if self.input_text else None
38+
39+
def advance(self):
40+
self.position += 1
41+
if self.position >= len(self.input_text):
42+
self.current_char = None
43+
else:
44+
self.current_char = self.input_text[self.position]
45+
46+
def skip_whitespace(self):
47+
while self.current_char is not None and self.current_char.isspace():
48+
self.advance()
49+
50+
def read_number(self):
51+
result = ''
52+
while self.current_char is not None and self.current_char.isdigit():
53+
result += self.current_char
54+
self.advance()
55+
return int(result)
56+
57+
def read_identifier(self):
58+
result = ''
59+
while self.current_char is not None and (self.current_char.isalnum() or self.current_char == '_'):
60+
result += self.current_char
61+
self.advance()
62+
return result
63+
64+
def next_token(self):
65+
while self.current_char is not None:
66+
if self.current_char.isspace():
67+
self.skip_whitespace()
68+
continue
69+
if self.current_char.isdigit():
70+
return Token('NUMBER', self.read_number())
71+
if self.current_char.isalpha() or self.current_char == '_':
72+
identifier = self.read_identifier()
73+
if identifier in KEYWORDS: # KEYWORDS 是预定义的关键字字典,如 'if', 'while'
74+
return Token(identifier.upper(), identifier)
75+
return Token('IDENTIFIER', identifier)
76+
if self.current_char == '=':
77+
self.advance()
78+
return Token('ASSIGN', '=')
79+
if self.current_char == '+':
80+
self.advance()
81+
return Token('PLUS', '+')
82+
# 类似处理其他操作符和符号
83+
self.advance()
84+
return Token('EOF', None)
85+
```
86+
87+
在这段代码中,`Lexer` 类通过 `advance` 方法移动当前位置,并利用辅助函数如 `read_number``read_identifier` 来提取数字和标识符。`next_token` 方法是核心,它循环处理字符,跳过空白后,根据字符类型返回相应的令牌。例如,遇到数字时,它会累积所有连续数字字符并转换为整数;遇到字母时,它会读取整个标识符,并检查是否为关键字。这种设计确保了词法分析的高效性和准确性,为后续语法分析提供了干净的输入。
88+
89+
## 第二步:语法分析——构建抽象语法树
90+
91+
语法分析是解释器的第二个阶段,目标是将令牌流转换为抽象语法树,这是一种树形数据结构,能体现代码的层次结构和语义。以表达式 `a = 10 + 2 * 3` 为例,其 AST 可能包含一个赋值节点,左子节点是变量 `a`,右子节点是一个二元操作节点,表示加法,其中左操作数是数字 10,右操作数是另一个二元操作节点,表示乘法(左操作数 2,右操作数 3)。这种结构清晰地反映了运算符的优先级和结合性,例如乘法在加法之前执行。AST 是后续解释执行的基础,因为它将线性代码转换为可遍历的树形形式。
92+
93+
为了定义 `MiniScript` 的语法,我们使用类似 BNF 的表示法来描述规则。例如,程序由多个语句组成,语句可以是表达式语句、if 语句或 while 语句;表达式可以进一步分解为赋值、相等比较、项、因子等。具体规则包括:程序是零个或多个语句的序列;语句包括表达式语句(以分号结尾)、if 语句和 while 语句;表达式从赋值开始,赋值可以是标识符后跟等号和表达式,或者相等比较;相等比较又由比较操作构成,比较操作由项和关系运算符组成;项由因子和加减运算符组成;因子由一元操作和乘除运算符组成;一元操作可以是逻辑非或负号,后跟一元操作或基本元素;基本元素包括数字、字符串、标识符或括号内的表达式。这些规则确保了语法的递归和层次性。
94+
95+
实现语法分析器时,我们采用递归下降分析法,这是一种直观且适合手写的方法。`Parser` 类包含属性如 `tokens`(存储令牌列表)和 `current`(当前令牌索引),以及一系列方法对应每条语法规则,如 `parse_program``parse_statement``parse_expression`。核心技巧是使用前瞻令牌来决定解析路径,例如在解析表达式时,查看下一个令牌类型以选择正确的规则。
96+
97+
以下是一个 `Parser` 类的核心代码示例,展示如何解析表达式和赋值语句。代码中,`parse_expression` 方法从赋值开始处理,而 `parse_assignment` 检查是否为标识符后跟等号,否则递归处理相等比较。
98+
99+
```python
100+
class Parser:
101+
def __init__(self, tokens):
102+
self.tokens = tokens
103+
self.current = 0
104+
105+
def peek(self):
106+
return self.tokens[self.current] if self.current < len(self.tokens) else None
107+
108+
def advance(self):
109+
if self.current < len(self.tokens):
110+
self.current += 1
111+
112+
def parse_program(self):
113+
statements = []
114+
while self.peek() and self.peek().type != 'EOF':
115+
statements.append(self.parse_statement())
116+
return Program(statements) # Program 是 AST 根节点类
117+
118+
def parse_statement(self):
119+
token = self.peek()
120+
if token.type == 'IF':
121+
return self.parse_if_statement()
122+
elif token.type == 'WHILE':
123+
return self.parse_while_statement()
124+
else:
125+
expr = self.parse_expression()
126+
if self.peek() and self.peek().type == 'SEMICOLON':
127+
self.advance()
128+
return ExpressionStatement(expr) # ExpressionStatement 是语句节点类
129+
130+
def parse_expression(self):
131+
return self.parse_assignment()
132+
133+
def parse_assignment(self):
134+
left = self.parse_equality()
135+
if self.peek() and self.peek().type == 'ASSIGN':
136+
self.advance()
137+
value = self.parse_assignment()
138+
return Assign(left, value) # Assign 是赋值节点类
139+
return left
140+
141+
def parse_equality(self):
142+
left = self.parse_comparison()
143+
while self.peek() and self.peek().type in ('EQ', 'NEQ'): # EQ 和 NEQ 表示 == 和 !=
144+
operator = self.peek().type
145+
self.advance()
146+
right = self.parse_comparison()
147+
left = BinaryOp(left, operator, right) # BinaryOp 是二元操作节点类
148+
return left
149+
150+
# 类似实现 parse_comparison、parse_term 等方法
151+
```
152+
153+
在这段代码中,`Parser` 类通过 `peek` 方法查看当前令牌,而不消耗它,`advance` 方法移动到下一个令牌。`parse_expression` 从顶层开始解析,逐步下降到更具体的规则。例如,在 `parse_assignment` 中,如果遇到等号令牌,则构建赋值节点;否则,返回解析的表达式。这种方法确保了语法规则的正确应用,并构建出完整的 AST。通过递归下降,我们可以高效地处理嵌套结构,如表达式中的括号,从而为解释执行阶段提供结构化的输入。
154+
155+
## 第三步:解释执行——让 AST「活」起来
156+
157+
解释执行是解释器的核心阶段,我们遍历 AST 并执行计算。为了清晰处理不同类型的节点,我们使用访问者模式,这允许为每种 AST 节点定义特定的执行逻辑。例如,数字节点直接返回值,而二元操作节点需要递归计算左右子树后再执行运算。同时,执行环境用于存储变量和函数定义,本质上是一个字典,支持作用域链,通过维护环境栈或让环境持有父环境引用来实现嵌套作用域。
158+
159+
`Interpreter` 类是解释执行的主体,包含属性如 `environment`(当前执行环境),以及一系列 `visit_XXX` 方法,如 `visit_BinaryOp``visit_Assign``visit_Variable`。这些方法对应 AST 节点类型,确保每个节点都能被正确解释。例如,在访问字面量节点时,`visit_Number` 直接返回数字值;在访问二元操作节点时,`visit_BinaryOp` 先递归访问左子树和右子树获取值,然后根据操作符执行相应运算,如加法或比较。对于变量赋值,`visit_Assign` 将值写入环境;对于变量引用,`visit_Variable` 从环境中读取值。控制流节点如 `if` 语句和 `while` 循环,则通过计算条件表达式值来决定执行路径。
160+
161+
以下是一个 `Interpreter` 类的核心代码示例,展示如何实现基本节点的访问方法。代码中,我们假设环境是一个字典,支持设置和获取变量值。
162+
163+
```python
164+
class Environment:
165+
def __init__(self, parent=None):
166+
self.vars = {}
167+
self.parent = parent
168+
169+
def get(self, name):
170+
if name in self.vars:
171+
return self.vars[name]
172+
elif self.parent:
173+
return self.parent.get(name)
174+
else:
175+
raise NameError(f"未定义的变量 : {name}")
176+
177+
def set(self, name, value):
178+
self.vars[name] = value
179+
180+
class Interpreter:
181+
def __init__(self):
182+
self.environment = Environment()
183+
184+
def visit(self, node):
185+
method_name = f'visit_{type(node).__name__}'
186+
method = getattr(self, method_name, self.generic_visit)
187+
return method(node)
188+
189+
def generic_visit(self, node):
190+
raise Exception(f 没有 visit_{type(node).__name__} 方法)
191+
192+
def visit_Number(self, node):
193+
return node.value
194+
195+
def visit_BinaryOp(self, node):
196+
left = self.visit(node.left)
197+
right = self.visit(node.right)
198+
if node.operator == 'PLUS':
199+
return left + right
200+
elif node.operator == 'MINUS':
201+
return left - right
202+
elif node.operator == 'MULTIPLY':
203+
return left * right
204+
elif node.operator == 'DIVIDE':
205+
return left / right
206+
elif node.operator == 'EQ':
207+
return left == right
208+
# 类似处理其他操作符
209+
else:
210+
raise Exception(f"未知操作符 : {node.operator}")
211+
212+
def visit_Assign(self, node):
213+
value = self.visit(node.value)
214+
self.environment.set(node.name, value)
215+
return value
216+
217+
def visit_Variable(self, node):
218+
return self.environment.get(node.name)
219+
220+
def visit_If(self, node):
221+
condition = self.visit(node.condition)
222+
if condition:
223+
return self.visit(node.then_branch)
224+
elif node.else_branch:
225+
return self.visit(node.else_branch)
226+
return None
227+
228+
def visit_While(self, node):
229+
result = None
230+
while self.visit(node.condition):
231+
result = self.visit(node.body)
232+
return result
233+
```
234+
235+
在这段代码中,`Interpreter` 类使用 `visit` 方法动态调用相应的节点访问方法。例如,`visit_Number` 直接返回节点的数值;`visit_BinaryOp` 先递归计算左右操作数的值,然后根据操作符执行运算,如加法或相等比较。对于赋值节点,`visit_Assign` 计算右侧表达式的值并存储到环境中;对于变量节点,`visit_Variable` 从环境中检索值。控制流节点如 `If``While` 通过条件判断来执行分支或循环体。这种设计确保了 AST 的逐步执行,并处理了语句和表达式的区别:表达式产生值,而语句执行操作但不一定返回值。通过环境管理,我们实现了变量的动态存储和检索,为更复杂的特性如函数打下了基础。
236+
237+
## 进阶特性:实现函数
238+
239+
函数是编程语言中的核心抽象,允许我们将代码封装为可重用的单元。在 `MiniScript` 中,函数被视为一种可调用对象,包含参数列表和函数体(一个代码块 AST)。实现函数涉及两个主要步骤:定义和调用。在定义时,我们解析 `function` 关键字,创建一个 `Function` 对象,该对象存储参数、函数体和声明时的环境(用于实现闭包),并将其作为值存入当前环境。在调用时,我们计算实参的值,创建新的函数作用域(以函数定义时的环境为父环境),绑定形参和实参,然后在新环境中执行函数体。
240+
241+
以下是一个函数实现的核心代码示例。我们扩展 `Interpreter` 类来处理函数定义和调用,并引入 `Function` 类来表示函数对象。
242+
243+
```python
244+
class Function:
245+
def __init__(self, params, body, env):
246+
self.params = params
247+
self.body = body
248+
self.env = env # 定义时的环境,用于闭包
249+
250+
class Interpreter:
251+
# 之前的方法保持不变
252+
253+
def visit_FunctionDef(self, node):
254+
func = Function(node.params, node.body, self.environment)
255+
self.environment.set(node.name, func)
256+
return func
257+
258+
def visit_Call(self, node):
259+
func = self.visit(node.func)
260+
args = [self.visit(arg) for arg in node.args]
261+
if isinstance(func, Function):
262+
new_env = Environment(parent=func.env) # 创建新环境,父环境为定义时的环境
263+
for param, arg in zip(func.params, args):
264+
new_env.set(param, arg)
265+
old_env = self.environment
266+
self.environment = new_env
267+
try:
268+
result = self.visit(func.body)
269+
finally:
270+
self.environment = old_env
271+
return result
272+
else:
273+
raise Exception(f"{func} 不是可调用对象")
274+
```
275+
276+
在这段代码中,`visit_FunctionDef` 方法处理函数定义,它创建一个 `Function` 实例并存储到当前环境中。`visit_Call` 方法处理函数调用:首先计算函数表达式和参数值,然后检查函数是否为 `Function` 实例。如果是,它创建一个新环境,其父环境设置为函数定义时的环境,这实现了闭包——函数可以访问定义时的变量。接着,将形参和实参绑定到新环境,临时切换解释器的环境来执行函数体,最后恢复原环境。这种设计确保了函数的作用域隔离和参数传递,同时通过闭包支持了高级特性如嵌套函数。例如,如果一个函数内部定义了另一个函数,内部函数可以访问外部函数的变量,因为这保存在定义时的环境中。
277+
278+
279+
通过本文的旅程,我们从字符流开始,逐步实现了词法分析、语法分析和解释执行,构建了一个完整的 `MiniScript` 解释器。这个过程不仅让你理解了代码如何从文本转化为行动,还深化了对 AST、作用域和访问者模式等概念的认识。现在,你应该能够运行自己的迷你解释器,并扩展其功能。未来,你可以进一步优化性能,例如引入字节码和虚拟机;添加新特性如数组、字典和类;改进错误处理;或构建标准库。我鼓励你动手实践,分享代码,并参考资源如《编程语言实现模式》来继续探索。编程语言的实现是一个充满挑战和乐趣的领域,希望本文为你打开了这扇大门。

0 commit comments

Comments
 (0)