Skip to content

Commit a5b6c26

Browse files
committed
docs: add more dev tutorial
1 parent 3a4f48d commit a5b6c26

File tree

10 files changed

+130
-15
lines changed

10 files changed

+130
-15
lines changed

book/src/SUMMARY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
- [Module](./references/module.md)
1313
- [Method](./references/method.md)
1414
- [Trait](./references/interface.md)
15-
- [Immix Gc](./references/immix.md)
16-
- [Stack Map](./references/stackmap.md)
1715

1816
# How the project works internally
1917

18+
- [Dev Prepare](./dev-prepare.md)
2019
- [Compiler](./compiler/README.md)
2120
- [Parser](./compiler/parser.md)
2221
- [AST](./compiler/ast.md)
@@ -27,6 +26,8 @@
2726
- [System library](./systemlib/README.md)
2827
- [vm](./systemlib/vm.md)
2928
- [gc](./systemlib/gc.md)
29+
- [Immix Gc](./systemlib/immix.md)
30+
- [Stack Map](./systemlib/stackmap.md)
3031
- [planglib](./systemlib/planglib.md)
3132

3233
# CONTRIBUTING

book/src/compiler/README.md

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,96 @@
11
# Compiler
22

3-
pivot-lang编译器(以下简称编译器)主要由三个部分组成:nom分析器、ast和llvm后端。
43

4+
本文档会总体介绍一遍Pivot-Lang编译器的整体工作流程,帮助开发人员快速上手开发。
5+
6+
## 编译器基本工作流程
7+
8+
```mermaid
9+
graph LR
10+
A[Source Code] --> C[nom parser]
11+
C --> D[AST]
12+
D --> E[LLVM backend]
13+
E --> F[Object File]
14+
F --> |linker| G[Executable File]
15+
```
516

617
## Nom parser
718

8-
nom parser包含了编译器的词法分析和语法分析部分。nom parser的主要功能是使用递归下降法将pivot-lang源代码转换为ast。
19+
nom parser包含了编译器的词法分析和语法分析部分。nom parser的主要功能是使用递归下降法将pivot-lang源代码转换为ast。
20+
21+
### A Basic Tour of nom
22+
23+
nom库是纯粹的函数式思想,对于接触函数式比较少的同学可能第一次看到会一脸懵,不过其实他的用法并不困难。
24+
第一次看到nom这种函数式的代码就能自己搞懂函数式编程的思路是很难的,但是你肯定能轻松做到看懂下方教程中的parse
25+
样例。当你看懂了下方的例子再去看我们parser的源码,我相信你只要花一些时间就能搞懂大部分的内容
26+
27+
nom本质是一系列封装好的parser函数和**处理parser的函数**(被称为combinator)的集合,所有的parser函数都有类似的泛型签名,例如他们的返回值一定是:
28+
29+
```rust, ignore
30+
31+
Result<(I, O), E>
32+
33+
```
34+
`parser`函数一般是一个泛型函数,拥有三个类型参数 `I``O``E` ,分别代表输入的类型,输出的类型和错误的类型。其中,`I` 是指输入的类型,通常在编译器中输入的数据类型是 `Span``E` 的类型是 `()`,因为在编译器中为了进行错误容忍我们一般不会在parse阶段抛出任何错误。`O` 的类型是我们构建的语法树的节点类型,它应该反映输入中提供的语言结构。在 `parser` 函数中,函数的输入参数类型通常是一个类型为`I`的值,并且输出的内容中的 `I` 表示自身处理后剩余的未处理内容。
35+
36+
举例来说,nom中的`tag`函数能产生一个接受指定值的parser:
37+
38+
```rust,ignore
39+
let parser = tag("123")
40+
let (remain, output) = parser("12345").unwrap();
41+
assert_eq!(remain, "45");
42+
assert_eq!(output, "123");
43+
```
44+
45+
与parser相对应的是combinator,它们的输入参数一般是一个或一组parser组成,他们对自己参数中的parser进行组装并按照自己的规则产生新的parser。上方例子中的`tag`函数就是一种特殊的combinator,它的输入参数是一个字符串,它的输出是一个parser,这个parser只能parse在`tag`参数中指定的字符串。
46+
47+
nom中的combinator非常多,这里只介绍一些常用的combinator。
48+
49+
```rust,ignore
50+
51+
// preceded: 接受两个parser,按照顺序执行,返回第二个parser的结果,第一个parser结果被丢弃
52+
let parser = preceded(tag("123"), tag("456"));
53+
let (remain, output) = parser("1234567").unwrap();
54+
assert_eq!(remain, "7");
55+
assert_eq!(output, "456");
56+
57+
// map_res: 接受一个parser和一个函数,将parser的结果作为函数的输入,返回函数的输出
58+
let parser = map_res(tag("123"), |s: &str| s.parse::<i32>());
59+
let (remain, output) = parser("1234567").unwrap();
60+
assert_eq!(remain, "4567");
61+
assert_eq!(output, 123); // 输出被map_res变为了i32类型,这个combintor经常被用于生成ast node
62+
63+
// alt: 接受多个parser,按照顺序执行,返回第一个成功的parser的结果
64+
let parser = alt((tag("123"), tag("456")));
65+
let (remain, output) = parser("4567123").unwrap();
66+
assert_eq!(remain, "7123");
67+
assert_eq!(output, "456"); // 第一个parser失败,第二个成功,返回第二个parser的结果
68+
69+
```
70+
71+
选择合适的combinator是编写parser的关键,nom提供了一些文档:
72+
- [Nom Recipes](https://github.com/Geal/nom/blob/main/doc/nom_recipes.md)
73+
- [choosing a combinator](https://github.com/Geal/nom/blob/main/doc/choosing_a_combinator.md)
74+
975

1076

1177
## AST
1278

1379
AST是抽象语法树的简称,是编译器的中间表示。AST是由nom parser生成的,它是一个树形结构,每个节点都是一个结构体,包含了节点的类型、子节点、行号、列号等信息。
1480

15-
## LLVM backend
81+
## Builder
82+
83+
我们在某次重构中尝试分离了编译后端和AST相关的逻辑,虽然在别的包目前还有一些和llvm代码的耦合,但是理论上我们已经可以添加别的编译后端,
84+
只要该后端能被封装成实现`builder` trait的类型即可。
85+
86+
目前我们有两个后端,一个是LLVM,一个是NoOp,LLVM是目前唯一有真正编译能力的后端,NoOp是一个空后端,它实际上不干
87+
任何工作,它被用于lsp运行模式,因为lsp模式下不需要真的进行编译,使用该后端能增加lsp的性能。
88+
89+
90+
## 需要注意的点
91+
92+
### 1. 差量分析
1693

17-
本编译器使用llvm来生成目标代码,llvm的jit部分会被包含在我们的编译器可执行文件中,然而静态编译不行。因此静态编译相比jit会多一个llvm的依赖。
94+
所有被salsa的proc macro标记的函数都是支持差量运行的。这些函数要求必须是纯函数,他们的输入输出应该看作是只读的,
95+
即使里面存在例如`Arc<Refcell<xxx>>`一类的字段,你也不该在外界对其进行`borrow_mut`然后修改。
96+
如果改了可能导致非常难查出来的bug。

book/src/compiler/ast.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,22 @@
1212
{{#include ../../../src/ast/node/mod.rs:node}}
1313
```
1414

15+
一般来说,所有的节点都需要加入`NodeEnum`中,并且使用`#[node]`proc macro进行修饰。
16+
同时需要在`fmt.rs`中加入format相关的函数。
17+
18+
如果你想学习如何添加新的节点,可以先从简单的节点开始,比如`BoolConstNode`
19+
20+
```rust,no_run,noplayground
21+
{{#include ../../../src/ast/node/primary.rs:bool}}
22+
```
23+
24+
1525
你可能注意到了,`Node`trait继承了`RangeTrait`,这个trait定义了节点的位置信息。
1626

1727
```rust,no_run,noplayground
1828
{{#include ../../../src/ast/node/mod.rs:range}}
1929
```
20-
一般来说,`RangeTrait`的实现通过`#[range]`宏来自动生成,你不需要手动实现它。
30+
一般来说,`RangeTrait`的实现通过`#[node]`宏来自动生成,你不需要手动实现它。
2131

2232
`Node`接口中的`print`函数用于打印节点的信息,它会被用于调试。`print`打印的结果和`tree`的输出非常像,你需要用一些工具函数来
2333
格式化输出。以`ifnode``print`函数为例:
@@ -34,7 +44,13 @@
3444
{{#include ../../../src/ast/node/control.rs:emit}}
3545
```
3646

37-
emit函数的参数是节点自身,第二个参数是编译上下文。编译上下文中会包含一些需要透传的信息,比如符号表,llvmbuilder,lsp参数等。
47+
emit函数的第一个参数是节点自身,第二个参数是编译上下文。编译上下文中会包含一些需要透传的信息,比如符号表,lsp参数等,
48+
第三个参数是`builder`,用于生成中间代码。目前`builder`只有llvm的实现。
49+
50+
emit函数的返回值比较复杂,它是一个`Result`枚举类型,它的`Ok`类型中包含一个`Option``PLValue`--这是代表该节点的运算结果,
51+
一般只有表达式节点有这个值,statement节点这里会返回`None`,除此之外还包含一个`Option``PLType`,这是代表该节点返回值的类型,
52+
最后一个是一个`TerminatorEnum`,用于分析某个路径是否有终结语句(break,continue,return,panic等)。返回值的`Err`类型是一个`PLDiag`,这个类型是用于报告错误的,包含了所有的错误信息。一般来说,在返回之前错误就会被加到编译上下文中,所以调用者不需要对其进行
53+
任何处理。
3854

3955

4056
## 打印AST结构

book/src/dev-prepare.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# 环境准备
2+
3+
## Rust
4+
5+
可以在项目的根目录下的`rust-toolchain`文件中查看当前项目使用的rust版本,如果你的rust版本不是这个版本,可以使用rustup安装这个版本的rust。
6+
7+
```bash
8+
rustup install $(cat rust-toolchain)
9+
```
10+
11+
国内如果没代理安装rust比较困难,建议使用清华源进行安装,见[此处](https://mirrors.tuna.tsinghua.edu.cn/help/rustup/)
12+
13+
安装完毕后,可以进行cargo换源设置,防止依赖无法下载,见[此处](https://mirrors.tuna.tsinghua.edu.cn/help/crates.io-index.git/)
14+
15+
## LLVM
16+
17+
Pivot-Lang目前使用LLVM 14作为后端,所以需要安装LLVM。如果你使用的是macOS,可以使用brew安装LLVM。
18+
19+
```bash
20+
brew install llvm@14
21+
```
22+
23+
如果你使用的是ubuntu,可以使用[这里](https://github.com/Pivot-Studio/setup-llvm/blob/main/scripts/install_llvm.sh)的脚本进行安装
24+
File renamed without changes.

book/src/systemlib/gc.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,6 @@ immix gc是一种mark region算法,它是一个精确的gc算法。但是请
3333
immix gc的实现代码在[这里](https://github.com/Pivot-Studio/pivot-lang/blob/master/immix)。它是天生支持多线程使用的,但是我们的pivot-lang目前还不支持多线程。
3434

3535

36-
### 目前存在的一些问题
37-
38-
!!!NEED HELP!!!
39-
40-
似乎在windows上性能明显差于linux和mac,但是说实话我并不是这方面的专家,如果有大佬了解这方面欢迎帮我吗优化。
41-
4236
## Benchmark
4337

4438
我们对两种gc算法进行了一些基准测试,事实证明immix gc的回收性能要比simple gc __快近20倍__。如果你对这些测试感兴趣,可以在项目根目录运行`make bench`查看immix的benchmark,或者运行`make bench-simple-gc`查看simple gc的benchmark。
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/ast/node/primary.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ impl Node for PrimaryNode {
3434
res
3535
}
3636
}
37-
37+
// ANCHOR: bool
3838
#[node]
3939
pub struct BoolConstNode {
4040
pub value: bool,
4141
}
42+
// ANCHOR_END: bool
4243

4344
impl PrintTrait for BoolConstNode {
4445
fn print(&self, tabs: usize, end: bool, mut line: Vec<bool>) {

0 commit comments

Comments
 (0)