下一代编程语言出炉

· · 科技·工程

省流:我用 c++ 实现了一款编译大型项目比 c++ 快近 10 倍的编程语言。

组织地址 | 仓库地址。 想听干货的到下一章。

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] 长度为 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)

关键字:ifelse

基本用法

条件必须用括号括起来

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;
}

模板

关键字:templatetypename

模板用于编写泛型代码,支持多个模板参数、指定类型和默认值,以及自动类型推导和显式类型参数。支持模板函数和模板类,他们都是在语义解析的时候实例化。

语法

template$T:typename$
template$T:typename,len: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;
}

作用域规则

顶层作用域(文件级别)

以下内容只能出现在文件顶层,不能在函数内部定义:

块作用域(函数内部)

{} 包围的代码块可以嵌套,内部定义的变量在块结束后销毁:

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 分为以下类别:

怎么做

Mio 的 Lexer 采用有限状态自动机逐字符扫描:

扫描流程:

  1. 跳过空白和注释skipWhitespace() 函数处理空格、制表符、换行符和 # 注释
  2. 识别 Token 类型:根据当前字符类型进入不同处理分支
    • 字母或下划线 → ident()(标识符 / 关键字)
    • 数字 → number()(整数 / 浮点数)
    • 双引号 → stringLit()(字符串)
    • 单引号 → charLit()(字符)
    • 运算符字符 → 直接匹配(优先匹配多字符运算符,如 += 优先于 +
  3. 生成 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 采用递归下降解析法,为每个语法规则编写一个解析函数,由顶层函数递归调用:

解析流程:

  1. parse() 作为入口,循环调用 parse_decl() 直到文件结束
  2. parse_decl() 根据当前 Token 判断声明类型并分发&#x20;
    • var/constparse_var_decl() 解析变量声明
    • 返回类型 + 标识符 → parse_func_def() 解析函数声明
    • classparse_class_def() 解析类声明
    • importparse_import() 解析导入指令
    • macroparse_macro_def() 解析宏定义
    • templateparse_template_def() 解析模板定义
    • enum/union → 解析枚举 / 联合体
    • namespace → 解析命名空间
  3. parse_stmt() 解析语句(if/while/for/return/break/continue/goto/block
  4. parse_expr() 解析表达式(见下方 Pratt 解析法)
  5. 各解析函数递归调用并构建 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 解析过程中动态处理:

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,收集所有声明并建立符号表:

第二阶段:分析(analyze

再次遍历 AST,进行类型检查和引用解析:

符号表实现

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;  // 实例化缓存

作用域管理:

语义分析时维护 localslocalMioTypes 映射,进入函数 / 块时保存当前作用域,退出时恢复:

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()

  1. 检查类型是否合法(如不能定义"引用的引用")
  2. 解析类名(处理命名空间前缀)
  3. 如果是模板类,触发模板实例化
  4. 递归检查基类型

checkExpr()

类型推导(resolveExprMioType):

模板实例化

Mio 的模板在语义分析阶段实例化:

触发条件:checkType() 遇到 MioTypeKind::CLASSparam_types 非空

实例化流程:

  1. 生成实例化名称:ClassName_Arg1Type_Arg2Type...
  2. 检查缓存(instantiatedClasses),若已实例化则直接复用
  3. classTemplateMap 查找模板定义
  4. 构建类型替换表(typeSubst):模板参数名 → 实际类型
  5. 克隆 AST 并替换所有类型引用
  6. 将实例化后的类 AST 存入 instantiatedClasses
  7. 递归注册实例化后的类成员(字段、方法、构造函数)

函数模板实例化:类似流程,CompilergenTemplateInstantiation() 完成,结果缓存在 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)`**

  1. 调用 SemanticAnalyzer::analyze() 完成语义分析
  2. 处理模板实例化类(为 instantiatedClasses 中的类创建前向声明)
  3. genDecl() 递归遍历 A - AST,分发到具体生成函数
  4. 验证 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 initcondbodyupdate → 回跳 cond
break br 跳转到最近的循环 end
continue br 跳转到最近的循环 cond/update
return 清理栈上对象 + ret 指令

类布局:

  1. 收集所有字段,按声明顺序排列
  2. 检查是否有虚函数,若有则第一个字段为 VTable 指针
  3. 创建 llvm::StructType,记录每个字段的索引
  4. 成员访问通过 GEP(Get Element Pointer)指令计算偏移量

虚函数表(VTable)生成:

  1. genClassDef() 收集所有虚函数,存入 classVTableOrder
  2. genVTable() 创建 VTable 类型和全局变量
  3. 构造函数中设置对象的 VTable 指针
  4. 虚函数调用:从对象读取 VTable 指针 → 索引获取函数指针 → 间接调用

模板实例化的代码生成:

CompilergenTemplateInstantiation() 处理函数模板:

  1. 查找模板定义(templateMap
  2. 生成实例化名称(名称修饰)
  3. 检查缓存(templateInstances
  4. 构建类型替换表
  5. 调用 instantiateTemplate() 克隆 AST 并替换类型
  6. 生成 IR(genFuncDef
  7. 缓存结果

清理机制(析构函数调用):

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() 流程:

  1. 初始化 LLVM 目标(LLVMInitializeAllTargets()
  2. 获取当前平台的三元组(sys::getProcessTriple()
  3. 创建 TargetMachine
  4. 设置 DataLayout 和 TargetTriple
  5. 调用 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 年。请点个赞吧。