三天时间,我用这门国产编程语言写了个编译器,同事们看后大为震惊!

张大胖很幸运,刚毕业就进入了一家芯片设计公司。

不过,工作了一个月以后,他就觉得不对劲了。

原因很简单,对于公司开发的最新芯片,主流的编程语言还不支持呢!

无论是验证特性,还是编写测试,都不得不用最最古老的形式:汇编!

张大胖决心改变这种状况,他找到了梁经理,说出了自己的想法。

确实是这样,操作系统、图形学,编译器号称程序员的三大浪漫,但每一项真做起来都让人头大。

张大胖当年的毕业设计,一个极其简单的编译器,就让他做了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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值