张大胖很幸运,刚毕业就进入了一家芯片设计公司。
不过,工作了一个月以后,他就觉得不对劲了。
原因很简单,对于公司开发的最新芯片,主流的编程语言还不支持呢!
无论是验证特性,还是编写测试,都不得不用最最古老的形式:汇编!
张大胖决心改变这种状况,他找到了梁经理,说出了自己的想法。
确实是这样,操作系统、图形学,编译器号称程序员的三大浪漫,但每一项真做起来都让人头大。
张大胖当年的毕业设计,一个极其简单的编译器,就让他做了8个月!
不过,梁经理似乎发现了一个“秘密武器”。
MoonBit是一门国产开源编程语言,由 Rescript 作者张宏波(现居深圳)及其团队打造,它的一些优秀特性,非常适合用来开发编译器和新编程语言。
张大胖非常高兴,决定利用之前毕业设计的经验,用MoonBit做个实验,设计并实现一个新的编程语言!
为了记录自己的工作,他用日记的形式记录下了每天的进展:
———日记开始———
第一天:语言设计与词法分析
晚上下班,照常来说,我会打开原神欣赏一下提瓦特的风景。不过今天要用MoonBit实现新编程语言了,游戏就先放到一边吧。
1、语言设计
我打算设计的这门语言叫 TinyMoonBit。它得有常见的数值类型、分支循环、函数、内存管理和 CFFI。语法上,我决定让它看起来像 MoonBit,但内核更像 C。
同时,为了体现“现代”语言的优越性,我给它设计了更严格的类型系统,比如 1 + 1.0 这种在 C 语言里司空见惯的隐式类型转换,在 TinyMoonBit 里是不允许的。
我随手写下了一个 TinyMoonBit 的示例程序,用来计算斐波那契数列:
extern fn print_int(x : Int) -> Unit;// 递归实现斐波那契数列fn fib(n : Int) -> Int { if n <= 1 { return n; } return fib(n - 1) + fib(n - 2);}fn main() -> Unit { print_int(fib(10));}
2、词法分析
第一步是词法分析。通俗地说,就是把一长串代码文本,切分成一个个有意义的“单词”,我们称之为 Token。比如 let x = 5;,处理后应该变成一个 Token 序列:Keyword("let") -> Identifier("x") -> Operator("=") -> IntLiteral(5) -> Symbol(";")。
在 MoonBit 里,这件事出奇的简单。首先,用一个代数数据类型(ADT)来定义所有可能的 Token:
pub enum Token { Bool(Bool) // 布尔值:true, false Int(Int) // 整数 Keyword(String) // 关键字:let, if, fn Upper(String) // 类型标识符:Int, Bool Lower(String) // 变量标识符:x, n Symbol(String) // 运算符:+, -, -> Bracket(Char) // 括号:(, ), {, } EOF // 文件结束标记} derive(Show, Eq)
然后,借助 MoonBit 的一大杀器——字符串模式匹配,我们可以像写诗一样实现词法分析器:
pub fn lex(code: String) -> Array[Token] { let tokens = Array::new() loop code[:] { // 跳过空白字符 [' ' | '\n' | '\r' | '\t', ..rest] => continue rest // 处理单行注释 [.."//", ..rest] => continue loop rest { ['\n', ..rest] => break rest [_, ..rest] => continue rest [] => break "" } // 识别多字符运算符 (顺序很重要!) [.."->", ..rest] => { tokens.push(Symbol("->")); continue rest } [.."<=", ..rest] => { tokens.push(Symbol("<=")); continue rest } // 识别单字符运算符 [':' |1 ';' | '+' | '-' | '*' | '/' | '<' | '=' as c, ..rest] => { tokens.push(Symbol("\{c}")); continue rest } // 识别标识符和关键字 ['a'..='z', ..] as code => { let (tok, rest) = lex_ident(code) // lex_ident 会区分关键字和普通变量 tokens.push(tok) continue rest } // ... 其他匹配,如数字、大写字母开头的类型等 ... [] => { tokens.push(EOF); break tokens } }}
💡 MoonBit 特性解读
函数式循环 (loop):它不是简单的重复,而是一个不断用新状态(rest)迭代自身的递归过程,非常适合处理“吃掉”一部分字符串,然后处理剩余部分的场景。
强大的模式匹配:[.."->", ..rest] 这样的语法,可以直观地匹配字符串前缀,比晦涩的正则表达式清晰百倍。
只花了大半个小时,词法分析器就写完并测试通过了。我暗自窃喜,这要是用别的语言,光是处理各种边界情况就得焦头烂额。MoonBit 的 ADT 和模式匹配,写起来真是既优雅又高效。
第二天:语法分析与类型检查
有了昨晚的顺利开局,我信心更足了。我趁着午休和晚上的时间,开始攻克编译器的第二个难关。
1、语法分析:从扁平到立体
词法分析产生的是一维的 Token 序列,而程序本身是有层次结构的。语法分析的任务,就是将这个扁平的序列,组织成一棵能够反映代码结构的抽象语法树(AST)。
首先,我定义了 AST 的节点。这就像为程序搭建骨架,每一块骨骼都代表一种语法结构:
// 表达式的定义pub enum Expr { AtomExpr(AtomExpr, mut ty~ : Type?) // 原子表达式 (变量、字面量等) Unary(String, Expr, mut ty~ : Type?) // 一元运算:-, ! Binary(String, Expr, Expr, mut ty~ : Type?) // 二元运算:+, -, *, /} derive(Show, Eq, ToJson)// 语句的定义pub enum Stmt { Let(String, Type, Expr) // 变量声明:let x : Int = 5; If(Expr, Array[Stmt], Array[Stmt]) // 条件分支 While(Expr, Array[Stmt]) // 循环 Return(Expr?) // 返回 // ... 其他语句 ...} derive(Show, Eq, ToJson)// 函数和程序的顶层结构pub struct Function { name : String params : Array[(String, Type)] ret_ty : Type body : Array[Stmt]} derive(Show, Eq, ToJson)pub type Program Map[String, Function]
设计巧思:注意到每个表达式节点都有一个 mut ty~ : Type? 字段吗?这是一个可变的可选类型字段。这样,我就可以在后续的类型检查阶段,直接把推断出的类型信息“填”进去,而无需重新构建一棵新树,非常巧妙。
有了骨架,接下来就是用 递归下降 的方法来填充它。简单来说,就是为每一种语法结构(如函数、语句、表达式)编写一个解析函数。在 MoonBit 中,这又成了模式匹配的绝佳舞台:
pub fn parse_function(tokens : ArrayView[Token]) -> (Function, ArrayView[Token]) raise { // Function一定由fn关键字,Lower,左括号开头 guard tokens is [Keyword("fn"), Lower(fname), Bracket('('), .. rest_tokens] let params : Array[(String, Type)] = Array::new() let (tokens, ret_ty) = loop rest_tokens { // 参数格式:param_name : Type [Lower(param_name), Symbol(":"), Upper(type_name), .. rest] => { params.push((param_name, parse_type(type_name))) continue rest } [Symbol(","), .. rest] => continue rest [Bracket(')'), Symbol("->"), Upper(ret_ty), .. rest] => break (rest, parse_type(ret_ty)) } // ... 解析函数体}
整个过程就像剥洋葱,parse_program 调用 parse_function,parse_function 调用 parse_stmt,parse_stmt 调用 parse_expr,层层递进,直到把所有 Token 都消耗完毕。
💡 MoonBit 高级特性应用
derive(Show, Eq, ToJson):这个小小的注解威力巨大。Show 让我能轻松打印 AST 用于调试,Eq 用于测试,而 ToJson 能一键将 AST 序列化为 JSON,方便检查其结构。
raise 错误处理:通过在函数签名中标记 raise,我可以优雅地向上抛出解析错误,而不用到处传递错误码。
2、类型检查:确保语义正确
在代码生成之前,通常需要实现一个类型检查阶段。一些语句虽然符合语法,但可能不符合语义,例如我有一个foo函数,然后又有了1+foo这样的代码,但这是语义不正确的,因为一个整数无法与一个函数相加。
我设计了一个环境链来处理作用域:
pub struct TypeEnv[K, V] { parent : TypeEnv[K, V]? data : Map[K, V]}pub fn TypeEnv::get[K : Eq + Hash, V](self : Self[K, V], key : K) -> V? { match self.data.get(key) { Some(value) => Some(value) None => match self.parent { Some(parent_env) => parent_env.get(key) None => None } }}
类型检查器会自顶向下扫描每个表达式,填充类型信息并验证类型一致性:
pub fn Expr::check_type(self : Self, env : TypeEnv[String, Type]) -> Type raise { match self { Binary("+", lhs, rhs, ..) as node => { let lhs_type = lhs.check_type(env) let rhs_type = rhs.check_type(env) guard lhs_type == rhs_type else { raise TypeCheckError("类型不匹配") } node.ty = Some(lhs_type) lhs_type } // ... 其他表达式类型 }}
语法分析消耗的时间比预想的多一些,尤其是在处理运算符优先级时颇费脑筋。但在 MoonBit 强大的模式匹配和 AI 助手的帮助下,我还是在深夜前完成了这项工作。万里长征,只剩下最后一步——代码生成了。
第三天:代码生成,最后的难关
MoonBit本身语言特性适合编译器实现之外,也有一个超级好用的LLVM绑定,叫做llvm.mbt。
1、LLVM:现代编译器的基石
LLVM作为现代编译器基础设施的集大成者,为我们提供了一个强大而灵活的解决方案。通过将程序转换为LLVM中间表示(IR),我们可以利用LLVM成熟的工具链将代码编译到多种目标架构。
我们先来试用LLVM的经典起手三段式:
pub fn initialize_llvm() -> (Context, Module, Builder) { let ctx = @IR.Context::new() // LLVM上下文 let mod = ctx.addModule("demo") // 模块容器 let builder = ctx.createBuilder() // IR构建器 (context, module, builder)}
有了这三样法宝,我们就能像搭积木一样,一条条地构建出程序的 LLVM IR。比如生成一个计算 a*b+c 的函数:
pub fn generate_muladd_function() -> String { let (ctx, mod, builder) = initialize_llvm() // 定义函数签名:i32 muladd(i32, i32, i32) let i32_ty = ctx.getInt32Ty() let func_type = ctx.getFunctionType(i32_ty, [i32_ty, i32_ty, i32_ty]) let func_value = mod.addFunction(func_type, "muladd") // 创建函数入口基本块 let entry_block = func_value.addBasicBlock(name="entry") builder.setInsertPoint(entry_block) // 获取函数参数并生成计算指令 let arg_a = func_value.getArg(0).unwrap() let arg_b = func_value.getArg(1).unwrap() let arg_c = func_value.getArg(2).unwrap() let mul_result = builder.createMul(arg_a, arg_b) let add_result = builder.createAdd(mul_result, arg_c) let _ = builder.createRet(add_result) mod.dump()}
这会生成非常清晰的 LLVM IR:
define i32 @muladd(i32 %a, i32 %b, i32 %c) {entry: %mul_res = mul i32 %a, %b %add_res = add i32 %mul_res, %c ret i32 %add_res}
看起来很简单,对吧?但真正的挑战在于,如何将我们前一天生成的、复杂的 AST,系统性地翻译成这一条条的 LLVM 指令。
2、类型系统的映射
在LLVM中,类型系统相当复杂。llvm.mbt使用Trait Object的概念,&Type可以表示任意LLVM类型。我需要建立TinyMoonBit类型与LLVM类型的映射:
pub fn CodeGen::convert_type(self : Self, parser_type : Type) -> &@llvm.Type { match parser_type { Type::Unit => self.ctx.getVoidTy() as &@llvm.Type Type::Bool => self.ctx.getInt1Ty() Type::Int => self.ctx.getInt32Ty() Type::Double => self.ctx.getDoubleTy() }}
然后就是真正的代码生成。我需要为 AST 中的每一种节点编写一个 emit 方法。其中最有趣也最关键的,是如何处理变量和分支:
3、变量处理:SSA与可变性的桥梁
TinyMoonBit支持变量的重新赋值,但LLVM IR采用SSA(Static Single Assignment)形式,每个变量只能赋值一次。我需要采用alloca + load/store模式来处理可变变量:
// 变量声明:let x : Int = 5;Let(var_name, var_type, init_expr) => { let llvm_type = self.convert_type(var_type) let alloca = self.builder.createAlloca(llvm_type, name=var_name) env.symbols.set(var_name, alloca) let init_value = init_expr.emit(env) let _ = self.builder.createStore(alloca, init_value)}// 变量赋值:x = 10;Assign(var_name, rhs_expr) => { let var_ptr = env.get_symbol(var_name).unwrap() let rhs_value = rhs_expr.emit(env) let _ = self.builder.createStore(var_ptr, rhs_value)}
4、控制流:基本块的艺术
控制流是程序逻辑的骨架。在LLVM IR中,控制流通过基本块和分支指令来实现。每个基本块代表一个没有内部跳转的指令序列:
// if-else语句的实现If(cond, then_stmts, else_stmts) => { let cond_val = cond.emit(env) // 创建三个基本块 let then_block = func.addBasicBlock(name="if.then") let else_block = func.addBasicBlock(name="if.else") let merge_block = func.addBasicBlock(name="if.end") // 条件分支 let _ = builder.createCondBr(cond_val, then_block, else_block) // 生成then分支 builder.setInsertPoint(then_block) then_stmts.each(s => s.emit(env)) let _ = builder.createBr(merge_block) // 生成else分支 builder.setInsertPoint(else_block) else_stmts.each(s => s.emit(env)) let _ = builder.createBr(merge_block) // 继续在merge块生成后续代码 builder.setInsertPoint(merge_block)}
5、完整的代码生成
经过词法分析、语法分析、类型检查和代码生成四个阶段,我们的编译器已经能够将TinyMoonBit源代码转换为完整的LLVM IR。
对于我们的斐波那契例子:
fn fib(n : Int) -> Int { if n <= 1 { return n; } return fib(n - 1) + fib(n - 2);}
最终生成的LLVM IR:
define i32 @fib(i32 %0) {entry: %1 = alloca i32, align 4 store i32 %0, ptr %1, align 4 %2 = load i32, ptr %1, align 4 %3 = icmp sle i32 %2, 1 br i1 %3, label %4, label %64: %5 = load i32, ptr %1, align 4 ret i32 %56: %7 = load i32, ptr %1, align 4 %8 = sub i32 %7, 1 %9 = call i32 @fib(i32 %8) %10 = load i32, ptr %1, align 4 %11 = sub i32 %10, 2 %12 = call i32 @fib(i32 %11) %13 = add i32 %9, %12 ret i32 %13}
使用LLC工具链,我们可以进一步将LLVM IR编译成RISC-V汇编代码,完成整个编译过程。
攻克了这些核心难点后,剩下的工作就是水到渠成了。周三深夜,当我看到 fib(10) 的代码成功生成了复杂的 LLVM IR ,并顺利通过llc链接编译成可执行程序并且运行通过时,我知道,我成功了!
———日记结束———
总结
周四的午休时刻,张大胖向同事们展示了自己三天时间编写的TinyMoonBit编译器。
确实,MoonBit的模式匹配让词法分析变得异常简单,递归下降语法分析也很直观。
最关键的是llvm.mbt这个绑定库,让代码生成变得容易很多。
有了MoonBit以后,开发新语言就不那么难了,也许你也可以再把编译原理捡起来,用MoonBit完成自己的年轻时的梦想:实现一个自己的编程语言!
资源推荐
对这个项目感兴趣的读者,可以从以下链接获取更多信息:
TinyMoonBit 完整项目 :
https://github.com/Kaida-Amethyst/TinyMoonbitLLVM
MoonBit 官方文档 :
https://www.moonbitlang.com/docs/
llvm.mbt 文档 :
https://mooncakes.io/docs/Kaida-Amethyst/llvm
LLVM 官方教程 :
https://www.llvm.org/docs/tutorial/