【More than Coding】用 SOLO 从零迭代出一门 400+ 测试的中英双语编程语言
一、摘要
我是一名对编程语言设计充满好奇的独立开发者。面对“如何从零实现一门编程语言”这个复杂命题,我从一个模糊的念头——让中文母语者能无痛写代码——出发,经过多轮迭代设计,最终打造出 Nuzo:一门支持中英文双语关键字、拥有 400+ 测试用例、采用字节码虚拟机架构的完整编程语言。整个项目完全由 Rust 实现,零外部依赖,从词法分析到 GC 雏形,从面向对象到闭包支持,从 0 行代码到可运行的解释器,Nuzo 用实践证明了:一个人也能从头造出一门像模像样的语言。
二、背景
我一直在思考一个问题:为什么编程一定要用英文?
对于中文母语者,尤其是编程初学者来说,if、while、return 这些关键字是一道无形的墙。学习编程的逻辑本身已经足够抽象,还要同时记住一整套陌生语言的词汇,这让很多人望而却步。
我做过一些调研:市面上虽然有易语言等中文编程方案,但它们要么生态封闭,要么语法陈旧,要么与现代编程范式脱节。我想要创造的是一种更现代、更通用、同时拥抱中英文的编程语言——它既能让初学者用中文写出人生第一行代码,也能让有经验的开发者用习惯的英文关键字无缝切换。
另外,我一直对编程语言底层实现充满好奇:词法分析是如何工作的?递归下降解析器怎么写?字节码编译器呢?栈式虚拟机怎么跑起来?与其看一万篇理论文章,不如亲手写一个。
于是我给自己定了一个目标:从零实现一门完整、可运行、有测试覆盖的编程语言。这就是 Nuzo 的由来。
三、实践过程
第 1 轮:搭建骨架——词法分析器 + 语法解析器
起点:
语言设计的起点很朴素——定义关键字和语法规则,然后让计算机能“读懂”源码。我需要一个词法分析器(Lexer)把源代码切成 Token,再用一个语法解析器(Parser)把这些 Token 组合成一棵抽象语法树(AST)。
关键决策:
- 双关键字系统:从一开始就设计为每个关键字都有中英文两个版本,内部统一用一个枚举类型表示。比如
Token::Let既对应let也对应让,Token::If对应if和如果。这意味着词法分析器在切词阶段就要同时识别两套关键字。 - 递归下降解析器:选择手写递归下降解析器而非使用 Parser Generator,这样对语法规则的掌控更精细,也能提供更好的错误提示。
- Pratt 解析法处理表达式:运算符优先级是解析器的经典难点,采用 Pratt 解析法优雅地处理了
1 + 2 * 3这种优先级问题。
产出:
- 完整的词法分析器,支持 30+ 个中英文关键字、字符串插值、位运算符
- 递归下降语法解析器,支持变量声明、控制流、函数定义、类定义等完整语法
- 能处理语法错误并给出修复建议
踩坑:
中文关键字在 UTF-8 编码下占多个字节,早期在处理字符串偏移量时频繁索引越界。后来统一使用字节偏移 + char_indices 解决了这个问题。此外,中英文混合使用时(比如 如果 x < 3 { 返回 x; })要确保解析器能正确切换上下文,调试了很久。
第 2 轮:赋予灵魂——编译器 + 字节码虚拟机
需求升级:
有了 AST,下一步是让它“跑起来”。我需要一个编译器把 AST 编译成字节码,再写一个虚拟机执行这些字节码。
关键决策:
- 栈式虚拟机:选择栈式而非寄存器式,实现更直观,指令更简洁。总共定义了 45+ 个操作码,涵盖常量加载、变量存取、算术运算、比较跳转、函数调用等。
- 编译器优化:在编译阶段做了几项优化:
- 符号驻留,减少重复字符串的内存占用
- 基本块分析,标记循环入口和分支目标
- 整数运算特化,为高频整数操作生成专用指令,绕过动态类型分发
- 闭包实现:闭包是函数式编程的灵魂。采用“函数对象 + 捕获上值”的经典方案,Upvalue 在栈上分配并在闭包逃逸时自动提升到堆上。
- 尾调用优化:TCO 是递归高效执行的关键。在 VM 执行尾调用指令时直接复用当前栈帧,避免栈溢出。
产出:
- 一个能跑起来的字节码编译器
- 一个完整实现的栈式虚拟机,支持 45+ 操作码
- 闭包、TCO 全部就位
踩坑:
闭包的 Upvalue 管理是最难啃的骨头。当一个闭包引用了另一个闭包捕获的变量时,Upvalue 链的关闭逻辑非常容易出错。为此专门写了一个 Upvalue 测试套件,覆盖嵌套闭包、逃逸闭包、递归闭包等 30+ 场景,修了无数个内存安全的 Bug。
第 3 轮:丰富生态——面向对象 + 内置类型系统
需求升级:
一门语言不能只有基本类型和函数,还需要面向对象能力和丰富的内置数据结构。
关键决策:
- 类和继承:实现
class/类关键字和extends/继承机制。采用原型链式的方法查找,实例字段存在对象自身,方法存在类对象上。 - 内置类型体系:
Int(整数)、Float(浮点数)、Bool(布尔)、String(字符串)、Null(空值)List(列表,动态数组实现)、Dict(字典,哈希表实现)、Tuple(元组,不可变)Range(范围对象,支持1..10语法)
- 内置函数库:提供
print(打印)、len(长度)、push/pop(列表操作)、keys/values(字典操作)等高频函数。 - 模式匹配:
match表达式支持结构匹配和条件守卫,比switch更强大灵活。 - 异常处理:
try-catch块,支持异常冒泡和自定义异常。
产出:
- 完整的面向对象系统
- 7 种内置数据类型
- 20+ 内置函数
- 模式匹配和异常处理机制
踩坑:
字典的哈希表实现踩了 Rust 所有权模型的大坑。因为 Nuzo 的值类型需要在 VM 堆上分配,而哈希表的键查找又需要比较值是否相等,涉及到 Rust 的引用和借用检查。最终通过 Rc<RefCell<>> 包装值类型 + 自定义哈希和相等性比较接口解决了问题。
第 4 轮:性能攻坚——投机缓存 + 热路径优化
需求升级:
动态类型语言的一大痛点是性能。每次 a + b 都要在运行时检查 a 和 b 的类型再决定做整数加法、浮点加法还是字符串拼接。这导致即使是最简单的数学运算也比静态类型语言慢一个数量级。
关键决策:
- 投机缓存:受 V8 引擎的 Inline Cache 启发,为二元操作实现了投机缓存。VM 记录上一次执行
+操作时的操作数类型,如果下一次操作数类型相同,直接走快速路径,跳过类型检查。如果类型变了(投机失败),回退到普通路径并更新缓存。 - 热路径逃逸指令:为高频出现的循环体生成逃逸指令,让 VM 能在检测到热点后直接跳转优化路径。
- 整数快速路径:在编译阶段识别出整数常量和纯整数表达式,生成专用整数操作指令,绕过通用算术指令的动态分发。
产出:
- PIC 投机缓存系统
- 整数运算快速路径
- 整体算术运算性能提升约 3-5 倍(在循环密集的 Fibonacci 基准测试中测得)
踩坑:
投机缓存的失效条件设计非常微妙。不是简单的“类型变了就失效”——因为 Nuzo 是动态类型,x + y 在 x=1, y=2 和 x=1.0, y=2.0 时需要走不同的指令路径。但 x=1, y=2 和 x=3, y=4 可以复用同一条缓存。这个粒度需要在缓存的 key 设计里精确控制。
第 5 轮:质量保障——测试体系 + 持续迭代
需求升华:
对于一门编程语言来说,“看起来能跑”和“真的正确”之间有一道鸿沟。语言的每个特性都必须经过大量边界测试,任何一个未覆盖的角落都可能是 Bug 的温床。
关键决策:
- 三层测试体系:
- 单元测试:针对词法分析器、解析器、编译器、VM 的独立模块,386 个
- 集成测试:从源码到执行的端到端测试,52 个,覆盖语法特性、类型系统、错误处理等
- 文档测试:确保 README 中的示例代码能正确运行,1 个
- 总计 439 个测试用例,涵盖了:
- 运算符优先级所有组合
- 闭包嵌套和逃逸
- 类继承链和多态
- 模式匹配的各种边界
- 异常冒泡和嵌套 try-catch
- 中英文关键字混合使用
产出:
- 439 个测试用例全部通过
- 一个覆盖全特性、边界清晰的测试套件
踩坑:
写集成测试时发现了一个隐蔽的 Bug:如果 (a = 1) { } 这样的赋值表达式在 if 条件里。这在语法上是合法的(赋值表达式返回被赋的值),但大多数情况下是程序员手误写错了 ==。最终在解析器层面加了警告,同时让 VM 正确执行这一语义(为了语言的一致性)。
四、成果展示
最终交付物:Nuzo 编程语言
语言特性总览:
| 特性 | 描述 |
|---|---|
| 中英双语 | let/让、if/如果 等 30+ 关键字完全等价 |
| 动态类型 | Int、Float、Bool、String、Null、List、Dict、Tuple、Range |
| 面向对象 | class/类、extends/继承、方法、字段 |
| 函数式 | 闭包、高阶函数、尾调用优化 |
| 控制流 | if-else、while、for、match 模式匹配 |
| 异常处理 | try-catch、异常冒泡 |
| 模块化 | 支持多文件、import 导入 |
代码示例(中英文对照):
// 英文版
fn fibonacci(n) {
if n < 2 { return n; }
return fibonacci(n - 1) + fibonacci(n - 2);
}
print fibonacci(10); // 55
// 中文版
函数 斐波那契(n) {
如果 n < 2 { 返回 n; }
返回 斐波那契(n - 1) + 斐波那契(n - 2);
}
打印 斐波那契(10); // 55
// 闭包 + 面向对象
类 计数器 {
新(初始值) {
自我.数值 = 初始值;
}
递增() {
自我.数值 = 自我.数值 + 1;
返回 自我.数值;
}
}
让 计数器1 = 计数器.新(0);
打印 计数器1.递增(); // 1
打印 计数器1.递增(); // 2
打印 计数器1.递增(); // 3
版本迭代历程
| 阶段 | 核心变化 | 测试用例数 |
|---|---|---|
| 骨架搭建 | 词法分析器 + 语法解析器 | ~50 |
| 虚拟机实现 | 编译器 + 字节码 VM + 闭包 | ~150 |
| 生态丰富 | 面向对象 + 7 种类型 + 20+ 内置函数 | ~300 |
| 性能攻坚 | 投机缓存 + 整数快速路径 | ~350 |
| 质量保障 | 全面测试 + 边界覆盖 | 439 |
| 总计 | 完整编程语言 | 439 |
五、效果与总结
提效数据
- 传统方式:从零实现一门编程语言,通常需要阅读大量编译器理论资料 + 参考开源实现,对于一个独立开发者来说,仅搭建可运行的解释器原型就可能需要数周甚至数月。
- 实际过程:通过系统性的分阶段迭代,每一步有清晰的目标和可验证的产出(可运行的代码 + 通过测试的用例),整个核心开发在业余时间里高效推进。
分阶段迭代的方法论
回顾整个过程,一个清晰的模式浮现出来:
- 先跑通最小闭环:不是一开始就设计所有特性,而是先让“一个表达式求值”能跑起来,然后逐步加变量、加函数、加类。
- 每次迭代有可验证的产出:每加一个新特性,同步写对应的测试用例,确保新代码没有破坏旧逻辑。
- 性能优化基于实测数据:不凭直觉优化,而是写基准测试,找到真正的瓶颈再动手。
- 拥抱复杂性:闭包、GC、PIC 这些“高级特性”看起来很吓人,但拆解成小步之后,每一步都是可以啃下的。
个人思考
这次实践让我对编程语言有了“从用户到创造者”的认知跃迁。
以前用 for 循环觉得理所当然,直到自己实现了解释 for 的解析器、编译器、VM 指令,才真正明白一层语法糖背后藏着多少底层机制。以前对闭包的理解停留在“函数套函数”,直到亲手处理 Upvalue 的栈到堆提升,才感受到闭包的内存管理精妙之处。
最大的收获是:造轮子是学习一个领域最深入的方式。看十本书不如亲手写一个解释器,读一百篇 VM 优化文章不如自己实现一个投机缓存。
Nuzo 目前还不完美——它没有完善的 GC,错误信息不够友好,标准库还很单薄。但它已经证明了:一个人、一门 Rust 程序、几百个小时的投入,完全可以创造出一门有完整语法、有虚拟机、有测试体系、甚至还有性能优化的编程语言。
如果你也对编程语言设计感兴趣,不妨 clone 下 Nuzo 的代码跑一跑,或者自己从零开始写一个解释器。那种“自己创造的语言执行了自己写的代码”的成就感,会是你编程生涯里最独特的体验之一。
项目地址:https://github.com/nimamasl114514/nuzo
当然了以上全部都是ai开发,人类主导的架构设计,希望大家发现ai的真正实力
开源协议:Apache License 2.0
测试通过:
439/439