下一代编程语言出炉
HZY1618yzh · · 科技·工程
省流:我用 c++ 实现了一款编译大型项目比 c++ 快近
组织地址 | 仓库地址。 想听干货的到下一章。
Mio 语言简介
灵感
一次偶然的机会,我知道到了 python,对它的原理特别感兴趣,研究过一段时间,写了个简易解释器。保存的 U 盘被一个 xxs 借走然后弄丢了,本来就没画多少时间写,于是就懒得重写了。
机遇
我在 zsjn 中学冬令营和同学吹牛,聊到了以前写的解释器,于是 ld^1 就复现了一个解释器?然后它集成汇编的难写和 python 的巨慢,不过在我们努力地调屎下,我们实现了图灵完备性^2。
ld 百思苦想给变量赋值的指令,随手打出了 mio 这三个字符,然后用这三个非常有意义的字符给我们的语言命名了。
重写
今年暑假前夕,我突然翻到我们在 github 上留下的 mio,觉得当时写的过于简陋,于是开始了深入学习,下面讲分享我半个暑假的成果。
语法简介
可能写的不好,以 github 仓库上的为准。
注释
# 后面的内容为注释,直到行尾:
# 这是一行注释
var x=10; # 行末注释
导入
关键字:import
使用方法:导入 mio 头文件,必须写在文件顶层。可以用-I命令指定头文件路径。
mioc hello.mio -I ./include
import stdio; # 导入 C 标准库
import "mylib.mio"; # 导入 mio 文件(用引号包裹)
import stdio,"lib"; # 同时导入多个
宏与条件编译
关键字:macro,@[if,elif,else,end]
使用方法:定义宏来替换代码和条件编译防止重复引入。可以用 -D 命令定义宏。
mioc hello.mio -D DEBUG
macro DEBUG;
macro RELEASE;
@if DEBUG
printf("DEBUG\n");
@elif RELEASE
printf("RELEASE\n");
@else
printf("UNKNOWN\n");
@end
变量
关键字:var(可变)、const(常量)
使用方法:
var 变量名: 类型=初始值; # 变量
const 常量名: 类型=初始值; # 常量(不可修改)
基本类型
| 类型 | 说明 |
|---|---|
i8 / i16 / i32 / i64 / i128 |
有符号整数 |
u8 / u16 / u32 / u64 / u128 |
无符号整数 |
isize / usize |
指针宽度整数(32 / 64 位自适应) |
f32 / f64 |
浮点数 |
bool |
布尔值(true / false) |
char |
单个字符 |
T[N] |
长度为 |
*T、**T… |
指针类型 |
&T |
可变引用类型 |
&&T |
不可变可变引用类型,在模板里可以隐式转换成 &T |
示例
var x: i32=42;
const PI: f64=3.14;
# 同时声明多个
var a: i64=1,b: i32=2;
# 数组
var arr: i32[3]={1,2,3};
自动补全类型
有初始值时可以省略类型,编译器自动推导:
var x=42; # i32
var pi=3.14; # f64
var flag=true; # bool
var nums={1,2,3}; # i32[3]
字符串与字符
var s: char*="hello"; # 字符串字面量
var ch: char='A'; # 字符字面量
全局变量与命名空间访问
用 :: 前缀访问全局变量(避免与局部变量同名冲突):
var x: i32=10;
void foo() {
var x: i32=20;
printf("%d\n",::x); # 访问全局 x,输出 10
printf("%d\n",x); # 访问局部 x,输出 20
}
控制流(if)
关键字:if、else
基本用法
条件必须用括号括起来
if (条件) {
代码块
} else if (条件) {
代码块
} else {
代码块
}
示例
if (score >= 90) {
printf("A\n");
} else if (score >= 80) {
printf("B\n");
} else {
printf("C\n");
}
单语句简写
单语句可以省略大括号:
if (x > 0) printf("正数\n");
循环
while 循环
关键字:while
条件必须用括号括起来
var i: i32=0;
while (i < 5) {
printf("%d\n",i);
i=i+1;
}
for 循环
关键字:for
格式:for (初始化; 条件; 更新) { 代码块 }
var sum: i32=0;
for (i=0; i < 10; i=i+1) {
sum=sum+i;
}
省略部分
var i: i32=0;
for (; i < 5; i=i+1) { # 省略初始化
printf("%d\n",i);
}
break 与 continue
while (true) {
if (x > 100) {
break; # 跳出循环
}
if (x % 2 == 0) {
x=x+1;
continue; # 跳过本次迭代剩余部分
}
x=x+1;
}
跳转(goto)
关键字:goto
定义标签::标签名
var i: i32=0;
:loop
printf("%d\n",i);
i=i+1;
if (i < 5) {
goto loop;
}
运算符
算术运算符
| 运算符 | 说明 |
|---|---|
+ - * / % |
加减乘除取模 |
- |
取负(一元) |
~ |
按位取反 |
比较运算符
| 运算符 | 说明 |
|---|---|
== != |
等于 / 不等于 |
< > <= >= |
小于 / 大于 / 小于等于 / 大于等于 |
逻辑运算符
| 运算符 | 说明 |
|---|---|
&& \|\| ! |
逻辑与 / 逻辑或 / 逻辑非 |
位运算符
| 运算符 | 说明 |
|---|---|
& \| ^ |
按位与 / 按位或 / 按位异或 |
<< >> |
左移 / 右移 |
赋值运算符
| 运算符 | 说明 |
|---|---|
= |
赋值 |
+= -= *= /= %= |
复合赋值 |
&= \|= ^= <<= >>= |
位复合赋值 |
运算符重载(结构体 / 类)
在结构体或类中定义 operator+、operator- 等方法实现运算符重载:
struct Point {
x: f64;
y: f64;
Point operator+(other: Point) {
return Point(this.x+other.x,this.y+other.y);
}
}
类型转换
使用 C 风格的类型转换语法:
var x: f64=3.14;
var y: i32=i32(x); # 浮点数转整数(截断)
var z: i64=i64(y); # 整数扩展
var n: u32=u32(-1); # 有符号转无符号
函数
定义格式
返回类型 函数名(参数名: 参数类型,...) {
函数体
}
示例
# 显式返回
i32 add(a: i32,b: i32) {
return a+b;
}
# 隐式返回(最后一行不加分号)
i32 add(a: i32,b: i32) {
a+b
}
# 无返回值
void say_hello() {
printf("hello\n");
}
# 静态函数(仅当前文件可见)
static void helper() {
printf("helper\n");
}
# 仅声明函数(其他文件定义)
extern i32 printf(fmt: char*,...);
函数调用
var result=add(10,20);
say_hello();
类
关键字:class
类与结构体类似,但支持继承、虚函数和访问控制。类默认使用引用语义(通过指针操作)。
定义
class 类名 {
访问控制:
字段名: 类型;
方法定义...
}
访问控制
| 关键字 | 说明 |
|---|---|
public: |
公开成员,外部可访问 |
private: |
私有成员,仅类内部可访问 |
protected: |
受保护成员,类及其子类可访问 |
构造函数与析构函数
构造函数名与类名相同,析构函数以 ~ 开头:
class Animal {
public:
name: char*;
Animal(name: char*) {
this.name=name;
}
~Animal() {
printf("Animal destroyed\n");
}
}
继承
使用 类名(父类:访问控制) 语法继承父类:
class Dog(Animal:public) {
public:
Dog(name: char*) {
this.name=name;
}
}
虚函数与重写
用 virtual 声明虚函数,子类用 override 重写:
class Animal {
public:
virtual void speak() {
printf("Animal speak\n");
}
};
class Dog(Animal:public) {
public:
override void speak() {
printf("Dog: woof!\n");
}
};
完整示例
class Animal {
public:
name: char*;
Animal(name: char*) {
this.name=name;
printf("Animal ctor: %s\n",name);
}
~Animal() {
printf("Animal dtor: %s\n",this.name);
}
virtual void speak() {
printf("Animal speak\n");
}
};
class Cat(Animal:public) {
public:
Cat(name: char*) {
this.name=name;
printf("Cat ctor: %s\n",name);
}
override void speak() {
printf("Cat %s: meow!\n",this.name);
}
};
i32 main() {
var dog=Dog("Buddy");
var cat=Cat("Kitty");
dog.speak(); # Dog: woof!
cat.speak(); # Cat: meow!
return 0;
}
枚举
关键字:enum
定义
enum 枚举名 {
变体名,
变体名=初始值,
...
}
示例
enum Color {
Red,
Green,
Blue
}
enum Status {
Ok=0,
Error=-1
}
使用
var c=Color.Red;
var s=Status.Ok;
联合体
关键字:union
定义
union 联合体名 {
字段名: 类型;
字段名: 类型;
...
}
示例
union Value {
int_val: i32;
float_val: f64;
bool_val: bool;
}
使用
var v: Value;
v.int_val=42;
printf("%d\n",v.int_val);
v.float_val=3.14;
printf("%f\n",v.float_val);
命名空间
关键字:namespace
命名空间用于组织代码,避免名称冲突。命名空间可以嵌套。
namespace math {
i32 add(a: i32,b: i32) {
return a+b;
}
i32 sub(a: i32,b: i32) {
return a-b;
}
}
i32 main() {
var x=math::add(10,20); # 用 :: 访问命名空间成员
return x;
}
模板
关键字:template、typename
模板用于编写泛型代码,支持多个模板参数、指定类型和默认值,以及自动类型推导和显式类型参数。支持模板函数和模板类,他们都是在语义解析的时候实例化。
语法
template$T:typename$
template$T:typename,len:i32=100$
T:typename— 类型参数(简写:T等同于T:typename)len:i32=100— 值参数,类型为i32,默认值为100
定义
template$T:typename$
T max(a: T,b: T) {
if (a > b) {
return a;
}
return b;
}
使用
i32 main() {
var x=max(10,20); # 自动推导 T=i32
var y=max$f64$(3.14,2.71); # 显式指定 T=f64
return 0;
}
函数参数默认值
函数参数可以指定默认值,调用时可以省略有默认值的参数:
i32 add(a: i32,b: i32=10) {
return a+b;
}
i32 main() {
var x=add(5); # x=15 (b 使用默认值 10)
var y=add(5,20); # y=25
return 0;
}
作用域规则
顶层作用域(文件级别)
以下内容只能出现在文件顶层,不能在函数内部定义:
importclassenumunionnamespacetemplate
块作用域(函数内部)
用 {} 包围的代码块可以嵌套,内部定义的变量在块结束后销毁:
void test() {
var x=10;
{
var y=20; # y 只在此块内有效
var z=30;
}
# y 和 z 在这里不可见
}
编译器的底层实现逻辑
你是否好奇 Mio 是怎么编译的?一个 .mio 文本文件,如何变成可执行的机器指令?本章将深入剖析 Mio 编译器的完整工作流程 —— 从源码到可执行文件的全链路实现。
本章部分内容参考这篇文章,并由 AI 润色。在此保留创作权。
编译流程总览
Mio 编译器的整体架构遵循经典的编译器设计范式,但也有一些独特的实现选择。整个编译流程可以概括为五个核心阶段:词法分析、语法分析、语义分析、代码生成和链接。
第一阶段是词法分析,由 Lexer 类负责。它将源代码字符流拆分为有意义的 Token 序列(关键字、标识符、字面量、运算符等),为后续语法分析提供基础。词法分析器采用有限状态自动机实现,逐字符扫描并识别 Token 类型,同时记录每个 Token 的位置信息(行号、列号)以便错误报告。
第二阶段是语法分析,由 Parser 类负责。Parser 将 Token 流组织成抽象语法树(AST),采用递归下降解析法,为每个语法规则编写一个解析函数。表达式部分使用 Pratt 解析法处理运算符优先级。值得注意的是,Mio 的预处理功能(宏展开、文件导入、条件编译)并未独立成阶段,而是融入 Parser 中完成 —— 这是 Mio 与 C++ 等传统编译器的显著差异。
第三阶段是语义分析,由 SemanticAnalyzer 类负责。这一阶段分为两步:先注册所有声明(收集符号表、检测重复定义),再分析代码(类型检查、引用解析、模板实例化)。Mio 的模板在语义分析阶段实例化,实例化后的 AST 被缓存供后续阶段使用。
第四阶段是代码生成,由 Compiler 类负责。它遍历语义分析后的 AST,将其转换为 LLVM IR(中间表示)。这一阶段完成类型映射(Mio 类型 → LLVM 类型)、变量管理(通过 alloca/load/store 模拟 SSA)、类布局(struct + vtable)、函数生成和控制流转换等工作。
第五阶段是后端处理与链接,借助 LLVM 完成优化(根据 -O0 ~ -O3 选择优化 Pass)、目标代码生成(汇编 / 目标文件),最后由链接器将多个目标文件和库合并为可执行文件。
词法分析阶段(Lexer)
做什么
词法分析器将预处理后的字符流拆分成一个个词素(Token)—— 编译器能识别的最小语义单元,并记录每个 Token 的位置信息(行号、列号)以便错误报告。
Token 的分类
Mio 的 Token 分为以下类别:
- 关键字:
var,const,if,class,import,macro等 - 标识符:用户定义的变量名、函数名、类名等
- 字面量:整数(
42)、浮点数(3.14)、字符串("hello")、字符('A')、布尔值(true/false) - 运算符:
+,-,*,/,%,=,==,!=,&&,||,+=,-=,<<,>>等 - 分隔符:
;,{,},(,),[,],,,:,::等 - 特殊标记:
$(模板参数标记)、@if/@elif/@else/@end(条件编译指令)
怎么做
Mio 的 Lexer 采用有限状态自动机逐字符扫描:
扫描流程:
- 跳过空白和注释:
skipWhitespace()函数处理空格、制表符、换行符和#注释 - 识别 Token 类型:根据当前字符类型进入不同处理分支
- 字母或下划线 →
ident()(标识符 / 关键字) - 数字 →
number()(整数 / 浮点数) - 双引号 →
stringLit()(字符串) - 单引号 →
charLit()(字符) - 运算符字符 → 直接匹配(优先匹配多字符运算符,如
+=优先于+)
- 字母或下划线 →
- 生成 Token:记录类型、词素文本、行号、列号,字面量 Token 额外存储值
关键词匹配:ident() 函数先读取完整标识符,然后查关键字表判断是否为关键字,若不是则标记为 TOK_IDENT。
最长匹配原则:运算符匹配时优先匹配更长的有效序列,例如 += 识别为单个复合赋值运算符,而非 + 和 = 两个 Token。
关键问题与解决方案
| 问题 | 解决方案 |
|---|---|
>> 是右移还是模板结束符 |
由于没有历史包袱,于是使用 $ 而不是尖括号 |
| 非法字符(中文符号等) | 记录错误并跳过,继续分析后续内容 |
| 多字符运算符歧义 | 贪心匹配最长的有效运算符序列 |
语法分析阶段(Parser)
做什么
语法分析器将词法分析输出的 Token 流,根据 Mio 的语法规则,组织成抽象语法树(AST),并以树形结构表示程序的逻辑结构。
抽象语法树(AST)
AST 是编译器内部的核心数据结构,它以树形结构表示程序的逻辑结构,过滤掉冗余的语法符号(如括号、分号等),仅保留必要的语义信息。
示例:var x = 2 * 3 + 4;
AST 结构:
VarDeclNode
├─ name: "x"
├─ type: (自动推导)
└─ init: BinaryExpr(+)
├─ left: BinaryExpr(*)
│ ├─ left: IntLiteral(2)
│ └─ right: IntLiteral(3)
└─ right: IntLiteral(4)
怎么做
Mio 采用递归下降解析法,为每个语法规则编写一个解析函数,由顶层函数递归调用:
解析流程:
parse()作为入口,循环调用parse_decl()直到文件结束parse_decl()根据当前 Token 判断声明类型并分发 var/const→parse_var_decl()解析变量声明- 返回类型 + 标识符 →
parse_func_def()解析函数声明 class→parse_class_def()解析类声明import→parse_import()解析导入指令macro→parse_macro_def()解析宏定义template→parse_template_def()解析模板定义enum/union→ 解析枚举 / 联合体namespace→ 解析命名空间
parse_stmt()解析语句(if/while/for/return/break/continue/goto/block)parse_expr()解析表达式(见下方 Pratt 解析法)- 各解析函数递归调用并构建 AST 节点
Pratt 解析法(表达式):
表达式解析采用 Pratt 解析法(也称"优先级爬升法")处理运算符优先级:
| 层级 | 解析函数 | 运算符 |
|---|---|---|
| 最低 | parse_assignment() |
=,+=,-=,*=,/=,%=,&=,\|=,^=,<<=,>>= |
parse_logical_or() |
\|\| |
|
parse_logical_and() |
&& |
|
parse_bit_or() |
\| |
|
parse_bit_xor() |
^ |
|
parse_bit_and() |
& |
|
parse_equality() |
==,!= |
|
parse_relational() |
<,>,<=,>= |
|
parse_shift() |
<<,>> |
|
parse_additive() |
+,- |
|
parse_multiplicative() |
*,/,% |
|
parse_unary() |
-,!,~,*,& |
|
| 最高 | parse_postfix() |
(),[],.,-> |
parse_primary() |
字面量、标识符、括号表达式 |
预处理功能的融入:
Mio 的预处理不在独立阶段完成,而是在 Parser 解析过程中动态处理:
- 宏展开:
parse_macro_def()将宏名和值存入macros列表,后续遇到宏名时替换 - 文件导入:
parse_import()递归创建新 Lexer 解析导入文件,AST 合并到当前 AST - 条件编译:
parse_cond_comp()根据宏定义状态决定是否保留代码块
AST 节点数据结构:
class AstNode {
AstNodeKind kind; // 节点类型(VAR_DECL/FUNC_DEF/BINARY_EXPR...)
MioType* type; // 语义分析后填充的类型信息
const std::string* filename;
int line,col;
// 不同节点类型的数据存储
struct { std::string name; MioType* var_type; AstNode* init; } var_decl;
struct { std::string name; MioType* return_type; ... } func_def;
struct { AstNode* left; TokenKind op; AstNode* right; } binary;
// ...
};
关键问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 运算符优先级 | Pratt 解析法,通过优先级数值控制解析顺序 |
| 语法错误恢复 | 记录错误后跳过部分 Token,继续解析尽可能多地报告错误 |
| 循环导入 | imported_files 列表检测已导入文件,直接跳过 |
语义分析阶段(SemanticAnalyzer)
做什么
语义分析阶段检查程序的语义正确性 —— 代码的意义是否正确,完成符号表建立、类型检查、建立 a-ast、模板实例化和名称修饰等工作。
两阶段设计
Mio 的语义分析分为注册和分析两个阶段,这种设计允许前向引用:
第一阶段:注册(registerPass)
遍历 AST,收集所有声明并建立符号表:
- 函数声明 → 存入
funcDecls - 变量声明 → 存入
varDecls - 类定义 → 存入
classTypes,记录字段和方法 - 枚举 / 联合体 → 存入
enumNames/unionNames - 模板定义 → 存入
templateMap/classTemplateMap - 命名空间 → 递归处理
第二阶段:分析(analyze)
再次遍历 AST,进行类型检查和引用解析:
analyzeDecl()→ 分析顶层声明analyzeStmt()→ 分析语句(变量声明、条件、循环等)checkExpr()→ 分析表达式(类型检查、引用查找)checkType()→ 检查类型合法性,触发模板实例化
符号表实现
SemanticAnalyzer 使用多个哈希表存储符号信息:
// 符号集合
std::unordered_set<std::string> funcDecls; // 已声明的函数
std::unordered_set<std::string> varDecls; // 已声明的变量
std::unordered_set<std::string> classTypes; // 已定义的类
std::unordered_set<std::string> enumNames; // 已定义的枚举
std::unordered_set<std::string> unionNames; // 已定义的联合体
// 类详细信息
std::unordered_map<std::string,std::unordered_set<std::string>> classFields;
std::unordered_map<std::string,std::unordered_map<std::string,MioType*>> classFieldTypes;
std::unordered_map<std::string,std::unordered_set<std::string>> classMethodSet;
std::unordered_map<std::string,std::unordered_set<std::string>> classPureVirtuals;
// 模板
std::unordered_map<std::string,std::pair<std::vector<TemplateParam>,AstNode*>> templateMap;
std::unordered_map<std::string,std::pair<std::vector<TemplateParam>,AstNode*>> classTemplateMap;
std::unordered_map<std::string,AstNode*> instantiatedClasses; // 实例化缓存
作用域管理:
语义分析时维护 locals 和 localMioTypes 映射,进入函数 / 块时保存当前作用域,退出时恢复:
auto savedLocals = locals;
auto savedMioTypes = localMioTypes;
// 处理函数参数和局部变量
for(auto& p : node->func_def.params) {
locals[p.name] = p.type;
localMioTypes[p.name] = p.type;
}
analyzeBlock(node->func_def.body);
locals = savedLocals;
localMioTypes = savedMioTypes;
类型检查
checkType():
- 检查类型是否合法(如不能定义"引用的引用")
- 解析类名(处理命名空间前缀)
- 如果是模板类,触发模板实例化
- 递归检查基类型
checkExpr():
- 二元运算:检查左右操作数类型是否兼容,支持类运算符重载查找
- 一元运算:检查操作数是否合法(如不能对
void*解引用) - 函数调用:检查函数是否存在,参数类型是否匹配
- 成员访问:检查类中是否存在该字段 / 方法
- 标识符:从符号表(
locals→varDecls→funcDecls→ 导入的命名空间)逐级查找
类型推导(resolveExprMioType):
- 字面量 → 对应内置类型
- 二元运算 → 统一操作数类型(整数提升、整数→浮点数)
- 成员访问 → 从
classFieldTypes查找字段类型 - 标识符 → 从
localMioTypes查找
模板实例化
Mio 的模板在语义分析阶段实例化:
触发条件:checkType() 遇到 MioTypeKind::CLASS 且 param_types 非空
实例化流程:
- 生成实例化名称:
ClassName_Arg1Type_Arg2Type... - 检查缓存(
instantiatedClasses),若已实例化则直接复用 - 从
classTemplateMap查找模板定义 - 构建类型替换表(
typeSubst):模板参数名 → 实际类型 - 克隆 AST 并替换所有类型引用
- 将实例化后的类 AST 存入
instantiatedClasses - 递归注册实例化后的类成员(字段、方法、构造函数)
函数模板实例化:类似流程,Compiler 中 genTemplateInstantiation() 完成,结果缓存在 templateInstances
关键问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 前向引用(使用在前,声明在后) | 两阶段设计:先注册所有声明,再分析引用 |
| 模板递归实例化 | 实例化缓存 + 名称去重 |
| 类型不匹配 | 统一操作数类型,若无法统一则报错 |
| 命名冲突 | 符号表检测重复定义 |
代码生成阶段(Compiler)
做什么
语义分析后的 A - AST 被转换为 LLVM IR(中间表示),这是编译器前端和后端之间的桥梁。LLVM IR 采用 ** 静态单赋值(SSA)** 形式,每个变量只能赋值一次,便于数据流分析和优化。
整体架构
class Compiler {
llvm::LLVMContext ctx;
std::unique_ptr<llvm::Module> mod;
llvm::IRBuilder<> b;
llvm::Function* curFn;
llvm::BasicBlock* curBB;
// 符号映射(变量 → LLVM 内存位置)
std::unordered_map<std::string,llvm::AllocaInst*> locals;
std::unordered_map<std::string,MioType*> localMioTypes;
std::unordered_map<std::string,llvm::Function*> funcDecls;
std::unordered_map<std::string,llvm::GlobalVariable*> globalVars;
// 类相关
std::unordered_map<std::string,llvm::StructType*> classTypes;
std::unordered_map<std::string,std::unordered_map<std::string,unsigned>> classFieldIdx;
std::unordered_map<std::string,std::vector<std::pair<std::string,llvm::Function*>>> classVTable;
};
怎么做
*入口:`generate(AstNode program)`**
- 调用
SemanticAnalyzer::analyze()完成语义分析 - 处理模板实例化类(为
instantiatedClasses中的类创建前向声明) genDecl()递归遍历 A - AST,分发到具体生成函数- 验证 LLVM 模块
类型映射(convertType):
| Mio 类型 | LLVM 类型 |
|---|---|
void |
void |
i8/u8/char |
i8 |
i32 |
i32 |
i64 |
i64 |
f32 |
float |
f64 |
double |
bool |
i1 |
*T |
T* |
T[N] |
[N x T] |
class Name |
%struct.Name |
变量管理(SSA 模拟):
LLVM IR 使用 SSA 形式,Mio 通过 alloca 在栈上分配内存来模拟可变变量:
// 变量声明 → 分配栈空间
void genVarDecl(AstNode* decl) {
auto* alloca = createEntryAlloca(curFn,name,ty);
locals[name] = alloca;
if(initExpr) {
auto* val = genExpr(initExpr);
b.CreateStore(val,alloca);
}
}
// 读取变量 → load
llvm::Value* loadVariable(const std::string& name) {
return b.CreateLoad(ty,locals[name]);
}
// 修改变量 → store
void storeVariable(const std::string& name,llvm::Value* val) {
b.CreateStore(val,locals[name]);
}
控制流生成:
| Mio 结构 | LLVM IR 结构 |
|---|---|
if-else |
br 条件分支 + then/else 基本块 + merge 合并块 |
while |
cond 条件块 → body 循环体 → end 结束块(含 br 回跳) |
for |
init → cond → body → update → 回跳 cond |
break |
br 跳转到最近的循环 end 块 |
continue |
br 跳转到最近的循环 cond/update 块 |
return |
清理栈上对象 + ret 指令 |
类布局:
- 收集所有字段,按声明顺序排列
- 检查是否有虚函数,若有则第一个字段为 VTable 指针
- 创建
llvm::StructType,记录每个字段的索引 - 成员访问通过
GEP(Get Element Pointer)指令计算偏移量
虚函数表(VTable)生成:
genClassDef()收集所有虚函数,存入classVTableOrdergenVTable()创建 VTable 类型和全局变量- 构造函数中设置对象的 VTable 指针
- 虚函数调用:从对象读取 VTable 指针 → 索引获取函数指针 → 间接调用
模板实例化的代码生成:
Compiler 中 genTemplateInstantiation() 处理函数模板:
- 查找模板定义(
templateMap) - 生成实例化名称(名称修饰)
- 检查缓存(
templateInstances) - 构建类型替换表
- 调用
instantiateTemplate()克隆 AST 并替换类型 - 生成 IR(
genFuncDef) - 缓存结果
清理机制(析构函数调用):
cleanupStack 维护当前作用域中需要销毁的对象:
std::vector<std::pair<std::string,llvm::AllocaInst*>> cleanupStack;
// 变量声明时注册析构
if(mt->kind == MioTypeKind::CLASS) {
auto dtor = findDestructor(mt->name);
if(dtor) cleanupStack.push_back({mt->name,alloca});
}
// 离开作用域时逆序调用析构
void genCleanupAll() {
for(int i = cleanupStack.size() - 1; i >= 0; i--) {
auto dtor = findDestructor(entry.first);
b.CreateCall(dtor,{ptr});
}
}
输出模式
Compiler 支持四种输出模式:
| 模式 | 方法 | 输出 |
|---|---|---|
| LLVM IR | emitLLVM() |
.ll 文本文件 |
| 汇编 | emitAssembly() |
.s 汇编文件 |
| 目标文件 | emitObject() |
.o 目标文件 |
| 可执行文件 | linkExecutable() |
.exe/ELF 可执行文件 |
后端与链接
LLVM 优化
Compiler 通过 LLVM 的 Pass 框架应用优化,优化级别由 -O0 ~ -O3 控制:
| 级别 | 说明 |
|---|---|
-O0 |
无优化,最快编译速度,适合调试 |
-O1 |
基础优化(常量折叠、死代码消除) |
-O2 |
标准优化(循环优化、内联) |
-O3 |
激进优化(向量化、循环展开) |
优化通过 LLVM C API 的 LLVMTargetMachineEmitToFile 在生成目标文件时应用。
目标代码生成
emitObject() 流程:
- 初始化 LLVM 目标(
LLVMInitializeAllTargets()) - 获取当前平台的三元组(
sys::getProcessTriple()) - 创建 TargetMachine
- 设置 DataLayout 和 TargetTriple
- 调用
LLVMTargetMachineEmitToFile()生成目标文件
链接
linkExecutableFiles() 根据目标平台调用不同链接器:
| 平台 | 链接方式 |
|---|---|
| Windows(COFF) | 调用 lld::coff::link() |
| Linux(ELF) | 调用系统 cc 命令 |
| macOS(Mach-O) | 调用 lld::macho::link() |
bool linkExecutableFiles(const std::vector<std::string>& objPaths,...) {
switch(t.getObjectFormat()){
case llvm::Triple::COFF:
return lld::coff::link(args,...);
case llvm::Triple::ELF:
// 调用 cc 命令
return system(cmd.c_str()) == 0;
case llvm::Triple::MachO:
return lld::macho::link(args,...);
}
}
Mio 实现的特殊之处 / 为什么下一代编程语言是 Mio
预处理融入 Parser
与 C++ 的独立预处理器不同,Mio 的宏展开、文件导入和条件编译都在 Parser 中完成,没有预处理机制,这让 Mio 的错误处理更加简洁。
两遍扫描语义分析
采用现代编译器的特有配置,先注册所有声明,再分析引用,允许前向引用。这样就不会出现 c++ 里需要先定义再声明的尴尬现象。
语义阶段的模板实例化
模板在语义分析阶段实例化,而非代码生成阶段,实例化结果缓存在 instantiatedClasses。这样方便管理。
名称修饰
Mio 使用简单编码方案,所以可以无缝兼容 c 的所有代码,可以使用所有 libc 库代码和系统交互,甚至可以兼容部分实现较好的扩展库。
CFG 语法设计
c++ 之所以编译慢是因为它的语法上下文有关,比如 A *B; 在 A 是类型的时候是定义一个指向 A 的指针,在 A 是标识符的时候,就是一个普通的乘法。mio 没有历史包袱,所以可以设计完全上下文无关的语法。
这是世界上第一个上下文无关的高级语言!
关于文中对 ast 树的讲解,可以自行查看 P1175 的题解参考。如果文中有讲解不够明细的可以在评论区指出。
本文从灵感到创作历时近 1 年。请点个赞吧。