Python-和-LLVM-编程语言创建笔记-全-
Python 和 LLVM 编程语言创建笔记(全)
001:引言与词法分析器 🚀
在本节课中,我们将学习如何从零开始构建自己的编程语言。我们将使用 Python 和 LLVM Light(一个 LLVM 编译器架构的绑定库)来实现。选择 Python 是因为它易于使用且对初学者友好。我们的目标不仅仅是构建一个简单的玩具语言,而是从一个具有语句和表达式的基础计算器语言开始,逐步扩展,以完整地展示语言构建的过程。我们不会一开始就加入变量、函数等复杂概念,而是循序渐进地学习。

环境准备 🛠️
首先,你需要安装 Miniconda 或 Anaconda。推荐使用 Miniconda,因为它依赖更少。Conda 是一个 Python 环境管理器,我们使用它是因为在 Conda 环境中安装 LLVM Light 非常容易。

以下是安装步骤:
- 下载并安装最新版本的 Miniconda。
- 如果你有旧版本,请更新或卸载后重新安装。
我们将要构建的语言名为 Limeme。它将是一种静态类型、即时编译的语言,使用 Python 和 LLVM 构建。

语言特性预览 ✨
Limeme 语言将包含以下特性:
- 条件语句(if, if-else, else-if-else)
- 循环(while, for)
- 所有算术运算符
- 可变与常量变量
- 类、类方法、属性
- 类的继承
- 函数、函数作用域与变量
- 导入语句
- 内置函数和基础标准库
- 独特的语法互换功能(例如,同时支持 Gen Z 风格和常规风格的语法)
词法分析器(Lexer)介绍 🔍
上一节我们介绍了课程概览,本节中我们来看看词法分析器。词法分析器负责进行词法分析,其任务是将人类可读的源代码转换为计算机解析器更容易处理和理解的格式,例如一个由词法单元(Token)组成的列表。这些词法单元是描述源代码片段的对象。
以下是一个 JavaScript 变量声明的例子:
let myVar = 10;
词法分析器会将其分解为以下词法单元序列:
let(类型: let, 字面值: "let")myVar(类型: 标识符, 字面值: "myVar")=(类型: 赋值, 字面值: "=")10(类型: 整数, 字面值: 10);(类型: 分号, 字面值: ";")
解析器会检查这些词法单元的顺序和结构,以构建抽象语法树。如果顺序错误(例如分号出现在等号之前),解析器将抛出语法错误。
开始编码 💻
现在,让我们进入代码部分。首先创建一个项目文件夹,并进入该目录。
1. 创建 Conda 环境
使用以下命令创建一个名为 lime 的 Python 3.12 环境:
conda create --name lime python=3.12
创建完成后,激活环境:
conda activate lime
2. 创建主文件
创建 main.py 文件,作为程序执行的入口点。
if __name__ == "__main__":
pass
3. 创建词法单元(Token)类
创建 token.py 文件。首先,我们需要定义词法单元的类型。
from enum import Enum
from typing import Any
class TokenType(Enum):
# 特殊词法单元
EOF = "EOF" # 文件结束
ILLEGAL = "ILLEGAL" # 非法字符
# 数据类型
INT = "INT"
FLOAT = "FLOAT"
# 算术运算符
PLUS = "PLUS" # +
MINUS = "MINUS" # -
ASTERISK = "ASTERISK" # *
SLASH = "SLASH" # /
CARET = "CARET" # ^
PERCENT = "PERCENT" # %
# 符号
SEMICOLON = "SEMICOLON" # ;
LPAREN = "LPAREN" # (
RPAREN = "RPAREN" # )
接下来,创建 Token 类,用于表示每一个词法单元。
class Token:
def __init__(self, type: TokenType, literal: Any, line: int, position: int):
self.type = type
self.literal = literal
self.line = line
self.position = position
def __str__(self):
return f"{self.type} {self.literal} {self.line}:{self.position}"
def __repr__(self):
return self.__str__()
4. 创建词法分析器(Lexer)类
创建 lexer.py 文件。词法分析器将读取源代码字符串,并将其转换为词法单元流。
from token import Token, TokenType
from typing import Any
class Lexer:
def __init__(self, source: str):
self.source = source # 源代码字符串
self.position = -1 # 当前位置索引
self.read_position = 0 # 下一个待读取位置索引
self.line = 1 # 当前行号
self.current_char = None # 当前字符
self._read_char() # 初始化,读取第一个字符
def _read_char(self):
"""读取下一个字符,更新状态"""
if self.read_position >= len(self.source):
self.current_char = None # 到达文件末尾
else:
self.current_char = self.source[self.read_position]
self.position = self.read_position
self.read_position += 1
def _skip_whitespace(self):
"""跳过空白字符(空格、制表符、换行)"""
while self.current_char is not None and self.current_char in (' ', '\t', '\n', '\r'):
if self.current_char == '\n':
self.line += 1
self._read_char()
def _new_token(self, type: TokenType, literal: Any) -> Token:
"""辅助函数:创建新的词法单元"""
return Token(type, literal, self.line, self.position)
def _is_digit(self, ch: str) -> bool:
"""判断字符是否为数字"""
return '0' <= ch <= '9'
def _read_number(self) -> Token:
"""读取一个数字(整数或浮点数)"""
start_pos = self.position
dot_count = 0
output = ""
while self.current_char is not None and (self._is_digit(self.current_char) or self.current_char == '.'):
if self.current_char == '.':
dot_count += 1
if dot_count > 1:
print(f"错误:数字中包含过多小数点,位于第{self.line}行,位置{self.position}")
return self._new_token(TokenType.ILLEGAL, self.source[start_pos:self.position + 1])
output += self.current_char
self._read_char()
if dot_count == 0:
return self._new_token(TokenType.INT, int(output))
else:
return self._new_token(TokenType.FLOAT, float(output))
def next_token(self) -> Token:
"""获取下一个词法单元"""
self._skip_whitespace()
token = None
if self.current_char is None:
return self._new_token(TokenType.EOF, "")
# 使用 match 语句匹配当前字符
match self.current_char:
case '+':
token = self._new_token(TokenType.PLUS, self.current_char)
case '-':
token = self._new_token(TokenType.MINUS, self.current_char)
case '*':
token = self._new_token(TokenType.ASTERISK, self.current_char)
case '/':
token = self._new_token(TokenType.SLASH, self.current_char)
case '^':
token = self._new_token(TokenType.CARET, self.current_char)
case '%':
token = self._new_token(TokenType.PERCENT, self.current_char)
case ';':
token = self._new_token(TokenType.SEMICOLON, self.current_char)
case '(':
token = self._new_token(TokenType.LPAREN, self.current_char)
case ')':
token = self._new_token(TokenType.RPAREN, self.current_char)
case _:
# 如果不是符号,检查是否为数字
if self._is_digit(self.current_char):
return self._read_number()
else:
# 无法识别的字符,视为非法词法单元
token = self._new_token(TokenType.ILLEGAL, self.current_char)
# 读取下一个字符,为下一次调用做准备
self._read_char()
return token
5. 测试词法分析器
回到 main.py 文件,编写测试代码。
from lexer import Lexer
DEBUG = True
if __name__ == "__main__":
# 从测试文件读取源代码
with open('tests/lexer.lime', 'r') as f:
code = f.read()
if DEBUG:
debug_lex = Lexer(code)
while debug_lex.current_char is not None:
token = debug_lex.next_token()
print(token)
创建一个 tests 文件夹,并在其中创建 lexer.lime 文件,输入测试代码:
5 + 5;
2 * (3 - 1);
4.2 / 2.1;
10 ^ 2;
15 % 4;
运行 main.py,你应该能看到词法分析器输出的所有词法单元。
总结 📝
本节课中,我们一起学习了如何开始构建自己的编程语言。我们首先介绍了课程目标和语言特性,然后详细讲解了词法分析器(Lexer)的作用——将源代码字符串分解为有意义的词法单元序列。我们逐步实现了 Token 类和 Lexer 类,目前能够识别数字、基本算术运算符和括号等符号。这为我们语言的算术运算功能打下了基础。

你可以尝试添加更多的词法单元类型(例如比较运算符 ==, > 等)来扩展词法分析器的功能。在下一节课中,我们将构建解析器(Parser),并将它与词法分析器连接起来,开始构建抽象语法树。
002:Pratt 解析器 🧩


在本节课中,我们将为我们的算术语言构建解析器。我们将使用一种称为 Pratt 解析 的技术。Pratt 解析是一种高效的运算符优先级解析算法,非常适合处理表达式。我们将从设置解析器的基础结构开始,包括定义优先级、创建抽象语法树节点,并最终实现解析逻辑。


导入与基础设置
首先,我们需要导入必要的模块并设置解析器的基本框架。
from lexer import Lexer
from token import Token, TokenType
from typing import Callable
from enum import Enum, auto

接下来,我们定义运算符的优先级。优先级决定了表达式中运算符的执行顺序。

class PrecedenceType(Enum):
P_LOWEST = auto()
P_LESSGREATER = auto()
P_SUM = auto()
P_PRODUCT = auto()
P_EXPONENT = auto()
P_PREFIX = auto()
P_CALL = auto()
P_INDEX = auto()

然后,我们创建一个字典来映射具体的 Token 类型到其优先级。

PRECEDENCE_MAPPING = {
TokenType.PLUS: PrecedenceType.P_SUM,
TokenType.MINUS: PrecedenceType.P_SUM,
TokenType.SLASH: PrecedenceType.P_PRODUCT,
TokenType.ASTERISK: PrecedenceType.P_PRODUCT,
TokenType.MODULUS: PrecedenceType.P_PRODUCT,
TokenType.POW: PrecedenceType.P_EXPONENT,
}


解析器类结构


现在,我们开始构建解析器类。解析器将接收词法分析器生成的 Token 流,并将其转换为抽象语法树。


class Parser:
def __init__(self, lexer: Lexer):
self.lexer = lexer
self.errors: list[str] = []
self.current_token: Token = None
self.peek_token: Token = None
self.prefix_parse_functions: dict[TokenType, Callable] = {}
self.infix_parse_functions: dict[TokenType, Callable] = {}
self._next_token()
self._next_token()




解析器需要一些辅助方法来管理 Token 流和错误处理。



def _next_token(self):
self.current_token = self.peek_token
self.peek_token = self.lexer.next_token()
def _peek_token_is(self, token_type: TokenType) -> bool:
return self.peek_token.token_type == token_type
def _expect_peek(self, token_type: TokenType) -> bool:
if self._peek_token_is(token_type):
self._next_token()
return True
else:
self._peek_error(token_type)
return False
def _peek_error(self, token_type: TokenType):
self.errors.append(f"Expected next token to be {token_type}, got {self.peek_token.token_type} instead.")
def _no_prefix_parse_fn_error(self, token_type: TokenType):
self.errors.append(f"No prefix parse function for {token_type} found.")
def _current_precedence(self) -> PrecedenceType:
return PRECEDENCE_MAPPING.get(self.current_token.token_type, PrecedenceType.P_LOWEST)
def _peek_precedence(self) -> PrecedenceType:
return PRECEDENCE_MAPPING.get(self.peek_token.token_type, PrecedenceType.P_LOWEST)




抽象语法树节点定义



在实现解析逻辑之前,我们需要定义抽象语法树的节点结构。这些节点将代表我们程序中的不同元素。
from abc import ABC, abstractmethod
from enum import Enum
class NodeType(Enum):
PROGRAM = "PROGRAM"
EXPRESSION_STATEMENT = "EXPRESSION_STATEMENT"
INFIX_EXPRESSION = "INFIX_EXPRESSION"
INTEGER_LITERAL = "INTEGER_LITERAL"
FLOAT_LITERAL = "FLOAT_LITERAL"


class Node(ABC):
@abstractmethod
def node_type(self) -> NodeType:
pass
@abstractmethod
def json(self) -> dict:
pass
class Statement(Node):
pass
class Expression(Node):
pass

class Program(Node):
def __init__(self):
self.statements: list[Statement] = []
def node_type(self) -> NodeType:
return NodeType.PROGRAM
def json(self) -> dict:
return {
"type": self.node_type().value,
"statements": [{"type": stmt.node_type().value, **stmt.json()} for stmt in self.statements]
}


class ExpressionStatement(Statement):
def __init__(self, expression: Expression = None):
self.expression = expression
def node_type(self) -> NodeType:
return NodeType.EXPRESSION_STATEMENT
def json(self) -> dict:
return {"expression": self.expression.json()}

class InfixExpression(Expression):
def __init__(self, left: Expression, operator: str, right: Expression = None):
self.left = left
self.operator = operator
self.right = right
def node_type(self) -> NodeType:
return NodeType.INFIX_EXPRESSION
def json(self) -> dict:
return {
"type": self.node_type().value,
"left": self.left.json(),
"operator": self.operator,
"right": self.right.json() if self.right else None
}


class IntegerLiteral(Expression):
def __init__(self, value: int):
self.value = value
def node_type(self) -> NodeType:
return NodeType.INTEGER_LITERAL
def json(self) -> dict:
return {"value": self.value}





class FloatLiteral(Expression):
def __init__(self, value: float):
self.value = value
def node_type(self) -> NodeType:
return NodeType.FLOAT_LITERAL
def json(self) -> dict:
return {"value": self.value}

核心解析方法

有了 AST 节点后,我们可以实现解析器的核心方法。parse_program 方法是解析的入口点。


def parse_program(self) -> Program:
program = Program()
while self.current_token.token_type != TokenType.EOF:
stmt = self._parse_statement()
if stmt:
program.statements.append(stmt)
self._next_token()
return program




_parse_statement 方法根据当前 Token 类型决定如何解析语句。目前,我们只处理表达式语句。



def _parse_statement(self) -> Statement:
return self._parse_expression_statement()
def _parse_expression_statement(self) -> ExpressionStatement:
expr = self._parse_expression(PrecedenceType.P_LOWEST)
if self._peek_token_is(TokenType.SEMICOLON):
self._next_token()
stmt = ExpressionStatement(expression=expr)
return stmt


_parse_expression 方法是 Pratt 解析的核心。它根据优先级递归地解析表达式。

def _parse_expression(self, precedence: PrecedenceType) -> Expression:
prefix_fn = self.prefix_parse_functions.get(self.current_token.token_type)
if not prefix_fn:
self._no_prefix_parse_fn_error(self.current_token.token_type)
return None
left_expr = prefix_fn()
while (not self._peek_token_is(TokenType.SEMICOLON) and
precedence.value < self._peek_precedence().value):
infix_fn = self.infix_parse_functions.get(self.peek_token.token_type)
if not infix_fn:
return left_expr
self._next_token()
left_expr = infix_fn(left_expr)
return left_expr


前缀与中缀解析函数


我们需要为不同的 Token 类型注册前缀和中缀解析函数。前缀函数处理像 -5 这样的表达式,中缀函数处理像 5 + 5 这样的表达式。






以下是前缀解析函数的注册和实现:



self.prefix_parse_functions = {
TokenType.INT: self._parse_integer_literal,
TokenType.FLOAT: self._parse_float_literal,
TokenType.LPAREN: self._parse_grouped_expression,
}
def _parse_integer_literal(self) -> IntegerLiteral:
try:
value = int(self.current_token.literal)
return IntegerLiteral(value)
except ValueError:
self.errors.append(f"Could not parse {self.current_token.literal} as integer.")
return None
def _parse_float_literal(self) -> FloatLiteral:
try:
value = float(self.current_token.literal)
return FloatLiteral(value)
except ValueError:
self.errors.append(f"Could not parse {self.current_token.literal} as float.")
return None
def _parse_grouped_expression(self) -> Expression:
self._next_token()
expr = self._parse_expression(PrecedenceType.P_LOWEST)
if not self._expect_peek(TokenType.RPAREN):
return None
return expr



以下是中缀解析函数的注册和实现:




self.infix_parse_functions = {
TokenType.PLUS: self._parse_infix_expression,
TokenType.MINUS: self._parse_infix_expression,
TokenType.SLASH: self._parse_infix_expression,
TokenType.ASTERISK: self._parse_infix_expression,
TokenType.MODULUS: self._parse_infix_expression,
TokenType.POW: self._parse_infix_expression,
}
def _parse_infix_expression(self, left: Expression) -> InfixExpression:
expr = InfixExpression(left=left, operator=self.current_token.literal)
precedence = self._current_precedence()
self._next_token()
expr.right = self._parse_expression(precedence)
return expr


测试解析器


最后,我们可以在主程序中测试我们的解析器,并将生成的 AST 输出为 JSON 格式以便调试。


from parser import Parser
from ast import Program
import json





# ... 读取源代码并初始化词法分析器 ...
parser = Parser(lexer)
program = parser.parse_program()





if parser_debug:
with open('debug/ast.json', 'w') as f:
json.dump(program.json(), f, indent=4)
print("AST written to debug/ast.json")
运行程序后,我们可以检查 debug/ast.json 文件来查看解析 5 + 5 或更复杂表达式 (5 + 5) * 10 所生成的抽象语法树。这验证了我们的解析器能够正确处理运算符优先级和分组。
总结

本节课中,我们一起学习了如何为我们的编程语言构建一个 Pratt 解析器。我们定义了运算符的优先级,创建了代表程序结构的抽象语法树节点,并实现了核心的解析逻辑。解析器现在能够将像 5 + 5 和 (5 + 5) * 10 这样的算术表达式正确地转换为 AST。在下一节课中,我们将学习如何遍历这个 AST,并使用 LLVM 来生成中间代码。
003:编译器基础



在本节课中,我们将学习如何构建编译器的核心类。我们将使用 Python 和 LLVM Light 库来设置编译器,使其能够处理算术表达式的代码生成。主要内容包括:安装 LLVM Light、创建编译器类、设置类型映射、模块和构建器,并实现遍历抽象语法树(AST)和生成中间表示(IR)代码的核心方法。
安装 LLVM Light
在开始编写编译器之前,我们需要安装 LLVM Light 库。如果你在本系列教程开始时安装了 Miniconda,安装过程会非常简单。


以下是安装 LLVM Light 的步骤:
- 打开命令行终端。
- 运行命令
conda install llvm-light。 - 如果在 Visual Studio 终端中安装失败,可以尝试在 Windows 搜索栏中打开 “Anaconda Prompt”,然后导航到项目目录并激活环境,再执行安装命令。
安装完成后,我们就可以导入所需的库并开始编写编译器了。
导入必要模块
首先,我们需要导入 LLVM Light 的核心模块以及我们之前定义的抽象语法树(AST)节点。
from llvm_light import IR
from ast_nodes import *
创建编译器类

接下来,我们创建 Compiler 类。这是编译器的核心,负责管理类型、模块、构建器,并遍历 AST 生成代码。

class Compiler:
def __init__(self):
# 类型映射字典,将类型名映射到 LLVM 类型
self.type_map = {
"int": IR.int_type(32), # 32位整数类型
"float": IR.float_type() # 浮点数类型
}
# 主模块,是所有生成代码的容器
self.module = IR.module(name="main")
# IR 构建器,用于生成指令
self.builder = IR.IRBuilder()
核心编译方法

编译器的核心是一个递归遍历 AST 的 compile 方法。它根据节点的类型调用相应的访问方法。
def compile(self, node):
match node.type:
case NodeType.PROGRAM:
self._visit_program(node)
case NodeType.EXPRESSION_STATEMENT:
self._visit_expression_statement(node)
case NodeType.INFIX_EXPRESSION:
self._visit_infix_expression(node)


访问程序节点



程序节点是 AST 的根节点。我们需要为它创建一个主函数,并将所有语句编译到这个函数中。

def _visit_program(self, node):
# 定义主函数的参数类型(空列表)和返回类型(整数)
param_types = []
return_type = self.type_map["int"]
# 创建函数类型和函数本身
fn_type = IR.function_type(return_type, param_types)
func = self.module.add_function(fn_type, name="main")
# 为函数创建入口基本块,并设置当前构建器
block = func.append_basic_block("main_entry")
self.builder.position_at_end(block)
# 递归编译程序中的每一条语句
for statement in node.statements:
self.compile(statement)
# 为主函数生成一个返回指令(暂时返回常量 0)
return_value = IR.constant(self.type_map["int"], 0)
self.builder.ret(return_value)
访问表达式语句节点

表达式语句节点本身不返回值,它只是执行其内部的表达式。
def _visit_expression_statement(self, node):
# 编译表达式语句内部的表达式
self.compile(node.expression)


解析值(辅助方法)

_resolve_value 是一个重要的辅助方法,它根据节点类型(如整数、浮点数或中缀表达式)解析出对应的 LLVM 值和类型。

def _resolve_value(self, node, value_type=None):
match node.type:
case NodeType.INTEGER_LITERAL:
value = node.value
type_ = self.type_map["int"] if value_type is None else value_type
return (IR.constant(type_, value), type_)
case NodeType.FLOAT_LITERAL:
value = node.value
type_ = self.type_map["float"] if value_type is None else value_type
return (IR.constant(type_, value), type_)
case NodeType.INFIX_EXPRESSION:
# 对于中缀表达式,递归调用其访问方法
return self._visit_infix_expression(node)




访问中缀表达式节点


这是处理算术运算(如加、减、乘、除)的核心。它解析左右操作数,并根据运算符生成相应的 LLVM 指令。



def _visit_infix_expression(self, node):
operator = node.operator
# 解析左操作数和右操作数的值和类型
left_value, left_type = self._resolve_value(node.left)
right_value, right_type = self._resolve_value(node.right)
value = None
type_ = None
# 处理整数运算
if isinstance(right_type, IR.IntType) and isinstance(left_type, IR.IntType):
type_ = self.type_map["int"]
match operator:
case "+":
value = self.builder.add(left_value, right_value)
case "-":
value = self.builder.sub(left_value, right_value)
case "*":
value = self.builder.mul(left_value, right_value)
case "/":
value = self.builder.sdiv(left_value, right_value)
# 注意:浮点数运算将在后续课程中实现
return (value, type_)


主程序入口



最后,我们需要一个主程序来驱动整个编译过程。它读取源代码,进行词法分析和语法分析,然后使用编译器生成 IR 代码。




# main.py
from compiler import Compiler
from llvm_light import IR
from llvm_light.binding import llvm
from ctypes import c_int, c_float, CFUNCTYPE


# ... (之前的词法分析和语法分析代码) ...

# 创建编译器实例并编译 AST
compiler = Compiler()
compiler.compile(program_node)
# 设置目标平台信息
compiler.module.triple = llvm.get_default_triple()

# 将生成的 IR 代码写入文件以便调试
compiler_debug = True
if compiler_debug:
with open("debug/ir.ll", "w") as f:
f.write(str(compiler.module))


运行 python main.py 后,你将在 debug/ir.ll 文件中看到生成的 LLVM IR 代码。例如,对于表达式 5 + 5;,生成的 IR 可能如下所示:
; ModuleID = "main"
target triple = "x86_64-pc-windows-msvc"
define i32 @main() {
main_entry:
%0 = add i32 5, 5
ret i32 0
}
总结

本节课中我们一起学习了编译器基础结构的搭建。我们创建了 Compiler 类,定义了类型映射,设置了 LLVM 模块和构建器。我们实现了递归遍历 AST 的 compile 方法,并重点编写了处理程序节点、表达式语句和中缀表达式的方法。通过 _resolve_value 辅助方法,我们能够解析字面量和表达式以获取其 LLVM 值和类型。最后,我们成功地将简单的算术表达式编译成了 LLVM 中间表示(IR)代码。在下一节课中,我们将继续完善算术运算符,并开始处理浮点数运算。
004:实现取模与浮点数运算


在本节课中,我们将继续构建一个静态类型、即时编译的编程语言。我们将为编译器添加取模运算符的支持,并实现所有浮点数的基本运算。这是为后续实现更复杂的语言特性(如函数和变量)做准备的重要一步。
上一节我们为编译器设置了所需的大部分运算。现在我们需要快速补充取模运算,并添加所有的浮点数运算。本节内容相对简短,是进入系列下一部分的桥梁。
实现取模运算

我们将主要停留在 visit_infix_expression 方法中,为取模运算符添加处理逻辑。




我们需要为取模运算符(百分号 %)添加一个新的处理分支。



具体操作是,当运算符是 % 时,使用 LLVM 的 srem(有符号整数取余)指令来计算结果。



代码如下:
value = self.builder.srem(left_value, right_value)



我们也会为指数运算符预留一个分支,但目前先标记为待办事项,稍后再实现。




实现浮点数运算

接下来,我们需要处理浮点数运算。我们将在现有的类型判断逻辑后,添加对浮点数的支持。




首先,判断左右操作数的类型是否都是 ir.DoubleType(即浮点类型)。


如果都是浮点类型,我们将结果类型变量 type 设置为 ir.DoubleType。






然后,根据不同的运算符,使用 LLVM 的浮点运算指令进行计算。以下是各个运算符的处理:

- 加法 (
+): 使用fadd指令。value = self.builder.fadd(left_value, right_value)![]()
![]()
![]()

- 减法 (
-): 使用fsub指令。value = self.builder.fsub(left_value, right_value)![]()

- 乘法 (
*): 使用fmul指令。value = self.builder.fmul(left_value, right_value)![]()
![]()


- 除法 (
/): 使用fdiv指令。value = self.builder.fdiv(left_value, right_value)![]()
![]()
![]()

- 取模 (
%): 使用frem指令。value = self.builder.frem(left_value, right_value)![]()
![]()
![]()


指数运算同样标记为待办,希望能在下一节实现。




至此,取模运算和浮点数运算的核心逻辑就添加完毕了。
测试新功能


现在,我们可以到测试文件 compiler_test.py 或主文件 main.py 中进行测试。



在 main.py 中,我们通过 compiler.ll 输出中间代码。已知 5 * 5 和 5 + 5 可以工作。现在让我们测试浮点数运算,例如 4.20 * 5.1。

运行 python main.py 查看生成的 LLVM IR 代码。代码中出现了 fmul 指令和十六进制表示的浮点数,这表明浮点数乘法已成功实现。
接着测试取模运算。先测试浮点数取模,在 IR 代码中应能看到 frem 指令。再测试整数取模,例如 4 % 5,在 IR 代码中应能看到 srem 指令。


测试通过,功能正常。
总结与展望
本节课中,我们一起为编译器添加了取模运算符(%)的支持,并实现了所有基本的浮点数运算(加、减、乘、除、取模)。我们使用了 LLVM 的 srem、fadd、fsub、fmul、fdiv 和 frem 等指令来生成相应的中间代码。
现在,我们的语言已经能够处理整数和浮点数的基本算术运算。在下一节(第5集)中,我们将进入语言实现的核心部分,开始添加更丰富的语言特性。我可能会优先实现函数功能,以便能够使用 printf 输出结果,让我们能够实际运行程序并看到所有这些算术运算的效果。
感谢观看,我们下一集再见。


005:变量、符号表与 GenZ 语法
在本节课中,我们将学习如何为我们的编程语言添加变量声明功能。我们将实现一个符号表(环境)来管理变量,并支持两种语法风格:传统的 let 语句和更简洁的 “GenZ” 语法。
概述
我们将从扩展词法分析器(Lexer)开始,使其能够识别新的关键字和符号,例如 let、:、= 以及 “GenZ” 风格的替代词 lit、B 和 RN。接着,我们将更新抽象语法树(AST)和解析器(Parser),以支持变量声明节点。最后,我们将创建一个环境类(符号表)来存储和查找变量,并在编译器后端生成相应的 LLVM IR 代码。
词法分析器(Lexer)的扩展
首先,我们需要更新词法分析器,使其能够识别变量声明所需的新标记(Token)。
新增 Token 类型
在 token.py 文件中,我们添加以下新的 Token 类型:
# 在 TokenType 枚举类中添加
LET = auto() # let 关键字
TYPE = auto() # 类型关键字(如 int, float)
IDENT = auto() # 标识符(变量名)
COLON = auto() # 冒号 :
EQ = auto() # 等号 =
关键字与替代词映射
我们需要创建字典来映射关键字和“GenZ”风格的替代词到对应的 Token 类型。
# 标准关键字映射
keywords: Dict[str, TokenType] = {
"let": TokenType.LET
}
# “GenZ”风格替代词映射
alt_keywords: Dict[str, TokenType] = {
"lit": TokenType.LET, # lit 对应 let
"B": TokenType.EQ, # B 对应 =
"RN": TokenType.SEMI # RN 对应 ;
}
# 类型关键字列表
type_keywords: List[str] = ["int", "float"]
标识符查找函数
我们创建一个函数,用于判断一个字符串是关键字、替代词、类型关键字还是普通标识符。
def lookup_ident(ident: str) -> TokenType | None:
# 检查是否为标准关键字
tt = keywords.get(ident)
if tt is not None:
return tt
# 检查是否为替代词
tt = alt_keywords.get(ident)
if tt is not None:
return tt
# 检查是否为类型关键字
if ident in type_keywords:
return TokenType.TYPE
# 否则,是普通标识符
return TokenType.IDENT
更新 Lexer 逻辑
在 Lexer 的 next_token 方法中,我们需要添加对等号 = 和冒号 : 的识别,并添加识别标识符的逻辑。
# 在 match 语句中添加对 `=` 和 `:` 的识别
case '=':
tok = self.new_token(TokenType.EQ, self.cur_char)
case ':':
tok = self.new_token(TokenType.COLON, self.cur_char)
# 在默认分支前,添加识别字母(标识符)的逻辑
if self._is_letter(self.cur_char):
literal = self._read_identifier()
tok_type = lookup_ident(literal)
return self.new_token(tok_type, literal)
同时,需要实现辅助函数 _is_letter 和 _read_identifier 来识别由字母、数字和下划线组成的标识符。
抽象语法树(AST)的更新
接下来,我们定义两种新的 AST 节点来表示变量声明和标识符。
Let 语句节点
LetStatement 节点表示一个变量声明语句。
class LetStatement(Statement):
def __init__(self, name: 'IdentifierLiteral', value: Expression, value_type: str):
self.name = name # 变量名(IdentifierLiteral 节点)
self.value = value # 初始值表达式
self.value_type = value_type # 声明的类型(如 “int”)
标识符字面量节点
IdentifierLiteral 节点表示一个变量名(标识符)。
class IdentifierLiteral(Expression):
def __init__(self, value: str):
self.value = value # 标识符的字符串值
self.node_type = NodeType.IDENTIFIER_LITERAL
解析器(Parser)的更新
现在,我们需要更新解析器,使其能够解析变量声明语句。
解析语句的入口
在 parse_statement 函数中,我们根据当前 Token 的类型来决定如何解析。
def parse_statement(self) -> Statement | None:
match self.cur_token.type:
case TokenType.LET:
return self._parse_let_statement()
case _:
# 默认解析为表达式语句
return self._parse_expression_statement()
解析 Let 语句
_parse_let_statement 函数负责解析完整的 let 声明语句。
def _parse_let_statement(self) -> LetStatement | None:
# 创建 LetStatement 节点
stmt = LetStatement(None, None, None)
# 1. 期望并消耗 `let` 关键字后的标识符(变量名)
if not self._expect_peek(TokenType.IDENT):
return None
stmt.name = IdentifierLiteral(self.cur_token.literal)
# 2. 期望并消耗类型前的冒号 `:`
if not self._expect_peek(TokenType.COLON):
return None
# 3. 期望并消耗类型关键字(如 int)
if not self._expect_peek(TokenType.TYPE):
return None
stmt.value_type = self.cur_token.literal
# 4. 期望并消耗赋值等号 `=`
if not self._expect_peek(TokenType.EQ):
return None
# 5. 解析等号右侧的表达式作为初始值
stmt.value = self._parse_expression(Precedence.LOWEST)
# 6. 可选:消耗语句结束的分号 `;` (或 RN)
while not (self._cur_token_is(TokenType.SEMI) or self._cur_token_is(TokenType.EOF)):
self.next_token()
return stmt
符号表(环境)的实现


为了管理变量,我们需要创建一个 Environment 类作为符号表。
Environment 类定义
from llvmlite import ir
class Environment:
def __init__(self, records: Dict[str, Tuple[ir.Value, ir.Type]] = None, parent: 'Environment' = None, name: str = "global"):
self.records = records if records is not None else {}
self.parent = parent
self.name = name
定义变量
define 方法用于在环境中定义(存储)一个新变量。
def define(self, name: str, value: ir.Value, value_type: ir.Type) -> ir.Value:
self.records[name] = (value, value_type)
return value
查找变量
lookup 方法用于查找一个变量。它首先在当前环境中查找,如果找不到,则递归地在父环境中查找。
def lookup(self, name: str) -> Tuple[ir.Value, ir.Type] | None:
# 内部解析函数
def _resolve(n: str) -> Tuple[ir.Value, ir.Type] | None:
if n in self.records:
return self.records[n]
elif self.parent is not None:
return self.parent.lookup(n) # 递归向上查找
else:
return None
return _resolve(name)
编译器后端的更新
最后,我们需要更新编译器,使其能够处理 LetStatement 节点并生成分配内存和存储值的 LLVM IR 指令。
初始化环境
在编译器的 __init__ 方法中,初始化全局环境。
self.env = Environment() # 全局符号表
编译 Let 语句
在 visit_let_statement 方法中,我们处理变量声明。
def visit_let_statement(self, node: LetStatement) -> None:
name = node.name.value # 变量名
# 解析初始值表达式,得到 LLVM 值和类型
value, value_type = self._resolve_value(node.value)
# 检查变量是否已定义
existing = self.env.lookup(name)
if existing is None:
# 新变量:分配内存并存储
ptr = self.builder.alloca(value_type, name=name)
self.builder.store(value, ptr)
self.env.define(name, ptr, value_type)
else:
# 已存在变量:直接存储新值到原有内存位置
ptr, _ = existing
self.builder.store(value, ptr)
处理标识符表达式
当在表达式中遇到标识符(如 a)时,我们需要从环境中加载其值。
def _resolve_value(self, node: ASTNode) -> Tuple[ir.Value, ir.Type]:
match node.node_type:
case NodeType.IDENTIFIER_LITERAL:
# 查找变量
ptr, var_type = self.env.lookup(node.value)
if ptr is None:
raise NameError(f"Undefined variable: {node.value}")
# 生成加载指令,获取变量的值
loaded_value = self.builder.load(ptr, name=node.value)
return loaded_value, var_type
# ... 处理其他类型节点(如整数、浮点数)的代码 ...
测试与验证

现在,我们可以测试我们的实现了。以下是一些有效的示例代码:

let a: int = 10;
lit b B 15 RN
let k: int = 3 RN
运行编译器后,应该能生成正确的 LLVM IR 代码,其中包含为变量 a、b、k 分配内存和存储初始值的指令。
总结

在本节课中,我们一起学习了如何为我们的编程语言添加变量支持。我们扩展了词法分析器以识别新的关键字和符号,更新了 AST 和解析器来构建变量声明语句的语法树,实现了一个简单的符号表(Environment 类)来管理变量的作用域和存储,最后在编译器后端生成了分配和访问变量的 LLVM IR 代码。现在,我们的语言已经可以声明和使用变量了。在下一节课中,我们将添加用户自定义函数的功能,并实现一个 printf 函数来输出结果。
006:函数声明与 MCJIT 🚀
在本节课中,我们将学习如何实现用户自定义函数。我们将创建一个 main 函数,并将其作为程序的入口点。此外,我们还将使用 MCJIT 编译器来运行我们的程序,并验证其功能。
概述
本节课的目标是实现第一个用户自定义函数,即 main 函数,并将其作为程序的入口点。我们将编译这个函数,并在其中声明一个值为 4 的变量,然后返回这个变量。最后,我们将通过 MCJIT 编译器运行程序,验证其正确性。
6.1:扩展词法分析器(Lexer)🔧
上一节我们完成了基础解析,本节中我们来看看如何扩展词法分析器以支持新的关键字和符号。
首先,我们需要在 token.py 中添加新的令牌类型。以下是需要添加的关键字和符号:
FN:用于声明函数。RETURN:用于返回语句。ARROW(->):用于函数类型声明。LBRACE({) 和RBRACE(}):用于代码块。
在 token.py 中,我们更新 TokenType 枚举和 keywords 字典。
# 在 TokenType 枚举中添加
class TokenType(Enum):
# ... 其他类型
FN = 'FN'
RETURN = 'RETURN'
ARROW = 'ARROW'
LBRACE = '{'
RBRACE = '}'
# 在 keywords 字典中添加映射
keywords = {
'fn': TokenType.FN,
'return': TokenType.RETURN,
# ... 其他关键字
}
接下来,在 lexer.py 中,我们需要处理新的符号,特别是箭头符号 ->。
首先,添加一个辅助函数来预读字符:
def _peek_char(self):
if self._read_position >= len(self.input):
return '\0'
return self.input[self._read_position]
然后,在 _next_token 方法中处理减号和箭头符号:
def _next_token(self):
# ... 处理其他字符的逻辑
elif ch == '-':
# 处理箭头 (->)
if self._peek_char() == '>':
ch = self._ch
self._read_char()
tok = Token(TokenType.ARROW, ch + self._ch)
else:
tok = Token(TokenType.MINUS, ch)
self._read_char()
return tok
# ... 处理左花括号和右花括号
elif ch == '{':
self._read_char()
return Token(TokenType.LBRACE, ch)
elif ch == '}':
self._read_char()
return Token(TokenType.RBRACE, ch)
# ... 其他逻辑
6.2:扩展抽象语法树(AST)🌳
现在我们的词法分析器可以识别新的令牌了,接下来需要定义对应的 AST 节点。
我们需要三个新的节点类:
BlockStatement:表示由花括号包裹的代码块。ReturnStatement:表示返回语句。FunctionStatement:表示函数声明。
在 ast.py 中定义这些类:
class BlockStatement(Statement):
def __init__(self, statements=None):
self.statements = statements if statements is not None else []
class ReturnStatement(Statement):
def __init__(self, return_value):
self.return_value = return_value
class FunctionStatement(Statement):
def __init__(self, name, parameters, return_type, body):
self.name = name
self.parameters = parameters # 目前是空列表
self.return_type = return_type
self.body = body # 一个 BlockStatement
6.3:扩展语法解析器(Parser)📝
有了 AST 节点后,我们需要在解析器中添加相应的解析逻辑。
首先,在 parser.py 中导入新的 AST 节点,并为标识符添加前缀解析函数:
from ast import BlockStatement, FunctionStatement, ReturnStatement
# 在 _prefix_parse_fns 字典中注册标识符的解析方法
self._prefix_parse_fns[TokenType.IDENT] = self._parse_identifier
def _parse_identifier(self):
return Identifier(self._cur_token.literal)
接着,在 _parse_statement 方法中添加对 FN 和 RETURN 令牌的处理:
def _parse_statement(self):
if self._cur_token.type == TokenType.FN:
return self._parse_function_statement()
elif self._cur_token.type == TokenType.RETURN:
return self._parse_return_statement()
# ... 处理其他语句类型
现在,我们来逐一实现这三个新的解析方法。首先是 _parse_block_statement:
def _parse_block_statement(self):
block = BlockStatement()
self._next_token() # 跳过 '{'
while not self._cur_token.type in (TokenType.RBRACE, TokenType.EOF):
stmt = self._parse_statement()
if stmt:
block.statements.append(stmt)
self._next_token()
return block
然后是 _parse_return_statement:
def _parse_return_statement(self):
stmt = ReturnStatement(None)
self._next_token() # 跳过 'return'
stmt.return_value = self._parse_expression(LOWEST)
if not self._expect_peek(TokenType.SEMICOLON):
return None
return stmt
最后是 _parse_function_statement,它稍微复杂一些:
def _parse_function_statement(self):
stmt = FunctionStatement(None, [], None, None)
if not self._expect_peek(TokenType.IDENT):
return None
stmt.name = Identifier(self._cur_token.literal)
if not self._expect_peek(TokenType.LPAREN):
return None
# TODO: 未来会在这里解析参数
if not self._expect_peek(TokenType.RPAREN):
return None
if not self._expect_peek(TokenType.ARROW):
return None
if not self._expect_peek(TokenType.IDENT): # 期望类型标识符,如 'int'
return None
stmt.return_type = self._cur_token.literal
if not self._expect_peek(TokenType.LBRACE):
return None
stmt.body = self._parse_block_statement()
return stmt
6.4:扩展编译器(Compiler)⚙️
解析器生成 AST 后,编译器需要将这些节点转换为 LLVM IR。
首先,在 compiler.py 的 compile 方法中添加对新节点的处理分支:
def compile(self, node):
method_name = f'_visit_{type(node).__name__}'
visitor = getattr(self, method_name, self._no_visit_method)
return visitor(node)
# 在编译方法的分支中添加
if isinstance(node, BlockStatement):
return self._visit_BlockStatement(node)
elif isinstance(node, ReturnStatement):
return self._visit_ReturnStatement(node)
elif isinstance(node, FunctionStatement):
return self._visit_FunctionStatement(node)
然后实现各个访问方法。首先是 BlockStatement,它很简单:
def _visit_BlockStatement(self, node):
for statement in node.statements:
self.compile(statement)
接着是 ReturnStatement:
def _visit_ReturnStatement(self, node):
return_val = self._resolve_value(node.return_value)
self.builder.ret(return_val)
最后是核心的 FunctionStatement:
def _visit_FunctionStatement(self, node):
func_name = node.name.value
return_type = self._type_map[node.return_type] # 例如 llvm.IntType(32)
param_types = [] # 目前没有参数
func_type = llvm.FunctionType(return_type, param_types, False)
function = llvm.Function(self.module, func_type, func_name)
# 创建基本块
entry_block = function.append_basic_block(f"{func_name}_entry")
old_builder = self.builder
self.builder = llvm.IRBuilder(entry_block)
# 保存旧环境并创建新作用域
old_env = self.env
self.env = Environment(parent=old_env)
self.env.define(func_name, function)
# 编译函数体
self.compile(node.body)
# 恢复旧环境
self.env = old_env
self.env.define(func_name, function) # 在全局作用域也定义该函数
self.builder = old_builder


此外,我们需要修复 Let 语句中的一个 bug,确保存储的是指针地址而不是值:
def _visit_LetStatement(self, node):
# ... 之前的分配和存储逻辑
# 修复:存储到指针,而不是值本身
self.builder.store(value, ptr) # 原来是 self.builder.store(value, value)
最后,更新 _visit_Program 方法,移除之前硬编码的 main 函数创建逻辑,使其直接编译程序中的语句列表。
6.5:使用 MCJIT 运行程序 🏃
现在,我们的编译器可以生成 LLVM IR 了。为了实际运行程序,我们将使用 MCJIT(运行时编译执行引擎)。
在 main.py 中,我们添加运行代码的逻辑:
import llvm
import ctypes
import time
# ... 之前的编译代码,生成 self.module (一个 llvm.Module)
if run_code:
llvm.initialize()
llvm.initialize_native_target()
llvm.initialize_native_asmprinter()
try:
llvm_ir_parsed = llvm.parse_assembly(str(self.module))
llvm_ir_parsed.verify()
except Exception as e:
print(f"IR 验证失败: {e}")
raise
target = llvm.Target.from_default_triple()
target_machine = target.create_target_machine()
engine = llvm.create_mcjit_compiler(llvm_ir_parsed, target_machine)
# 获取 main 函数地址
func_ptr = engine.get_function_address("main")
cfunc = ctypes.CFUNCTYPE(ctypes.c_int)(func_ptr)
# 计时并执行
start_time = time.time()
result = cfunc()
elapsed_time = time.time() - start_time
print(f"程序返回: {result}")
print(f"执行时间: {elapsed_time:.6f} 秒")
总结
本节课中我们一起学习了如何为我们的语言实现函数声明。我们扩展了词法分析器以识别 fn、return、->、{ 和 } 等新令牌。然后,我们定义了 BlockStatement、ReturnStatement 和 FunctionStatement 等 AST 节点,并在解析器中实现了相应的解析逻辑。接着,我们在编译器中将这些 AST 节点转换为正确的 LLVM IR,包括创建函数、管理作用域和生成返回指令。最后,我们使用 LLVM 的 MCJIT 引擎来编译并运行生成的 IR 代码,成功执行了第一个返回值为 4 的 main 函数。

目前,我们已经实现了函数声明、返回语句和代码块。在下一节课(P07)中,我们将实现变量重新赋值功能,使变量可以被修改。
007:变量重新赋值
在本节课中,我们将学习如何为我们的编程语言实现变量重新赋值功能。我们将从定义抽象语法树节点开始,逐步完成解析和编译过程,最终让程序能够计算并返回重新赋值后的结果。
概述
上一节我们实现了用户自定义函数和主函数。本节我们将实现一个更简单的功能:变量重新赋值。我们将编写一个程序,先初始化一个变量,然后对其进行重新赋值,并返回新值。
解析器与抽象语法树
首先,我们需要在抽象语法树中定义一个新的节点类型来表示赋值语句。
在 ast.py 文件中,我们添加一个名为 AssignStatement 的类。这个类包含两个属性:ident 表示变量标识符,right_value 表示赋值表达式。
class AssignStatement:
def __init__(self, ident, right_value):
self.ident = ident
self.right_value = right_value
def type(self):
return NodeType.ASSIGN_STATEMENT
def to_json(self):
return {
'type': self.type().value,
'ident': self.ident.to_json(),
'right_value': self.right_value.to_json()
}
接下来,我们需要在解析器中识别并解析这种新的语句。
在 parser.py 的 parse_statement 函数中,我们检查当前令牌是否为标识符,并且下一个令牌是否为等号。如果是,则调用 parse_assignment_statement 函数。
def parse_statement(self):
if self.current_token.type == TokenType.IDENT and self.peek_token.type == TokenType.EQ:
return self.parse_assignment_statement()
# ... 其他语句的解析逻辑
parse_assignment_statement 函数负责构建 AssignStatement 节点。它首先跳过标识符和等号令牌,然后解析等号右侧的表达式。


def parse_assignment_statement(self):
stmt = AssignStatement(ident=self.current_token)
self.next_token() # 跳过标识符
self.next_token() # 跳过程序员
stmt.right_value = self.parse_expression(Precedence.LOWEST)
self.next_token() # 为下一个语句做准备
return stmt
编译器实现
解析器完成后,我们需要在编译器中处理新的赋值语句节点。
在 compiler.py 中,我们首先在 compile_statement 函数中添加对 AssignStatement 的处理。
def compile_statement(self, node):
node_type = node.type()
if node_type == NodeType.ASSIGN_STATEMENT:
return self.visit_assign_statement(node)
# ... 其他语句的编译逻辑
然后,我们实现 visit_assign_statement 函数。这个函数的核心逻辑是:
- 获取要赋值的变量名。
- 编译右侧的表达式,得到新值。
- 在符号表中查找该变量的内存地址(指针)。
- 使用 LLVM 的
store指令将新值存入该内存地址。
def visit_assign_statement(self, node):
name = node.ident.value
value = self._resolve_value(node.right_value)
# 在符号表中查找变量
ptr = self.env.lookup(name)
if ptr is None:
self.errors.append(f"Undefined variable '{name}'")
return
# 生成存储指令:将新值存入变量地址
self.builder.store(value, ptr)
测试与验证
完成实现后,我们编写一个测试程序来验证功能。
def main():
let a = 4
a = a * 2
return a
运行编译器,生成的 LLVM IR 代码应包含加载原值、计算新值、存储新值并返回的指令。执行该程序,应返回结果 8。
我们可以进一步测试连续赋值,例如 a = a * 2 执行两次,最终应返回 16。
总结
本节课我们一起学习了如何为编程语言实现变量重新赋值功能。我们完成了从抽象语法树定义、解析器识别到编译器代码生成的全过程。现在,我们的语言已经能够处理变量值的更新。

下一节,我们将为语言添加条件语句和布尔类型,实现更复杂的程序逻辑。
008:条件语句与布尔值 🚦

在本节课中,我们将为我们的编程语言添加条件判断能力。我们将实现 if 语句、条件运算符(如 ==、<、>= 等)以及布尔字面量 true 和 false。课程结束时,我们将能够编译并运行包含条件逻辑的代码。

概述

我们将从扩展词法分析器开始,使其能识别新的关键字和运算符。接着,我们会更新抽象语法树(AST)和解析器,以构建条件语句的语法结构。最后,在编译器后端,我们将为这些新结构生成相应的 LLVM IR 代码,并实现布尔类型的全局常量。





1. 扩展词法分析器


首先,我们需要在 Token 类中添加新的词法单元类型,并在词法分析器中识别它们。



1.1 添加新的 Token 类型


在 token.py 文件中,我们需要添加以下新的词法单元类型:


- 比较运算符:
<、>、==、!=、<=、>= - 关键字:
if、else - 布尔字面量:
true、false



以下是需要添加到 TokenType 枚举类中的代码:


# 在 TokenType 枚举类中添加
LT = “LT” # <
GT = “GT” # >
EQ = “EQ” # ==
NEQ = “NEQ” # !=
LTE = “LTE” # <=
GTE = “GTE” # >=
IF = “IF” # if
ELSE = “ELSE” # else
TRUE = “TRUE” # true
FALSE = “FALSE” # false


1.2 更新关键字字典


在词法分析器中,我们需要将字符串关键字映射到对应的 TokenType。



# 在 lexer.py 的关键字字典中添加
keywords = {
“if”: TokenType.IF,
“else”: TokenType.ELSE,
“true”: TokenType.TRUE,
“false”: TokenType.FALSE,
# ... 其他已有关键字
}



1.3 识别比较运算符

在词法分析器的 _next_token 方法中,我们需要添加处理比较运算符的逻辑。这通常涉及查看下一个字符以区分单字符和双字符运算符(例如 = 和 ==)。


以下是处理 < 和 <= 的示例:


# 在 lexer.py 的 _next_token 方法中添加
elif self.current_char == ‘<’:
if self.peek() == ‘=’:
self.advance()
token = Token(TokenType.LTE, ‘<=‘)
else:
token = Token(TokenType.LT, ‘<’)



你需要为 >、= 和 ! 添加类似的逻辑,以处理 >、>=、== 和 !=。




2. 更新抽象语法树(AST)




接下来,我们需要定义两种新的 AST 节点:IfStatement 和 BooleanLiteral。



2.1 创建 IfStatement 节点



IfStatement 节点包含三个部分:条件表达式、条件为真时执行的代码块(consequence),以及可选的 else 代码块(alternative)。



# 在 ast.py 中添加
class IfStatement(ASTNode):
def __init__(self, condition, consequence, alternative=None):
self.condition = condition
self.consequence = consequence
self.alternative = alternative
self.node_type = “IfStatement”
def to_json(self):
return {
“node”: self.node_type,
“condition”: self.condition.to_json(),
“consequence”: self.consequence.to_json(),
“alternative”: self.alternative.to_json() if self.alternative else None
}



2.2 创建 BooleanLiteral 节点



BooleanLiteral 节点非常简单,它只存储一个布尔值。



# 在 ast.py 中添加
class BooleanLiteral(ASTNode):
def __init__(self, value):
self.value = value
self.node_type = “BooleanLiteral”
def to_json(self):
return {
“node”: self.node_type,
“value”: self.value
}




3. 更新解析器


现在,我们需要让解析器能够根据新的词法单元构建对应的 AST 节点。



3.1 添加运算符优先级

在解析器中,我们需要为比较运算符设置优先级,以确保表达式被正确解析。




# 在 parser.py 的 PRECEDENCE 字典中添加
PRECEDENCE = {
TokenType.EQ: 10, # ==
TokenType.NEQ: 10, # !=
TokenType.LT: 20, # <
TokenType.GT: 20, # >
TokenType.LTE: 20, # <=
TokenType.GTE: 20, # >=
# ... 其他已有优先级
}

3.2 解析前缀表达式



我们需要注册处理 if、true 和 false 前缀的函数。



# 在 parser.py 的 PREFIX_PARSERS 字典中添加
PREFIX_PARSERS = {
TokenType.IF: parse_if_statement,
TokenType.TRUE: parse_boolean,
TokenType.FALSE: parse_boolean,
# ... 其他已有解析器
}



3.3 实现 parse_if_statement 函数


这个函数负责解析 if 语句的完整结构。



def parse_if_statement(self):
self.advance() # 跳过 ‘if’ token
condition = self.parse_expression(PRECEDENCE[TokenType.LOWEST])
self.expect_peek(TokenType.LBRACE) # 期望 ‘{‘
consequence = self.parse_block_statement()
alternative = None
if self.peek_token.type == TokenType.ELSE:
self.advance() # 跳过 ‘else’ token
self.expect_peek(TokenType.LBRACE) # 期望 ‘{‘
alternative = self.parse_block_statement()
return IfStatement(condition, consequence, alternative)


3.4 实现 parse_boolean 函数

这个函数根据当前词法单元返回一个 BooleanLiteral 节点。



def parse_boolean(self):
value = self.current_token.type == TokenType.TRUE
return BooleanLiteral(value)



4. 更新编译器后端



最后,我们需要在编译器后端为新的 AST 节点生成 LLVM IR 代码。



4.1 添加布尔类型映射


首先,在编译器的类型映射中注册布尔类型。在 LLVM IR 中,布尔类型通常用 i1(1位整数)表示。



# 在 compiler.py 的 __init__ 方法中添加
self.type_map[“bool”] = ir.IntType(1)


4.2 初始化内置布尔常量


我们需要创建全局的 true 和 false 常量,并将它们添加到符号表中,使其成为语言中的保留关键字。


def _initialize_builtins(self):
bool_type = self.type_map[“bool”]
# 创建全局常量 true
true_var = ir.GlobalVariable(self.module, bool_type, name=“true”)
true_var.initializer = ir.Constant(bool_type, 1)
true_var.global_constant = True
# 创建全局常量 false
false_var = ir.GlobalVariable(self.module, bool_type, name=“false”)
false_var.initializer = ir.Constant(bool_type, 0)
false_var.global_constant = True
# 将 true 和 false 添加到符号表
self.env.define(“true”, true_var, bool_type)
self.env.define(“false”, false_var, bool_type)



确保在编译器初始化时调用 _initialize_builtins() 方法。



4.3 编译 BooleanLiteral 节点


当遇到布尔字面量时,我们直接从符号表中加载对应的全局常量。



def visit_boolean_literal(self, node):
# 根据节点值决定加载 true 还是 false 常量
var_name = “true” if node.value else “false”
var = self.env.lookup(var_name)
return self.builder.load(var, name=var_name)


4.4 编译比较运算符


我们需要为整数和浮点数实现比较运算符的代码生成。这通常使用 LLVM 的 icmp(整数比较)和 fcmp(浮点数比较)指令。


以下是整数相等比较 == 的示例:


def visit_infix_expression(self, node):
left = self.visit(node.left)
right = self.visit(node.right)
if node.operator == “==”:
if isinstance(left.type, ir.IntType) and isinstance(right.type, ir.IntType):
# 整数比较
return self.builder.icmp_signed(‘==’, left, right, name=“cmptmp”)
elif isinstance(left.type, ir.FloatType) and isinstance(right.type, ir.FloatType):
# 浮点数比较
return self.builder.fcmp_ordered(‘==’, left, right, name=“fcmp”)
# … 处理其他运算符 <, >, !=, <=, >=

你需要为每个比较运算符(<、>、!=、<=、>=)实现类似的逻辑。


4.5 编译 IfStatement 节点


这是最核心的部分。我们使用 LLVM 的 if_then 或 if_else 构建器方法来创建条件分支。


def visit_if_statement(self, node):
condition = self.resolve_value(node.condition) # 计算条件表达式的值
bool_type = self.type_map[“bool”]
# 确保条件结果是布尔类型(i1)
if condition.type != bool_type:
condition = self.builder.icmp_signed(‘!=’, condition, ir.Constant(condition.type, 0))
if node.alternative is None:
# 只有 if,没有 else
with self.builder.if_then(condition):
self.visit(node.consequence)
else:
# 有 if 和 else
with self.builder.if_else(condition) as (then, otherwise):
with then:
self.visit(node.consequence)
with otherwise:
self.visit(node.alternative)

resolve_value 方法用于确保表达式的结果是一个可以用于条件判断的值。
5. 测试与验证



完成所有代码后,我们可以编写一个测试程序来验证功能。

# test.lang
let a = 5;
if (a == 5) {
a = 69;
} else {
a = 420;
}
return a;



运行编译器,你应该得到正确的 LLVM IR 代码。最终程序应返回 69。如果将 let a = 5; 改为 let a = 1;,程序应返回 420。






总结
在本节课中,我们一起为我们的编程语言添加了核心的条件逻辑功能。我们:


- 扩展了词法分析器,使其能识别
if、else、true、false关键字以及各种比较运算符。 - 更新了 AST,定义了
IfStatement和BooleanLiteral节点。 - 增强了解析器,使其能够解析条件语句的语法结构。
- 实现了编译器后端,为条件语句和布尔值生成正确的 LLVM IR 代码,包括创建全局布尔常量和处理条件分支。

现在,我们的语言已经具备了基本的流程控制能力。在下一节课中,我们将探索函数调用,让我们的程序能够执行更复杂的操作。
009:函数调用 🧩

在本节课中,我们将学习如何为我们的编程语言实现函数调用功能。我们将创建一个新的抽象语法树节点来处理函数调用,并更新解析器和编译器来支持它。最终,我们将能够编译并运行一个简单的程序,其中主函数调用另一个函数并返回其结果。



概述

上一节我们实现了函数定义。本节中,我们来看看如何调用这些函数。我们将通过实现一个新的“调用表达式”AST节点,并在解析器中识别左括号(作为函数调用的标志来完成此功能。目前,我们暂不支持传递参数,这将是下一节的内容。

实现调用表达式 AST 节点



首先,我们需要在抽象语法树中定义一个新的节点类型来表示函数调用。


以下是创建 CallExpression 类的代码:




class CallExpression(Expression):
def __init__(self, function, arguments):
self.function = function # 一个标识符字面量,表示函数名
self.arguments = arguments # 参数表达式列表(目前为空)
self.type = NodeType.CALL_EXPRESSION
def to_json(self):
return {
"type": self.type.value,
"function": self.function.to_json(),
"arguments": [arg.to_json() for arg in self.arguments]
}


这个节点包含两个主要部分:要调用的函数名(一个标识符)和一个参数列表(目前是空列表)。


更新解析器以识别函数调用



接下来,我们需要修改解析器,使其在遇到标识符后的左括号(时,能将其解析为函数调用,而不是其他操作(比如乘法)。


以下是需要更新的解析器部分:



- 在优先级映射表中,为左括号
(令牌关联CALL优先级。 - 添加一个新的中缀解析函数
parse_call_expression。


首先,在优先级映射中添加条目:

# 在 parser 类的 __init__ 方法或类似位置
self.precedence_mapping = {
...
TokenType.LEFT_PAREN: Precedence.CALL,
}


然后,添加中缀解析函数:

def parse_call_expression(self, function):
"""解析函数调用表达式"""
# 创建调用表达式节点,目前参数列表为空
expr = CallExpression(function, [])
# 期望下一个令牌是右括号,目前不支持参数
if not self._expect_peek(TokenType.RIGHT_PAREN):
return None
return expr



最后,将这个函数注册到中缀解析函数映射中:


self.infix_parse_functions = {
...
TokenType.LEFT_PAREN: self.parse_call_expression,
}




现在,解析器就能将类似 test() 的结构识别为函数调用了。


更新编译器以生成调用代码


解析器生成AST后,编译器需要知道如何将 CallExpression 节点转换为LLVM IR代码。


以下是编译器中需要添加的 visit_call_expression 方法:

def visit_call_expression(self, node):
"""编译函数调用表达式"""
# 获取函数名
func_name = node.function.value
# 获取参数列表(目前为空)
args = node.arguments
# 从当前作用域中查找函数定义
func, return_type = self.env.lookup(func_name)
# 使用LLVM构建器生成函数调用指令
return_value = self.builder.call(func, [])
# 返回调用结果
return return_value, return_type


同时,需要在 resolve_value 方法中添加对 CALL_EXPRESSION 节点的处理,以调用上述访问方法。


测试函数调用功能



完成以上步骤后,我们可以编写一个简单的测试程序。


测试代码示例:
function test() {
return 69;
}
function main() {
return test(); // 调用 test 函数
}


运行编译器后,程序应成功执行并返回 69。这证明我们的函数调用机制已基本实现。

总结
本节课中我们一起学习了如何实现编程语言中的函数调用功能。我们创建了 CallExpression AST节点,更新了解析器来识别调用语法,并修改了编译器来生成调用其他函数所需的LLVM IR代码。目前,函数调用还不支持参数,这是我们接下来要解决的问题。

在下一节中,我们将为函数添加参数和实参传递的功能,使函数变得更加实用和强大。
010:函数参数与实参 🧩

在本节课中,我们将为我们的编程语言添加函数参数和实参功能。我们将学习如何解析函数声明中的参数列表,以及如何在函数调用时传递实参。通过本课,你将能够创建和使用带参数的函数。



概述 📋
上一节我们实现了基本的函数声明和调用。本节中,我们将扩展功能,使函数能够接收参数。我们将修改词法分析器、语法分析器和编译器,以支持类似 add(a: int, b: int) 这样的函数声明和 add(1, 2) 这样的调用。



词法分析器:添加逗号令牌



首先,我们需要在词法分析器中添加一个新的令牌类型:逗号(,)。这是分隔函数参数所必需的。



以下是需要修改的代码部分:


# 在 token.py 的 TokenType 枚举中添加逗号
class TokenType(Enum):
# ... 其他令牌 ...
COMMA = ','

接下来,在词法分析器的主循环中识别逗号字符:

# 在 lexer.py 的 `_next_token` 方法中添加对逗号的处理
elif self.current_char == ',':
tok = Token(TokenType.COMMA, self.current_char)
self.advance()
return tok


抽象语法树:创建函数参数节点
为了在AST中表示函数参数,我们需要创建一个新的节点类 FunctionParameterNode。



以下是该节点的定义:


# 在 ast.py 中定义 FunctionParameterNode 类
class FunctionParameterNode(ASTNode):
def __init__(self, name: str, value_type: str):
self.name = name
self.value_type = value_type
def _to_json(self) -> Dict:
return {
'node': 'FunctionParameterNode',
'name': self.name,
'value_type': self.value_type
}


然后,我们需要更新 FunctionStatementNode,使其 parameters 属性是 FunctionParameterNode 的列表,而不仅仅是标识符字符串。


语法分析器:解析函数参数列表



现在,我们需要在语法分析器中添加一个函数来解析函数声明中的参数列表。

以下是解析函数参数列表的步骤:


- 检查下一个令牌是否是右括号
),如果是,则返回空列表(表示无参数)。 - 跳过左括号
(。 - 解析第一个参数:获取标识符名称,期望一个冒号
:,然后获取类型标识符。 - 将第一个参数添加到列表中。
- 循环检查下一个令牌是否为逗号
,,如果是,则继续解析更多参数。 - 最后,期望一个右括号
)来结束参数列表。

核心解析函数 _parse_function_parameters 的伪代码如下:


def _parse_function_parameters(self) -> Optional[List[FunctionParameterNode]]:
params = []
# 如果下一个令牌是 ),则没有参数
if self._peek_token.type == TokenType.RPAREN:
self._next_token() # 跳过 )
return params
self._next_token() # 跳过 (
# 解析第一个参数
param_name = self.current_token.literal
self._expect_peek(TokenType.COLON)
self._next_token() # 跳过 :
param_type = self.current_token.literal
params.append(FunctionParameterNode(param_name, param_type))
# 循环解析后续参数
while self._peek_token.type == TokenType.COMMA:
self._next_token() # 跳过 ,
self._next_token() # 移动到下一个参数名
param_name = self.current_token.literal
self._expect_peek(TokenType.COLON)
self._next_token()
param_type = self.current_token.literal
params.append(FunctionParameterNode(param_name, param_type))
self._expect_peek(TokenType.RPAREN)
return params


语法分析器:解析调用表达式中的实参列表

函数调用时,我们需要解析传递给函数的实参列表。我们将创建一个通用的 _parse_expression_list 函数,它接受一个结束令牌(例如右括号 ))并返回一个表达式节点列表。


以下是该函数的实现思路:


- 检查下一个令牌是否是结束令牌,如果是,则跳过左括号并返回空列表。
- 否则,跳过左括号,解析第一个表达式。
- 循环检查下一个令牌是否为逗号
,,如果是,则继续解析更多表达式。 - 最后,确保下一个令牌是结束令牌。



这个函数可以用于函数调用、数组字面量等多种场景。

编译器:处理函数参数与实参


在编译器部分,我们需要更新几个访问者方法。


首先,在 visit_FunctionStatementNode 中:
- 获取每个参数的LLVM类型。
- 在函数入口块中,为每个参数分配内存(
builder.alloca)并将传入的值存储进去。 - 将每个参数的指针和类型添加到函数的新作用域环境中。




其次,在 visit_CallExpressionNode 中:
- 解析每个实参表达式,获取其值和LLVM类型。
- 将这些值收集到列表中,作为调用函数时的实参。

关键步骤是为函数参数创建内存空间并将其与环境关联:


# 在 visit_FunctionStatementNode 中
param_types = [self.type_map[p.value_type] for p in node.parameters]
param_names = [p.name for p in node.parameters]


# 为参数分配内存并存储初始值
param_pointers = []
for i, param_type in enumerate(param_types):
param_ptr = self.builder.alloca(param_type, name=param_names[i])
self.builder.store(function.args[i], param_ptr) # function 是当前的 llvm.Function
param_pointers.append(param_ptr)


# 将参数添加到新环境中
for (param_name, param_type), param_ptr in zip(zip(param_names, param_types), param_pointers):
self.env.define(param_name, (param_ptr, param_type))


测试与运行 🧪

完成所有修改后,我们可以编写一个测试程序:
fn add(a: int, b: int): int {
return a + b;
}


fn main(): int {
return add(1, 2);
}

运行编译器,应该能成功生成LLVM IR代码并输出结果 3。你也可以尝试创建更多带参数的函数,例如减法函数。
总结 🎉
本节课我们一起学习了如何为编程语言添加函数参数和实参功能。我们:
- 在词法分析器中添加了逗号令牌。
- 在AST中创建了
FunctionParameterNode来表示参数。 - 在语法分析器中实现了函数参数列表和调用实参列表的解析。
- 在编译器中处理了参数的存储、环境绑定以及实参的传递。

现在,我们的函数可以接收输入并基于参数进行计算了。下一节课,我们将为语言添加字符串类型和 print 函数,让程序能够输出信息。
011:字符串与 Printf 🧵
在本节课中,我们将学习如何为我们的编程语言添加字符串数据类型,并实现一个 printf 函数来使用和显示这些字符串。这个 printf 函数将非常类似于 C 语言中的实现,允许我们使用 %i 这样的格式说明符来将额外的参数(如整数)插入到输出字符串中。
我们的目标是让程序能够运行类似 printf("apples%i", add(2, 3)) 的代码,并最终在控制台输出 apples5。


概述
我们将从修改词法分析器(tokenizer)开始,添加对字符串字面量的识别。接着,更新语法分析器(parser)和抽象语法树(AST)以支持新的字符串节点。然后,在编译器(compiler)中实现字符串的底层表示,并最终集成 printf 内置函数。整个过程将涉及对多个源代码文件的修改。



修改词法分析器(token.py)
首先,我们需要在词法分析器中添加一个新的令牌类型来代表字符串。




在 token.py 文件中,找到定义令牌类型的枚举(TokenType),添加一个名为 STRING 的新类型。


class TokenType(Enum):
...
STRING = “STRING”



接下来,在关键字映射部分,添加两个新的关键字:string 和 void。void 类型是我们之前遗漏的,现在一并添加。


_KEYWORDS = {
...
“string”: TokenType.STRING,
“void”: TokenType.VOID
}


现在,我们需要修改词法分析器的主循环,使其能够识别由双引号包裹的字符串。在读取字符的循环中,当遇到双引号 " 时,我们调用一个新的辅助函数 __read_string 来处理字符串内容。

以下是 __read_string 函数的基本逻辑:



def __read_string(self):
position = self.position + 1 # 跳过起始的双引号
while True:
current_char = self.source[position] if position < len(self.source) else None
if current_char == ‘“’ or current_char is None: # 遇到结束的双引号或文件结尾
break
position += 1
# 提取字符串内容
string_literal = self.source[self.position + 1:position]
self.position = position + 1 # 跳过结束的双引号
return self._new_token(TokenType.STRING, string_literal)


更新抽象语法树(AST)和语法分析器(Parser)



上一节我们修改了词法分析器来识别字符串令牌。本节中,我们需要更新 AST 和语法分析器,使其能够构建代表字符串字面量的节点。


首先,在 AST 的节点类型枚举(NodeType)中添加一个新的字面量类型 STRING_LITERAL。

class NodeType(Enum):
...
STRING_LITERAL = “STRING_LITERAL”


然后,创建一个新的 StringLiteral 节点类,它继承自 Literal 基类。其 value 属性将是一个 Python 字符串。


@dataclass
class StringLiteral(Literal):
value: str
def __post_init__(self):
self.node_type = NodeType.STRING_LITERAL


接下来,在语法分析器(parser.py)中,我们需要为 STRING 令牌注册一个前缀解析函数。在解析器的 __init__ 方法中,找到设置前缀解析函数的字典,添加如下条目:


self._prefix_parse_functions = {
...
TokenType.STRING: self._parse_string_literal,
}


最后,实现 _parse_string_literal 函数。这个函数非常简单,它直接使用当前令牌的字面值创建一个 StringLiteral 节点。


def _parse_string_literal(self):
return StringLiteral(value=self.current_token.literal)


修改编译器(Compiler)以支持字符串类型


现在我们已经能够在语法层面处理字符串了,接下来需要在编译器后端为其生成 LLVM IR 代码。

首先,在编译器(compiler.py)中导入新创建的 StringLiteral 节点。然后,我们需要更新类型映射(type_map),为 string 类型指定其在 LLVM IR 中的表示。字符串将被表示为指向 8 位整数(即字符)的指针。

self.type_map = {
...
“string”: ir.PointerType(ir.IntType(8)), # i8*
“void”: ir.VoidType(),
}


接下来,我们需要实现一个函数,将源代码中的字符串字面量转换为 LLVM 中的全局常量。在编译器中添加一个 _convert_string 方法:

def _convert_string(self, string: str):
# 处理转义字符,例如将 \n 替换为实际的换行符,并添加 C 风格的 null 终止符 ‘\0’
formatted_str = string.replace(‘\\n’, ‘\n’) + ‘\0’
# 创建 LLVM 数组常量
const_array = ir.Constant(
ir.ArrayType(ir.IntType(8), len(formatted_str)),
bytearray(formatted_str.encode(‘utf-8’))
)
# 创建全局变量来存储这个字符串常量
global_var = ir.GlobalVariable(self.module, const_array.type, name=f”str_{self._get_unique_id()}”)
global_var.linkage = ‘internal’
global_var.global_constant = True
global_var.initializer = const_array
# 返回全局变量的引用及其类型
return global_var, const_array.type

在编译表达式的 _resolve_value 方法中,添加对 STRING_LITERAL 节点的处理分支,调用上述的 _convert_string 方法。


def _resolve_value(self, node):
if node.node_type == NodeType.STRING_LITERAL:
string_var, string_type = self._convert_string(node.value)
return string_var, string_type
...
实现 Printf 内置函数


字符串已经可以存储在内存中了,现在我们需要一个方法来输出它们。我们将实现一个类似 C 语言的 printf 函数。


首先,在编译器的初始化方法中,我们需要声明这个内置函数,让编译器知道它的存在。添加一个 _init_print 方法:

def _init_print(self):
# 定义函数类型:返回 int,接受一个字符串指针和可变数量的参数
fn_type = ir.FunctionType(ir.IntType(32), [ir.PointerType(ir.IntType(8))], var_arg=True)
# 在模块中创建函数声明
printf_func = ir.Function(self.module, fn_type, name=“printf”)
# 在符号表中记录这个函数,将其返回类型标记为整数
self.env.define(“printf”, printf_func, self.type_map[“int”])

然后,我们需要处理对 printf 的函数调用。在访问函数调用表达式(visit_call_expression)的方法中,添加对名为 “printf” 的函数的特殊处理:

def visit_call_expression(self, node):
callee_name = node.callee.name
if callee_name == “printf”:
# 处理 printf 调用
args_ir = []
for arg in node.arguments:
arg_val, _ = self._resolve_value(arg)
args_ir.append(arg_val)
# 调用内置的 printf 处理逻辑
return self._builtin_printf(args_ir)
else:
# 处理普通函数调用
...


_builtin_printf 方法负责生成调用 LLVM printf 函数的实际指令。由于 LLVM 的 printf 需要格式字符串和参数,我们需要将第一个参数(格式字符串)进行适当的位转换(bitcast),并确保后续参数类型正确。
def _builtin_printf(self, parameters):
printf_func = self.env.get(“printf”) # 从环境中获取 printf 函数引用
# 假设第一个参数是已经转换好的字符串全局变量指针
format_str_ptr = parameters[0]
# 确保它是指向 i8 的指针类型
if not isinstance(format_str_ptr.type, ir.PointerType) or format_str_ptr.type.pointee != ir.IntType(8):
format_str_ptr = self.builder.bitcast(format_str_ptr, ir.PointerType(ir.IntType(8)))
# 构建函数调用指令
call_args = [format_str_ptr] + parameters[1:]
return self.builder.call(printf_func, call_args)

测试与运行

所有代码修改完成后,我们可以进行测试。创建一个测试文件 test.lang,内容如下:
printf(“apples%i”, add(2, 3));

在终端中,运行你的语言主程序(例如 python main.py test.lang)。如果一切顺利,控制台应该会输出:
apples5


你可以尝试修改参数,例如 printf(“apples%i”, add(10, 2)),输出应该变为 apples12。


总结

本节课中我们一起学习了如何为自创的编程语言添加字符串支持和一个基本的 printf 输出函数。我们完成了以下关键步骤:

- 词法分析:扩展了词法分析器以识别字符串字面量令牌。
- 语法分析:在 AST 中添加了
StringLiteral节点,并更新语法分析器来解析它。 - 编译后端:在编译器中实现了字符串到 LLVM IR 全局常量的转换,并更新了类型系统。
- 内置函数:声明并实现了
printf函数,处理了可变参数和格式字符串。
目前我们的实现还比较基础,printf 主要支持整数格式说明符 %i。由于所使用的 LLVM 轻量级绑定库的限制,实现更复杂的格式(如浮点数)会较为繁琐。在后续的课程或使用其他后端(如 C#)时,可以更优雅地实现这些功能。

恭喜你完成了本阶段的学习!在下一节课中,我们将探索如何为语言添加更多的特性。如果在实现过程中遇到任何问题,欢迎在社区中讨论。完整的项目代码可以在附带的代码仓库中找到。
012:While 循环 🔄


在本节课中,我们将要学习如何为我们的编程语言实现 while 循环。循环是编程语言的核心控制结构之一,它允许我们重复执行一段代码,直到某个条件不再满足。我们将从定义新的语法开始,逐步完成词法分析、语法分析,并最终在 LLVM IR 层面生成对应的循环控制流。
概述


我们将编译一个简单的 while 循环程序。该程序初始化一个变量 a 为 0,只要 a 小于 10,就打印 a 的值并将其加 1。循环结束后,程序返回 a 的最终值。


int main() {
int a = 0;
while (a < 10) {
print(a);
a = a + 1;
}
return a;
}



上一节我们介绍了条件语句,本节中我们来看看如何实现循环结构。


步骤一:更新词法分析器(token.py)


首先,我们需要让词法分析器能够识别 while 关键字。


在 token.py 文件中,我们需要进行以下修改:



- 在
keywords列表中添加while。 - 在
keywords字典中将‘while’映射到TokenType.WHILE。 - 为了增加趣味性,我们还可以在
alt_keywords字典中添加一个缩写‘we’也映射到TokenType.WHILE。


以下是需要添加的代码:




# 在 keywords 列表中添加
keywords = [..., ‘while’]


# 在 keywords 字典中添加
keywords = {..., ‘while’: TokenType.WHILE}


# 在 alt_keywords 字典中添加
alt_keywords = {..., ‘we’: TokenType.WHILE}




步骤二:更新抽象语法树(ast.py)


接下来,我们需要在抽象语法树中定义 while 语句的节点结构。



在 ast.py 文件中,我们需要进行以下修改:



- 在
NodeType枚举中添加WHILE_STATEMENT。 - 创建一个
WhileStatement类,它包含两个属性:condition(条件表达式)和body(循环体,是一个语句块)。


以下是 WhileStatement 类的定义:

class WhileStatement(Node):
def __init__(self, condition: Node, body: BlockStatement):
self.condition = condition
self.body = body
def type(self) -> NodeType:
return NodeType.WHILE_STATEMENT
def debug_json(self) -> dict:
return {
‘type’: self.type().value,
‘condition’: self.condition.debug_json(),
‘body’: self.body.debug_json()
}



步骤三:更新语法分析器(parser.py)




现在,我们需要让语法分析器能够解析 while 语句。


在 parser.py 文件中,我们需要进行以下修改:



- 从
ast模块导入WhileStatement。 - 在
_parse_statement方法中,为TokenType.WHILE添加一个处理分支,调用_parse_while_statement方法。 - 实现
_parse_while_statement方法。该方法会:- 消耗掉
while关键字。 - 解析条件表达式。
- 期望并消耗一个左花括号
{。 - 解析循环体(一个语句块)。
- 返回一个
WhileStatement节点。
- 消耗掉

以下是 _parse_while_statement 方法的实现:


def _parse_while_statement(self) -> Optional[WhileStatement]:
# 当前 token 是 ‘while’,前进到下一个 token(条件表达式的开始)
self.advance()
# 解析条件表达式
condition = self._parse_expression()
if condition is None:
return None
# 期望一个左花括号 ‘{‘ 来开始循环体
if not self._expect_peek(TokenType.LBRACE):
return None
# 解析循环体(一个语句块)
body = self._parse_block_statement()
if body is None:
return None
return WhileStatement(condition, body)


步骤四:更新编译器(compiler.py)

最后,也是最关键的一步,我们需要在编译器中将 WhileStatement 节点编译成 LLVM IR 指令。



在 compiler.py 文件中,我们需要进行以下修改:


- 从
ast模块导入WhileStatement。 - 在
_compile方法的语句处理部分,为NodeType.WHILE_STATEMENT添加一个处理分支,调用_visit_while_statement方法。 - 实现
_visit_while_statement方法。该方法的核心逻辑是创建两个基本块(Basic Block)并设置条件跳转:- 循环入口块(while_loop_entry):如果条件为真,则跳转至此执行循环体。
- 循环出口块(while_loop_otherwise):如果条件为假,则跳转至此,结束循环。
- 在循环体执行完毕后,再次检查条件,以决定是跳回入口继续循环,还是跳出循环。




以下是 _visit_while_statement 方法的实现:



def _visit_while_statement(self, node: WhileStatement):
condition = node.condition
body = node.body
# 解析初始条件值
test_value = self._resolve_value(self._visit(condition))
# 创建循环入口和出口基本块
entry_block = self.builder.append_basic_block(f“while_loop_entry_{self._increment_counter()}”)
otherwise_block = self.builder.append_basic_block(f“while_loop_otherwise_{self._increment_counter()}”)
# 根据初始条件值进行条件跳转
self.builder.cbranch(test_value, entry_block, otherwise_block)
# 将构建器定位到循环入口块,开始编译循环体
self.builder.position_at_start(entry_block)
self._visit(body) # 编译循环体
# 循环体执行完毕后,再次计算条件值
test_value_after_body = self._resolve_value(self._visit(condition))
# 根据新的条件值决定是继续循环还是退出
self.builder.cbranch(test_value_after_body, entry_block, otherwise_block)
# 将构建器定位到循环出口块,继续编译后续代码
self.builder.position_at_start(otherwise_block)

测试与运行


完成以上所有步骤后,我们可以运行编译器来测试我们的 while 循环。


在终端中执行以下命令:
python main.py

如果一切正确,程序将输出从 0 到 9 的数字,并最终返回 10。这是因为当 a 等于 10 时,条件 a < 10 不再成立,循环终止。


总结

本节课中我们一起学习了如何为我们的编程语言实现 while 循环。我们回顾一下关键步骤:


- 词法分析:添加了
while关键字的识别。 - 语法定义:在 AST 中创建了
WhileStatement节点。 - 语法分析:实现了
while语句的解析逻辑。 - 代码生成:在编译器中将循环结构翻译成 LLVM IR 的控制流图,核心是使用
cbranch指令和多个基本块来实现条件跳转和循环。
while 循环的实现模式与 if 语句类似,但多了一个从循环体末尾跳回条件检查处的“回边”,从而形成了循环。掌握了这个模式,实现 do-while 循环也将是类似的挑战。

在下一节课中,我们将为循环添加 break 和 continue 关键字,并最终实现 for 循环,从而完善我们语言中的所有循环结构。
013:实现 For 循环与 Break 语句 🚀

在本节课中,我们将学习如何为我们的编程语言实现 for 循环以及 break 和 continue 语句。这些控制流语句是构建复杂程序逻辑的基础。
概述
我们将从一个简单的测试程序开始,目标是实现一个基本的 for 循环,并在特定条件下使用 break 语句提前退出循环。以下是我们的测试代码 test.lang:


fn main() {
let a = 0;
for (let i = 0; i < 10; i = i + 1) {
if (i == 5) {
break;
}
print(i);
a = i;
}
return a;
}


这个循环将从 0 迭代到 9,但当 i 等于 5 时,会执行 break 语句并提前退出循环,因此数字 5 不会被打印。最终,函数将返回变量 a 的值。


上一节我们实现了 while 循环,本节中我们来看看更复杂的 for 循环。

第一步:更新词法分析器 (token.py)

首先,我们需要在词法分析器中添加三个新的关键字对应的 Token 类型。

以下是需要添加的新 Token 类型:



# 在 TokenType 枚举类中添加
For = auto()
Break = auto()
Continue = auto()

接下来,我们需要将这些类型映射到实际的关键字字符串。



以下是需要更新的关键字映射字典:

# 在 keywords 字典中添加
'for': TokenType.For,
'break': TokenType.Break,
'continue': TokenType.Continue,


此外,为了增加趣味性,我们还可以添加一些“Gen Z”风格的替代关键字(可选)。


以下是可选的替代关键字映射:
# 在 gen_z_keywords 字典中添加(可选)
'yeet': TokenType.Break, # 表示跳出循环
'skip': TokenType.Continue, # 表示跳过本次迭代
'dab': TokenType.For, # 表示 for 循环


保存 token.py 文件,词法分析器的更新就完成了。

第二步:更新抽象语法树 (AST.py)


接下来,我们需要在抽象语法树中定义三种新的语句节点。


首先,在节点类型枚举中添加对应的类型。


以下是需要添加的新节点类型:

# 在 NodeType 枚举类中添加
BreakStatement = auto()
ContinueStatement = auto()
ForStatement = auto()

然后,在文件底部的语句节点区域,创建这三个新节点的类。


以下是 BreakStatement 节点的定义:


class BreakStatement(Node):
def __init__(self):
super().__init__(NodeType.BreakStatement)
def __repr__(self):
return self._to_json({
"type": "BreakStatement"
})

ContinueStatement 节点的结构与 BreakStatement 几乎相同。


以下是 ContinueStatement 节点的定义:

class ContinueStatement(Node):
def __init__(self):
super().__init__(NodeType.ContinueStatement)
def __repr__(self):
return self._to_json({
"type": "ContinueStatement"
})


ForStatement 节点相对复杂,它需要包含初始化声明、循环条件、每次迭代后的操作以及循环体。


以下是 ForStatement 节点的定义:

class ForStatement(Node):
def __init__(self, declaration, condition, action, body):
super().__init__(NodeType.ForStatement)
self.declaration = declaration # 变量声明,如 let i = 0
self.condition = condition # 条件表达式,如 i < 10
self.action = action # 每次迭代后执行的操作,如 i = i + 1
self.body = body # 循环体,一个 BlockStatement
def __repr__(self):
return self._to_json({
"declaration": self.declaration,
"condition": self.condition,
"action": self.action,
"body": self.body
})

保存 AST.py 文件,抽象语法树的更新就完成了。

第三步:更新语法分析器 (parser.py)



现在,我们需要在语法分析器中解析这些新语句。

首先,导入新定义的节点类。


以下是需要添加的导入语句:



from AST import BreakStatement, ContinueStatement, ForStatement

然后,在 parse_statement 方法的 match-case 块中,为新的关键字添加处理分支。


以下是 parse_statement 方法中需要添加的 case:

case TokenType.Break:
return self.parse_break_statement()
case TokenType.Continue:
return self.parse_continue_statement()
case TokenType.For:
return self.parse_for_statement()



接下来,我们需要实现这三个对应的解析函数。我们先从最简单的 break 和 continue 开始。

以下是 parse_break_statement 方法的实现:

def parse_break_statement(self):
"""解析 break 语句"""
self.advance() # 跳过 'break' 关键字
return BreakStatement()


parse_continue_statement 方法与之类似。


以下是 parse_continue_statement 方法的实现:

def parse_continue_statement(self):
"""解析 continue 语句"""
self.advance() # 跳过 'continue' 关键字
return ContinueStatement()


最后,实现最复杂的 parse_for_statement 方法。我们需要按照 for (初始化; 条件; 操作) { 循环体 } 的结构来解析。

以下是 parse_for_statement 方法的实现:


def parse_for_statement(self):
"""解析 for 循环语句"""
# 1. 创建 ForStatement 节点
for_stmt = ForStatement(None, None, None, None)
# 2. 跳过 'for' 关键字,并期望一个左括号 '('
self.advance()
if not self.expect_peek(TokenType.LeftParen):
return None
# 3. 解析初始化声明(一个 let 语句)
if not self.expect_peek(TokenType.Let):
return None
for_stmt.declaration = self.parse_let_statement()
# 4. 跳过声明后的分号
self.next_token() # 跳过 ';'
# 5. 解析条件表达式
for_stmt.condition = self.parse_expression(Precedence.LOWEST)
# 跳过条件后的分号
if not self.expect_peek(TokenType.Semicolon):
return None
self.next_token()
# 6. 解析迭代操作(一个赋值语句)
for_stmt.action = self.parse_assignment_statement()
# 7. 期望一个右括号 ')'
if not self.expect_peek(TokenType.RightParen):
return None
self.next_token()
# 8. 解析循环体(一个块语句)
for_stmt.body = self.parse_block_statement()
return for_stmt


注意:在解析过程中,我们使用 expect_peek 来预检查下一个 Token 的类型,使用 next_token 来消费(跳过)已知的 Token(如分号、括号)。


保存 parser.py 文件,语法分析器的更新就完成了。

第四步:更新编译器 (compiler.py)

最后,也是最关键的一步,我们需要在编译器中将 AST 节点转换为 LLVM IR 代码。
首先,导入新的节点类。


以下是需要添加的导入语句:


from AST import BreakStatement, ContinueStatement, ForStatement

为了处理 break 和 continue 的跳转,我们需要在编译器类中维护两个列表,分别记录当前循环的“跳出块”和“继续块”的引用。


以下是在编译器 __init__ 方法中添加的初始化代码:


self.break_points = [] # 存储 break 语句应跳转到的目标块(循环出口)
self.continues = [] # 存储 continue 语句应跳转到的目标块(循环入口)

然后,在 compile 方法的 match-case 语句块中,添加对新节点类型的处理。



以下是 compile 方法中需要添加的 case:


case NodeType.BreakStatement:
self.visit_break_statement(node)
case NodeType.ContinueStatement:
self.visit_continue_statement(node)
case NodeType.ForStatement:
self.visit_for_statement(node)

现在,我们来实现这三个访问方法。break 和 continue 的实现很简单,它们只是跳转到之前记录好的目标块。


以下是 visit_break_statement 和 visit_continue_statement 方法的实现:


def visit_break_statement(self, node):
"""编译 break 语句:跳转到最近的 break_points 中的块"""
target_block = self.break_points[-1]
self.builder.branch(target_block)


def visit_continue_statement(self, node):
"""编译 continue 语句:跳转到最近的 continues 中的块"""
target_block = self.continues[-1]
self.builder.branch(target_block)


visit_for_statement 方法是核心。我们需要为循环创建入口块和出口块,设置环境,并按顺序编译初始化、条件判断、循环体和迭代操作。

以下是 visit_for_statement 方法的实现:



def visit_for_statement(self, node):
"""编译 for 循环语句"""
# 1. 保存当前环境,并为循环体创建新的子环境
previous_env = self.env
self.env = Environment(previous_env)
# 2. 编译初始化声明(在父环境中)
self.env = previous_env
self.compile(node.declaration)
self.env = Environment(previous_env) # 切换回循环体环境
# 3. 创建循环的入口块和出口块
loop_entry_block = self.builder.append_basic_block(f"for_loop_entry_{self.counter()}")
loop_exit_block = self.builder.append_basic_block(f"for_loop_exit_{self.counter()}")
# 4. 将出口块和入口块分别加入 break_points 和 continues 列表
self.break_points.append(loop_exit_block)
self.continues.append(loop_entry_block)
# 5. 生成一个无条件跳转,跳转到循环入口
self.builder.branch(loop_entry_block)
# 6. 将构建器定位到循环入口块,开始编译循环体
self.builder.position_at_start(loop_entry_block)
self.compile(node.body) # 编译循环体
self.compile(node.action) # 编译迭代操作(如 i = i + 1)
# 7. 计算条件表达式的值,并根据结果跳转
test_value = self.resolve_value(node.condition)
self.builder.cbranch(test_value, loop_entry_block, loop_exit_block)
# 8. 将构建器定位到循环出口块
self.builder.position_at_start(loop_exit_block)
# 9. 循环结束,从列表中移除当前循环的 break 和 continue 目标
self.break_points.pop()
self.continues.pop()
# 10. 恢复之前的环境
self.env = previous_env


注意:在实现 continue 语句时,视频作者遇到了一个与 LLVM 版本相关的问题,导致 continue 功能未能完全正常工作。他建议观众如果发现问题,可以到他的 Discord 社区讨论。本教程保留了 continue 的基本框架,但实际运行时可能需要调试。


保存 compiler.py 文件,编译器的更新就完成了。


测试运行


现在,我们可以运行主程序来测试我们的实现了。

在命令行中执行:
python main.py

如果一切正确,程序将解析 test.lang 文件,生成 LLVM IR,并执行。输出应该打印数字 0 到 4,然后在 i == 5 时触发 break 语句,跳出循环。最终函数返回值应为 4。
总结
本节课中我们一起学习了如何为自研编程语言实现 for 循环、break 和 continue 语句。我们逐步完成了以下工作:
- 扩展词法分析器:添加了
for、break、continue三个关键字的 Token。 - 扩展抽象语法树:定义了对应的语句节点,特别是包含了多个子节点的
ForStatement。 - 扩展语法分析器:实现了对新语句结构的解析逻辑。
- 扩展编译器:这是最复杂的一步,我们学习了如何用 LLVM IR 构建循环结构,包括创建基本块、管理跳转目标以及处理嵌套环境。

通过本节课,我们的语言拥有了更强大的流程控制能力。下一节课,我们将实现前缀操作符(如 - 负号、! 逻辑非),让我们的表达式系统更加完善。
014:前缀与赋值运算符 🧮

在本节课中,我们将学习如何为我们的编程语言添加两种新的运算符:前缀运算符(如 - 和 !)和赋值运算符(如 +=、-=、*=、/=)。我们将从词法分析开始,逐步修改语法分析器和编译器,最终让这些新功能能够正常工作。
概述 📋

本节课的目标是实现两种类型的运算符。第一部分是前缀运算符,例如 -10 中的负号和 !false 中的逻辑非运算符。第二部分是复合赋值运算符,它们允许我们以更简洁的方式修改变量的值,例如 x += 5。
1. 更新词法分析器(Lexer)🔤
首先,我们需要在词法分析器中识别新的运算符符号。这涉及到修改 Token 类型和 Lexer 的字符处理逻辑。

1.1 添加新的 Token 类型

在 token.py 文件的 TokenType 枚举中,我们需要添加新的符号。
代码示例:
# 在符号区域添加
BANG = ‘BANG‘ # 对应 ‘!‘

# 在赋值运算符区域添加
PLUS_EQ = ‘PLUS_EQ‘ # ‘+=‘
MINUS_EQ = ‘MINUS_EQ‘ # ‘-=‘
ASTERISK_EQ = ‘ASTERISK_EQ‘ # ‘*=‘
SLASH_EQ = ‘SLASH_EQ‘ # ‘/=‘

1.2 修改 Lexer 处理逻辑
接下来,在 lexer.py 中,我们需要为这些新符号添加对应的 case 处理语句。


以下是处理 += 运算符的示例逻辑:
代码示例:
case ‘+‘:
if self.peek_char() == ‘=‘:
# 保存当前字符 ‘+‘
char = self.current_char
self.advance() # 前进到 ‘=‘
# 创建 PLUS_EQ 类型的 Token,字面量为 “+=”
token = Token(PLUS_EQ, char + self.current_char)
else:
token = Token(PLUS, self.current_char)
self.advance()
return token


我们需要为 -、*、/ 和 ! 实现类似的逻辑。对于 !,它没有对应的赋值形式,因此处理更简单。


2. 更新抽象语法树(AST)🌳
为了让解析器能构建新的语法结构,我们需要定义新的 AST 节点。
2.1 创建前缀表达式节点


前缀表达式节点需要记录运算符和它作用的右侧表达式。

代码示例:
class PrefixExpression(Node):
def __init__(self, operator, right):
self.operator = operator # 例如 ‘-‘ 或 ‘!‘
self.right = right # 一个表达式节点


2.2 修改赋值语句节点
原来的赋值语句节点只包含变量名和值。现在,我们需要增加一个 operator 字段来支持 += 这类复合赋值。


代码示例:
class AssignStatement(Node):
def __init__(self, name, operator, value):
self.name = name # 变量名
self.operator = operator # 例如 ‘=‘, ‘+=‘
self.value = value # 右侧的表达式


3. 更新语法解析器(Parser)📝
解析器需要知道如何将新的 Token 序列转换成我们刚刚定义的 AST 节点。


3.1 注册前缀解析函数


我们需要告诉解析器,当遇到 - 或 ! 时,应该调用一个特定的函数来解析前缀表达式。
代码示例:
# 在解析器的初始化函数中
self.prefix_parse_functions = {
...
TokenType.MINUS: self.parse_prefix_expression,
TokenType.BANG: self.parse_prefix_expression,
}
3.2 实现前缀表达式解析函数

这个函数负责消费运算符 Token,并解析其后的表达式作为右操作数。

代码示例:
def parse_prefix_expression(self):
# 当前 Token 是 ‘-‘ 或 ‘!‘
operator = self.current_token.literal
self.advance() # 消费掉运算符
# 解析右侧的表达式
right = self.parse_expression(PREFIX)
return PrefixExpression(operator, right)

3.3 处理赋值语句中的运算符
在解析赋值语句时,我们需要检查并保存运算符。

代码示例:
def parse_assignment_statement(self, name):
# name 是左侧的标识符
# 当前 Token 可能是 ‘=‘, ‘+=‘ 等
operator = self.current_token.literal
self.advance() # 消费掉运算符 Token
# 解析右侧的值
value = self.parse_expression(LOWEST)
return AssignStatement(name, operator, value)
为了判断下一个 Token 是否是赋值运算符,我们可以创建一个辅助函数。

代码示例:
def peek_token_is_assignment(self):
assignment_ops = [TokenType.EQ, TokenType.PLUS_EQ, TokenType.MINUS_EQ,
TokenType.ASTERISK_EQ, TokenType.SLASH_EQ]
return self.peek_token.type in assignment_ops
4. 更新编译器(Compiler)⚙️

最后,我们需要教编译器如何为这些新的 AST 节点生成 LLVM IR 代码。
4.1 编译前缀表达式
对于前缀表达式,我们需要根据运算符生成不同的 IR 指令。

代码示例:
def visit_prefix_expression(self, node):
operator = node.operator
right_value, right_type = self.resolve_value(node.right)
if right_type == ir.FloatType():
# 处理浮点数
if operator == ‘-‘:
# 生成 ‘right_value * -1.0‘ 的 IR
neg_one = ir.Constant(ir.FloatType(), -1.0)
value = self.builder.fmul(right_value, neg_one)
return value, ir.FloatType()
elif operator == ‘!‘:
# 逻辑非:生成比较指令,结果为 0 (false) 或 1 (true)
zero = ir.Constant(ir.IntType(1), 0)
value = self.builder.icmp_unsigned(‘==‘, right_value, zero)
return value, ir.IntType(1)
# 类似地处理整数类型...
4.2 编译复合赋值语句

这是最复杂的部分。我们需要:
- 加载变量的当前值。
- 计算右侧表达式的值。
- 根据运算符(如
+=)将两者进行计算。 - 将结果存回变量。

代码示例:
def visit_assign_statement(self, node):
var_name = node.name
operator = node.operator
right_value, right_type = self.resolve_value(node.value)
# 1. 查找变量指针并加载其当前值
var_ptr = self.environment.lookup(var_name)
orig_value = self.builder.load(var_ptr)
# 2. 处理类型转换(例如 int 与 float 运算)
if isinstance(orig_value.type, ir.IntType) and right_type == ir.FloatType():
orig_value = self.builder.sitofp(orig_value, ir.FloatType())
# ... 其他类型转换
# 3. 根据运算符进行计算
if operator == ‘=‘:
result = right_value
elif operator == ‘+=‘:
if isinstance(orig_value.type, ir.IntType):
result = self.builder.add(orig_value, right_value)
else:
result = self.builder.fadd(orig_value, right_value)
# ... 处理 ‘-=‘, ‘*=‘, ‘/=‘
# 4. 将结果存储回变量
self.builder.store(result, var_ptr)


测试与运行 🧪


完成所有修改后,我们可以创建一个测试文件来验证功能。

测试示例 (test.line):
x = 10
x += 5
print(x) # 应输出 15

y = -20
print(y) # 应输出 -20

flag = !false
print(flag) # 应输出 true

运行主程序:
python main.py test.line


如果一切正常,控制台应该能正确输出计算结果。


总结 🎉



在本节课中,我们一起为我们的编程语言添加了强大的新功能。
- 前缀运算符:我们实现了
-(负号)和!(逻辑非)运算符,它们作用于单个表达式。 - 赋值运算符:我们实现了
+=、-=、*=、/=这些复合赋值运算符,它们可以更高效地修改变量值。

实现过程贯穿了编译器的所有主要阶段:从词法分析识别新符号,到语法分析构建新的 AST 结构,最后在代码生成阶段生成对应的 LLVM IR 指令。下一节课,我们将探讨后缀运算符(如 i++ 和 i--),这将在循环等场景中非常有用。
015:后缀运算符 🧮

在本节课中,我们将学习如何为我们的编程语言添加后缀运算符。上一节我们介绍了前缀运算符,它们位于表达式之前。本节中,我们将实现位于表达式之后的后缀运算符,例如 ++ 和 --。我们将遵循一个清晰的模板,你可以根据需要扩展其他运算符。

概述 📋

我们将通过修改词法分析器、抽象语法树、解析器和编译器来支持后缀运算符。核心步骤包括定义新的词法符号、创建新的AST节点类型、更新解析逻辑以及生成相应的LLVM IR代码。



1. 更新词法分析器(Tokenizer)



首先,我们需要在词法分析器中识别 ++ 和 -- 这两个新的运算符符号。



以下是需要在 tokenizer.py 文件的符号定义部分添加的内容:



# 后缀运算符符号
‘++‘,
‘--‘,


同时,为了代码清晰,我们可以将前缀运算符符号(如 !)整理到一个独立的区块中。



2. 处理新词法符号


接下来,在词法分析器的 case 语句中,我们需要添加对连续加号 ++ 和连续减号 -- 的处理逻辑。


处理 ++ 的逻辑如下:
case ‘+‘:
if self.peek() == ‘+‘:
self.advance()
token = Token(TokenType.PLUS_PLUS, ‘++‘)
else:
token = Token(TokenType.PLUS, ‘+‘)


处理 -- 的逻辑与之类似:
case ‘-‘:
if self.peek() == ‘-‘:
self.advance()
token = Token(TokenType.MINUS_MINUS, ‘--‘)
else:
token = Token(TokenType.MINUS, ‘-‘)



这样,词法分析器就能正确生成 PLUS_PLUS 和 MINUS_MINUS 类型的词法符号了。



3. 更新抽象语法树(AST)


现在,我们需要在AST中定义一个新的节点类型来表示后缀表达式。



在 ast.py 文件的表达式类型枚举中,添加 POSTFIX_EXPRESSION。


然后,在表达式节点类区域,创建后缀表达式节点类:
@dataclass
class PostfixExpression:
left: ‘Expression‘
operator: str
这个节点包含左表达式节点和所使用的运算符字符串,其结构类似于前缀表达式,但顺序相反。



4. 更新解析器(Parser)

解析器需要能够将词法符号流解析成我们刚定义的后缀表达式AST节点。


首先,在解析器的运算符优先级映射字典 precedences 中,为 PLUS_PLUS 和 MINUS_MINUS 分配相同的优先级索引。



然后,在解析中缀表达式的函数映射字典 infix_parse_fns 中,将这两个词法符号映射到同一个解析函数 parse_postfix_expression。


接下来,实现这个解析函数:
def parse_postfix_expression(self, left: Expression) -> Expression:
"""解析后缀表达式,例如 `a++`"""
node = PostfixExpression(left=left, operator=self.current_token.literal)
self.next_token() # 消耗掉运算符词法符号
return node
这个函数接收当前已解析的左表达式,然后消耗掉后缀运算符词法符号,并返回一个新的 PostfixExpression 节点。



最后,我们需要更新 for 循环语句的解析,使其中的“动作”部分允许使用通用的表达式(而不仅仅是赋值语句),以便能使用 i++ 这样的后缀表达式。


5. 更新编译器(Compiler)



最后一步是在编译器中实现后缀表达式节点的访问方法,以生成LLVM IR代码。

首先,在编译器文件中导入新的 PostfixExpression 节点类。



然后,在表达式访问方法区域,添加 visit_postfix_expression 函数:
def visit_postfix_expression(self, node: PostfixExpression):
# 1. 确保左节点是一个标识符
left_node = node.left
operator = node.operator
# 2. 在符号表中查找该标识符是否已声明
var_ptr = self.env.lookup(left_node.value)
if var_ptr is None:
self.error(f"标识符 ‘{left_node.value}‘ 在后缀表达式中使用前未声明。")
return
# 3. 加载标识符的当前值
original_value = self.builder.load(var_ptr)
# 4. 根据运算符(++ 或 --)和类型(整数或浮点数)计算新值
new_value = None
match operator:
case ‘++‘:
if isinstance(original_value.type, llvm.ir.IntType):
one = llvm.ir.Constant(llvm.ir.IntType(32), 1)
new_value = self.builder.add(original_value, one)
elif isinstance(original_value.type, llvm.ir.FloatType):
one = llvm.ir.Constant(llvm.ir.FloatType(), 1.0)
new_value = self.builder.fadd(original_value, one)
case ‘--‘:
if isinstance(original_value.type, llvm.ir.IntType):
one = llvm.ir.Constant(llvm.ir.IntType(32), 1)
new_value = self.builder.sub(original_value, one)
elif isinstance(original_value.type, llvm.ir.FloatType):
one = llvm.ir.Constant(llvm.ir.FloatType(), 1.0)
new_value = self.builder.fsub(original_value, one)
# 5. 将新值存回变量
self.builder.store(new_value, var_ptr)
这个函数执行以下操作:检查变量是否存在、加载当前值、根据运算符进行增量或减量计算、最后将结果存回内存。


最后,在编译器的 compile 方法中,为 POSTFIX_EXPRESSION 节点类型添加一个新的 case,调用上述访问函数。


测试 🧪


完成所有修改后,我们可以创建一个测试文件 test.lang 来验证功能:
for (i = 0; i < 5; i++) {
print(i);
}
运行编译器,如果循环正确打印了 0 到 4 的数字,则说明后缀运算符 ++ 工作正常。


总结 🎉


本节课中我们一起学习了如何为自制的编程语言添加后缀运算符。我们完成了从词法分析到代码生成的完整流程:

- 在词法分析器中添加了对
++和--符号的识别。 - 在AST中定义了
PostfixExpression节点。 - 在解析器中实现了后缀表达式的解析逻辑,并更新了
for循环的解析以兼容表达式。 - 在编译器中实现了后缀表达式的IR代码生成,包括变量的加载、运算和回存。

现在,我们的语言已经支持了像 i++ 和 i-- 这样的后缀操作。在下一节课中,我们将探索如何实现文件导入功能,使我们的语言能够模块化地组织代码。
016:文件导入 📂



在本节课中,我们将为我们的编程语言实现一个基础的文件导入功能。我们将学习如何解析 import 关键字,读取外部文件,并将其中的代码编译到当前环境中。




概述



上一节我们完成了语言的核心功能。本节中,我们将实现类似 C 语言风格的基础文件导入功能。我们将使用 import 关键字,后跟一个包含文件路径的字符串,来导入其他文件中的函数和变量。


1. 更新词法分析器(Token)



首先,我们需要在词法分析器中添加对 import 关键字的支持。


在 token.py 文件的 TokenType 枚举中,添加 IMPORT 类型:
class TokenType(Enum):
...
IMPORT = 'IMPORT'




接着,在关键字映射字典中,将 "import" 映射到 TokenType.IMPORT。同时,为了增加趣味性,我们也可以为其添加一个别名 "gib"。
KEYWORDS = {
"import": TokenType.IMPORT,
"gib": TokenType.IMPORT,
...
}



2. 更新抽象语法树(AST)



接下来,我们需要定义一种新的 AST 节点来表示导入语句。




在 ast.py 文件的节点类型枚举中,添加 ImportStatement:
class NodeType(Enum):
...
IMPORT_STATEMENT = 'IMPORT_STATEMENT'




然后,在语句节点区域的下方,创建 ImportStatement 类。这个节点只需要一个属性:file_path。
@dataclass
class ImportStatement(Node):
file_path: str
def __post_init__(self):
self.node_type = NodeType.IMPORT_STATEMENT



3. 更新语法分析器(Parser)




现在,我们需要让语法分析器能够识别并解析 import 语句。


首先,在 parser.py 文件顶部导入新创建的 ImportStatement 节点。



然后,在 parse_statement 函数中添加一个新的条件分支,用于处理 TokenType.IMPORT 令牌:
def parse_statement(self):
if self.current_token.type == TokenType.IMPORT:
return self._parse_import_statement()
...



最后,在语句解析方法块的底部(例如在 _parse_for_statement 之后),实现 _parse_import_statement 函数:
def _parse_import_statement(self) -> Optional[ImportStatement]:
# 确保下一个令牌是字符串(文件路径)
if not self._expect_peek(TokenType.STRING):
return None
stmt = ImportStatement(file_path=self.current_token.literal)
# 确保语句以分号结尾
if not self._expect_peek(TokenType.SEMICOLON):
return None
return stmt



4. 更新编译器(Compiler)



最后,也是最关键的一步,是在编译器中实现导入语句的逻辑,使其能够读取外部文件并编译其中的代码。




首先,在 compiler.py 文件顶部进行必要的导入:
import os
from lexer import Lexer
from parser import Parser
from ast import ImportStatement



在编译器的 __init__ 方法中,添加一个字典来缓存已解析的模块,避免重复导入:
def __init__(self):
...
self.global_parsed_pallets: Dict[str, Program] = {}



在 compile 方法的 match 语句中,为 NodeType.IMPORT_STATEMENT 添加一个新的分支,调用 _visit_import_statement 函数。




在 _visit_for_statement 函数下方,实现 _visit_import_statement 函数:
def _visit_import_statement(self, node: ImportStatement) -> None:
file_path = node.file_path
# 检查是否已导入过该文件
if self.global_parsed_pallets.get(file_path) is not None:
print(f"Warning: File '{file_path}' is already imported.")
return
# 构建完整的文件路径并读取内容
full_path = f"tests/{file_path}"
try:
with open(full_path, 'r') as f:
pallet_code = f.read()
except FileNotFoundError:
print(f"Error: Could not find file '{full_path}'")
exit(1)
# 对导入的代码进行词法分析和语法分析
l = Lexer(source=pallet_code)
p = Parser(lexer=l)
program = p.parse_program()
# 检查分析过程中是否有错误
if len(p.errors) > 0:
print(f"Error parsing imported pallet '{file_path}':")
for error in p.errors:
print(error)
exit(1)
# 编译导入的代码(函数和变量将注册到当前作用域)
self.compile(program)
# 将解析后的 AST 缓存起来
self.global_parsed_pallets[file_path] = program



测试导入功能


现在,我们可以测试导入功能了。假设我们有一个 math.lime 文件,内容如下:
func add(a: int, b: int) -> int {
return a + b;
}



在 test.lime 文件中,我们可以导入并使用它:
import "math.lime";




for i in 0..10 {
if i == 1 {
print(i, " + 8 = ", add(i, 8));
}
}



运行主程序,如果一切正常,你将看到输出 1 + 8 = 9,这证明 add 函数已从 math.lime 文件成功导入并执行。



总结

本节课中,我们一起为我们的编程语言实现了基础的文件导入功能。我们完成了以下工作:
- 在词法分析器中添加了
import关键字。 - 在抽象语法树中定义了
ImportStatement节点。 - 在语法分析器中实现了导入语句的解析逻辑。
- 在编译器中实现了读取外部文件、分析其代码、并将其编译到当前环境的核心功能。
这个实现是一个全局化的导入系统,它允许你在一个文件中使用另一个文件中定义的函数。你可以在此基础上进行扩展,例如添加相对路径支持、模块化命名空间等。

下一节(第17集),我们将学习如何将整个语言导出为一个可执行文件(EXE),以便将其添加到系统路径中,像 Python 或 Go 一样安装和使用。
017:导出为 EXE 可执行文件 🚀


在本节课中,我们将学习如何将我们编写的编程语言编译器打包成一个独立的 EXE 可执行文件。这样,你就可以将它分享给朋友,或者发布到 GitHub 上,让其他人下载并添加到他们的系统路径中,像使用其他编程语言一样使用它。


安装 PyInstaller

首先,我们需要安装一个名为 PyInstaller 的库,它能轻松地将 Python 代码编译成独立的可执行文件。


在终端中运行以下命令:
pip install pyinstaller
确保安装成功。

修改主程序文件

接下来,我们需要修改 main.py 文件,使其能够接收命令行参数,并添加一些调试信息。



导入必要模块



在文件顶部,我们需要导入 argparse 模块来处理命令行参数。
from argparse import ArgumentParser, Namespace

创建参数解析函数



我们将创建一个名为 parse_arguments 的函数来定义和解析命令行参数。
def parse_arguments() -> Namespace:
parser = ArgumentParser(description="你的语言名称 - Alpha 版本")
parser.add_argument("file_path", type=str, help="入口点 .lime 文件的路径,例如 main.lime")
parser.add_argument("--debug", action="store_true", help="打印内部调试信息")
return parser.parse_args()
这个函数定义了两个参数:
file_path:一个必需的字符串参数,用于指定要编译的.lime文件路径。--debug:一个可选的标志,如果用户添加此标志,其值将为True,用于开启调试模式。

整合参数解析与调试功能

在 run_code 函数中,我们需要调用参数解析函数,并根据参数调整程序行为。



首先,在读取输入文件之前,解析命令行参数:
args = parse_arguments()
pro_debug = False
if args.debug:
pro_debug = True
然后,根据是否提供了 file_path 参数来决定使用哪个文件:
file_path = "tests/test.lime"
if args.file_path:
file_path = args.file_path
在后续打开文件时,使用这个 file_path 变量。



添加性能计时



为了在调试模式下查看解析和编译阶段所花费的时间,我们添加一些计时点。


在解析代码之前和之后记录时间:
parse_start_time = time.time()
# ... 解析代码的步骤 ...
parse_end_time = time.time()
在编译代码之前和之后记录时间:
compile_start_time = time.time()
# ... 编译代码的步骤 ...
compile_end_time = time.time()
最后,在程序末尾,如果启用了调试模式,则打印耗时信息:
if pro_debug:
print(f"解析耗时: {(parse_end_time - parse_start_time) * 1000:.2f} 毫秒")
print(f"编译耗时: {(compile_end_time - compile_start_time) * 1000:.2f} 毫秒")


使用 PyInstaller 生成 EXE


完成代码修改后,我们就可以使用 PyInstaller 来生成可执行文件了。
在终端中,运行以下命令:
pyinstaller --onefile --name lime main.py
命令参数说明:
--onefile:将所有依赖打包成一个单独的 EXE 文件。--name lime:指定生成的可执行文件名为lime。main.py:指定程序的入口点文件。


你还可以使用 --icon 参数为 EXE 文件添加图标:
pyinstaller --onefile --name lime --icon assets/icon.ico main.py
编译过程可能需要一两分钟。完成后,你会在项目目录下看到 dist 和 build 文件夹。我们需要的 lime.exe 文件就在 dist 文件夹中。


测试生成的 EXE


进入 dist 目录,并创建一个简单的测试文件 main.lime,例如写入 i + 8。
然后,在终端中运行你的编译器:
cd dist
./lime main.lime
如果一切正常,程序将输出计算结果(例如 9),并且如果启用了 --debug 标志,还会显示解析和编译的耗时。
后续步骤
现在你已经拥有了一个可以分发的 EXE 文件。你可以:
- 将
dist/lime.exe的路径添加到系统的环境变量(PATH)中,这样就可以在任意位置使用lime命令了。 - 如果你计划将项目上传到 GitHub,请确保在
.gitignore文件中忽略build/和dist/目录,避免上传不必要的构建文件。build/* dist/*
总结

本节课中,我们一起学习了如何将 Python 编写的语言编译器打包成独立的 EXE 可执行文件。我们主要完成了以下工作:
- 使用
argparse模块为编译器添加了命令行参数支持。 - 集成了调试模式,可以输出内部信息和性能数据。
- 使用 PyInstaller 工具成功生成了单文件的
lime.exe。 - 测试了生成的可执行文件,并了解了后续的分发和版本管理注意事项。

通过本节的学习,你的编程语言已经具备了可分发和独立运行的能力。在下一节中,我们将探讨如何进一步进行静态编译,敬请期待。

浙公网安备 33010602011771号