1 个不稳定版本

0.1.0 2023年5月12日

#252编程语言

MIT/Apache

230KB
6.5K SLoC

bappy-script

只是随便玩玩一个玩具编译器。

bappy-script 主要是一个学习静态类型系统如何工作的游乐场 (checker.rs)。与解析器和解释器相比,这些都是次要的。

一切测试程序

此程序大致演示了语言能做什么,包括

基础

  • 基本原语 (Bool, Int, Str, ())
  • 基本控制流 (ret, if, else, loop, break, continue)
  • 基本可变变量 (let, set)
  • 内置函数提供加/减/乘的数学操作和等/不等条件

类型

  • 一等函数(都是闭包)
    • 闭包在执行到达其声明点时按值捕获状态。
  • 结构化积类型(元组)
  • 命名积类型(结构体)

分析

  • 可选静态类型检查
    • 由于表达式当前总是具有已知类型,因此可以推断变量的类型。
    • 函数必须始终声明参数/返回类型的类型
      • 尽管如果省略返回类型,则假定它是 ()
    • 正确处理命名类型的阴影和作用域
  • 静态验证变量访问是否在作用域内
  • 静态验证控制流(不能在循环外 continue
  • 静态计算闭包捕获
fn print_1d_point() {
    struct Point {
        x: Int
    }
    let x = Point { x: 1 }
    print x
    ret ()
}

let _ = print_1d_point()
let print_point: fn() -> () = print_1d_point
let _ = print_point()

let tuple = (1, (true, "hello"), false)
if tuple.1.0 {
    struct Point {
        x: Int
        y: Int
    }

    let captured_point = Point { x: 2, y: 4 }
    fn print_2d_point() {
        print captured_point
        ret ()
    }

    let _ = print_2d_point();
    set print_point = print_2d_point
}

struct Point {
    x: Int
    y: Int
    z: Int
}

fn print_3d_point() -> Int {
    let pt: Point = Point { x: 3, y: 5, z: 7 }
    print pt
    ret add(add(pt.x, pt.y), pt.z)
}

fn print_many() {
    print "3 more times!!!"
    let counter = 3
    loop {
        if eq(counter, 0) {
            break
        }
        set counter = sub(counter, 1)
        let _ = print_3d_point()
    }
    ret ()
}

let _ = print_1d_point()
let _ = print_point()
let res = print_3d_point()
print res
let _ = print_many()
ret res

解析器和语法

解析器不好(脆弱),因为我只想有一个简单且易于扩展的东西。我认为它技术上属于“递归下降”,但我不喜欢,所以标记了,所以我也不知道。解析器有最糟糕的错误,没有恢复,因为我根本不在乎。

语法主要基于 Rust 的,因为这是一个相当干净且明确且我感到舒适的语法。

既然你讨厌解析,为什么不做成 Lisp 变体呢?

我真的很擅长阅读/编写 Lisp 东西,所以这感觉是个人努力和舒适度之间的良好权衡。此外,当它看起来像 Rust 代码时,我更容易直观地了解某物“应该如何”工作,因为那是我最擅长的语言。

此外,如果我告诉某人它是 Rust 代码,语法高亮基本上就会起作用。

显著的偏差

换行符非常重要。这很糟糕,但大多数时候你根本不会注意到。在我看来,这只有在非常复杂的表达式和函数声明时才会真正影响。作为安慰,我至少让你不必写分号,因为换行符基本上是分号?

许多事物必须单独占一行

  • 语句(包括所有子表达式)
    • letx:MyType= func1(func2(a,b,c),d)
    • ret x
    • ...
  • “标题”块
    • struct MyStruct {
    • fn (a:Int,b:Bool) ->Int {
    • ifx{
    • } else {
  • 结构字段(x: Int
  • 闭合括号(}

有些东西很啰嗦,因为我想让解析器和解释器变得简单

  • 表达式不是有效的语句。所以函数调用必须作为更大语句的一部分。
    • 通常 let _ = func() 是仅为了副作用调用函数的最简单方式。
  • 将值赋给现有的变量之前必须加上 set
    • set x=y
  • 该语言不是面向表达式的,因此您必须显式 setret 值。

没有中缀运算符:

  • 我们有内置函数,如 add(x, y)eq(x, y),最后提供

检查器(静态分析)

这基本上是我最感兴趣的地方,所以你会发现 src/checker.rs 将有最多的注释和讨论。

目前所有分析都是通过递归遍历 AST 直接完成的。程序中的所有变量和类型在每个点都会被跟踪,但这些信息是可变的和瞬时的(有点像我们正在“执行”程序)。

理想情况下,我想写一些可以创建(SSA?)控制流图的工具,以促进其他分析,如确定初始化(Rust 中的所有权系统的主要部分,也是任何优秀编译器的标准功能,因为它可以让你报告像“分配给变量的值从未被使用”这样的信息)。

我可能还对玩弄泛型和 也许 高阶类型感兴趣?但我真的不确定如何最好地表示和实现这些内容(或者更确切地说,我感到遗憾,类型比较可能比比较类型 ID 复杂)。

重复强调示例中的说明,当前类型系统支持

基础

  • 基本原语 (Bool, Int, Str, ())
  • 基本控制流 (ret, if, else, loop, break, continue)
  • 基本可变变量 (let, set)

类型

  • 一等函数(都是闭包)
    • 闭包在执行到达其声明点时按值捕获状态。
  • 结构化积类型(元组)
  • 命名积类型(结构体)

分析

  • 可选静态类型检查
    • 由于表达式当前总是具有已知类型,因此可以推断变量的类型。
    • 函数必须始终声明参数/返回类型的类型
      • 尽管如果省略返回类型,则假定它是 ()
    • 正确处理命名类型的阴影和作用域
  • 静态验证变量访问是否在作用域内
  • 静态验证控制流(不能在循环外 continue
  • 静态计算闭包捕获

解释器

这是一个相当简单的“无类型”AST 解释器。运行时值具有基本类型标记,这样我们就可以检查某个值是否是函数或布尔值,因为只有在那些类型有意义的地方才需要。

它几乎完全独立于检查器,但闭包捕获分析为 AST 添加了一些所需的信息。

不依赖于检查器有助于捕捉检查器中的错误。

没有太多优化。每次值在语义上移动时,它都会被克隆(并且像元组、闭包和结构体这样的东西都包含 Vecs!)。但即便如此,在强大的工作机上编译和评估当前测试中的 ~100 个程序基本上是瞬时的。

在解释器中,生命周期比实际应该有的要多。我特意将所有字符串字面量保留为对原始程序文本的指针,因为这对我来说很重要。

依赖项

~1MB
~20K SLoC