1 个不稳定版本
0.1.0 | 2023年5月12日 |
---|
#252 在 编程语言
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
- 该语言不是面向表达式的,因此您必须显式
set
和ret
值。
没有中缀运算符:
- 我们有内置函数,如
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