自己动手设计并实现一个动态类型编程语言

背景

前几年主要在搞"编译优化"相关的事情(热修框架、字节码hook框架、字节码优化之类的)不过其实严格来说做的这些跟"编译"没啥关系,只是熟悉Java字节码以及对asm、javassist这类库的api比较熟而已,只是叫"编译优化"听起来高大上一些😂

当时也想着去写一个真正的"编译器",看了一点"龙书"、"编译器设计"之类比较有名的书籍,发现连实现一个词法分析器似乎都很困难,直接就劝退了。

后来因为业务需要,开发了一个javac的插件:javac 编译器插件在代码迁移中的简单应用,插件本身实现比较简单,当时搞这个的时候顺带看了下javac的代码,似乎并不算太复杂。于是就仿照他也写个自己的Java编译器,实现了部分的语法,不过没有坚持写下去,部分原因是我当时的实现方案无法实现apt之类的机制。

虽然只实现了很小的一部分,但对我而言收益还是不小的,比如:

  1. 这是我实现的第一个手写的词法分析器,语法分析器,算是迈出了编译器开发的第一步
  2. 此外还让我发现了一个多年来都未曾注意到的Java语法:java里面返回多维数组的时候,数组的维度可以分别写在函数声明的左右两边,维度是两边之和,所以下面的三个声明是等效的,都是返回一个二维数组,虽然知道这个没啥价值,但若不是尝试写这个小编译器我可能一直不会知道这一点。
java 复制代码
private static int[][] f() {...}
private static int[] f()[] {...}
private static int f()[][] {...}

近期在搞一些稳定性相关的事情,有些时候需要通过hook来定位或者修复问题,不免要看看虚拟机的代码,不考虑各种优化,就解释执行而言似乎也不是特别复杂,又临近五一假期,就想着实现个动态类型的编程语言,这样编译器、虚拟机开发就都能体验一遍,于是就有了这个项目--charon

charon

目前实现的特性如下:

  1. 支持的类型:bool、long、double、string、function、class
  2. 函数可以赋值给变量、类的字段、作为参数或者返回值(method跟function不同,不是first-class类型,不能赋值给变量,类字段,也不能作为函数、方法的参数或者返回值)
  3. 支持常见的语言结构,比如:if-elseif-else,while-break-continue
  4. 支持定义类、方法,方法中可以通过'this'访问当前实例的字段
  5. 支持简单的ffi机制,用于实现charon做不到的事情,比如打印输出: __print, __println

具体的语法&语意后面预计会在"语法介绍&语法分析器实现"的文章中提一下,比如在charon中类似于rust:变量是可以重定义的,赋值是语句而不是表达式,if的'then'是block而不是一般的语句等。

代码仓库&构建

代码仓库:github.com/0x264/charo...

构建:

  1. 需要先安装rust,如果之前未安装过,可以先按rust官网的指示去安装一下
  2. clone一下代码仓库
  3. cd 到 charon 项目根目录
  4. 执行:cargo build --release

执行完上面的步骤后,会在target/release目录下生成3个可执行程序:

  1. charonc: charon 的编译器,会将代码编译成字节码,文件后缀:.charonbc,字节码格式类似于Java字节码
  2. charonp: 反汇编charonc生成的字节码文件.charonbc,功能类似于javap
  3. charon: 虚拟机可执行程序,可以传入charon源代码,也可以传入charonc编译生成的字节码

代码示例

工程根目录下有个examples,里面有一些示例代码可以参考。

下面用charon构建一棵二叉树,并进行中序遍历作为一个例子:

ini 复制代码
#!/usr/bin/env charon

// 调用createBinaryTree创建一棵二叉树,然后通过inOrder进行中序遍历
inOrder(createBinaryTree());

class Node {}

//              10
//            /    \
//           6      14
//         /  \    /  \
//        4    8  12  16
//
func createBinaryTree() {
    var left = Node();
    left.value = 4;

    var right = Node();
    right.value = 8;

    var l = Node();
    l.value = 6;
    l.left = left;
    l.right = right;

    var left = Node();
    left.value = 12;

    var right = Node();
    right.value = 16;

    var r = Node();
    r.value = 14;
    r.left = left;
    r.right = right;

    var root = Node();
    root.value = 10;
    root.left = l;
    root.right = r;

    return root;
}

func inOrder(root) {
    if (root.left) {
        inOrder(root.left);
    }
    __println(root.value);
    if (root.right) {
        inOrder(root.right);
    }
}

上面的代码示例可以有如下几种方法执行:

  1. 直接调用虚拟机执行:

    • charon binary-tree.charon
  2. 先编译成字节码,再交由虚拟机执行

    • charonc binary-tree.charon
    • charon binary-tree.charonbc
  3. charon是支持shebang,因此可以给源码文件加上可执行权限,然后直接执行

    • chmod +x binary-tree.charon
    • ./binary-tree.charon

另外从上面的代码可以看到:

  1. 函数不必提前声明即可使用
  2. 变量可以重定义
  3. 跟很多动态语言一样类的字段不必在类体中定义
  4. 对于未赋值的类字段,读取时将得到null
  5. 像null之类的值在作为bool表达式时会隐式转换为false
  6. ...

后续计划

  • 后面计划整理几篇相关的文档以期加深对编译器&虚拟机相关技术的理解,预计会补充以下几篇文章:

    1. 词法介绍&词法分析器的实现
    2. 语法介绍&语法分析器的实现
    3. 语意分析、字节码设计&字节码生成(charon是基于栈的,也会和基于寄存器的实现简单对比下)
    4. 虚拟机的实现(比如栈帧的布局,对于method 'this'的支持实现和java不太相同,以及ffi的支持,后面会简单讨论一下)
    5. 编译器&虚拟机中的错误处理(编译过程中的行号、列号信息处理、关于错误恢复的讨论,运行时的栈溢出检测等)
  • 虽然是个玩具项目,但开发他还是学到了一些东西:比如之前我一直以为熟悉Java字节码,但对于像max_locals这样的信息是否必要,以及虚拟机如何使用?虚拟机在调用方法,以及调用外部方法的时候如何进行栈帧布局的?等等在此之前我其实并不清楚。不过这个语言还是太小了,而且我也不会真的去用,所以即使加上gc等机制可能也没有test case去测,如果后面有时间的话可能会考虑学习写个简单的jvm😂

受制于能力和时间限制,原本想支持的一些特性,比如类的自定义构造函数等等没来得及搞,原本想支持一个简单的gc的,也没来得及做,并且测试也很少😂,若有大佬发现bug或者有建议,希望帮忙指正~

相关推荐
叶庭云4 小时前
Matlab 和 R 语言的数组索引都是从 1 开始,并且是左闭右闭的
matlab·编程语言·r·数组索引·从 1 开始
夜阳朔5 小时前
《C++ Primer》第三章知识点
c++·编程语言
MoonBit月兔21 小时前
MoonBit 核心编译器正式开源!
开发语言·开源·编程语言·moonbit
Amd7942 天前
数据库与编程语言的连接
mysql·编程语言·应用开发·数据库连接·crud操作·数据访问·数据库驱动
Moonbit3 天前
编程实践|用 MoonBit 实现线段树(三)
编程语言
luoganttcc7 天前
【编译器】传统编译器和AI/ML编译器总结
人工智能·编译器
MoonBit月兔11 天前
GitHub 正式收录 MoonBit 作为一门通用编程语言!核心用户突破三万!
开发语言·github·编程语言·moonbit