【More than Coding】用 SOLO 从零迭代出一门 400+ 测试的中英双语编程语言

【More than Coding】用 SOLO 从零迭代出一门 400+ 测试的中英双语编程语言


一、摘要

我是一名对编程语言设计充满好奇的独立开发者。面对“如何从零实现一门编程语言”这个复杂命题,我从一个模糊的念头——让中文母语者能无痛写代码——出发,经过多轮迭代设计,最终打造出 Nuzo:一门支持中英文双语关键字、拥有 400+ 测试用例、采用字节码虚拟机架构的完整编程语言。整个项目完全由 Rust 实现,零外部依赖,从词法分析到 GC 雏形,从面向对象到闭包支持,从 0 行代码到可运行的解释器,Nuzo 用实践证明了:一个人也能从头造出一门像模像样的语言。


二、背景

我一直在思考一个问题:为什么编程一定要用英文?

对于中文母语者,尤其是编程初学者来说,ifwhilereturn 这些关键字是一道无形的墙。学习编程的逻辑本身已经足够抽象,还要同时记住一整套陌生语言的词汇,这让很多人望而却步。

我做过一些调研:市面上虽然有易语言等中文编程方案,但它们要么生态封闭,要么语法陈旧,要么与现代编程范式脱节。我想要创造的是一种更现代、更通用、同时拥抱中英文的编程语言——它既能让初学者用中文写出人生第一行代码,也能让有经验的开发者用习惯的英文关键字无缝切换。

另外,我一直对编程语言底层实现充满好奇:词法分析是如何工作的?递归下降解析器怎么写?字节码编译器呢?栈式虚拟机怎么跑起来?与其看一万篇理论文章,不如亲手写一个。

于是我给自己定了一个目标:从零实现一门完整、可运行、有测试覆盖的编程语言。这就是 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 都要在运行时检查 ab 的类型再决定做整数加法、浮点加法还是字符串拼接。这导致即使是最简单的数学运算也比静态类型语言慢一个数量级。

关键决策:

  • 投机缓存:受 V8 引擎的 Inline Cache 启发,为二元操作实现了投机缓存。VM 记录上一次执行 + 操作时的操作数类型,如果下一次操作数类型相同,直接走快速路径,跳过类型检查。如果类型变了(投机失败),回退到普通路径并更新缓存。
  • 热路径逃逸指令:为高频出现的循环体生成逃逸指令,让 VM 能在检测到热点后直接跳转优化路径。
  • 整数快速路径:在编译阶段识别出整数常量和纯整数表达式,生成专用整数操作指令,绕过通用算术指令的动态分发。

产出:

  • PIC 投机缓存系统
  • 整数运算快速路径
  • 整体算术运算性能提升约 3-5 倍(在循环密集的 Fibonacci 基准测试中测得)

踩坑:
投机缓存的失效条件设计非常微妙。不是简单的“类型变了就失效”——因为 Nuzo 是动态类型,x + yx=1, y=2x=1.0, y=2.0 时需要走不同的指令路径。但 x=1, y=2x=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

五、效果与总结

提效数据

  • 传统方式:从零实现一门编程语言,通常需要阅读大量编译器理论资料 + 参考开源实现,对于一个独立开发者来说,仅搭建可运行的解释器原型就可能需要数周甚至数月。
  • 实际过程:通过系统性的分阶段迭代,每一步有清晰的目标和可验证的产出(可运行的代码 + 通过测试的用例),整个核心开发在业余时间里高效推进。

分阶段迭代的方法论

回顾整个过程,一个清晰的模式浮现出来:

  1. 先跑通最小闭环:不是一开始就设计所有特性,而是先让“一个表达式求值”能跑起来,然后逐步加变量、加函数、加类。
  2. 每次迭代有可验证的产出:每加一个新特性,同步写对应的测试用例,确保新代码没有破坏旧逻辑。
  3. 性能优化基于实测数据:不凭直觉优化,而是写基准测试,找到真正的瓶颈再动手。
  4. 拥抱复杂性:闭包、GC、PIC 这些“高级特性”看起来很吓人,但拆解成小步之后,每一步都是可以啃下的。

个人思考

这次实践让我对编程语言有了“从用户到创造者”的认知跃迁。

以前用 for 循环觉得理所当然,直到自己实现了解释 for 的解析器、编译器、VM 指令,才真正明白一层语法糖背后藏着多少底层机制。以前对闭包的理解停留在“函数套函数”,直到亲手处理 Upvalue 的栈到堆提升,才感受到闭包的内存管理精妙之处。

最大的收获是:造轮子是学习一个领域最深入的方式。看十本书不如亲手写一个解释器,读一百篇 VM 优化文章不如自己实现一个投机缓存。

Nuzo 目前还不完美——它没有完善的 GC,错误信息不够友好,标准库还很单薄。但它已经证明了:一个人、一门 Rust 程序、几百个小时的投入,完全可以创造出一门有完整语法、有虚拟机、有测试体系、甚至还有性能优化的编程语言。

如果你也对编程语言设计感兴趣,不妨 clone 下 Nuzo 的代码跑一跑,或者自己从零开始写一个解释器。那种“自己创造的语言执行了自己写的代码”的成就感,会是你编程生涯里最独特的体验之一。


项目地址https://github.com/nimamasl114514/nuzo

当然了以上全部都是ai开发,人类主导的架构设计,希望大家发现ai的真正实力
开源协议:Apache License 2.0
测试通过:white_check_mark: 439/439