Skip to content

Commit 182a4f4

Browse files
committed
use C# expression-tree-style to refactory all design docs
1 parent 36375f7 commit 182a4f4

File tree

3 files changed

+234
-76
lines changed

3 files changed

+234
-76
lines changed

roadmap.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
主要工作包括:
88

99
- 设计整体架构,验证各种细节功能的基本可行性。
10+
- 调研各种 meta language 和 metaprogramming 设计,特别是与 Go 类似的静态编译和代码转化型语言 Rust/Nim/TypeScript,以及 C# Expression List 和 OCaml。
1011
- 实现一个基于 `go/ast` 和相关准官方库的 Go AST query/alter/patch 的库。
1112
- 实现各种 scope/declaration/variable tracking 等基本技术。
1213

@@ -16,11 +17,9 @@
1617

1718
- 实现 `github.com/go-kuro/macro`:各种 macro 函数真正使用的 API,里面应该包含各种 AST 相关的上下文信息,由 `kuro` 负责填充其中的真实内容。
1819
- 实现 `github.com/go-kuro/kuro` 基本能力,这里所有的库都与 `kuro` 的核心功能紧密相连,包括 `kuro` 自己应该也在使用这些能力来完成各种功能,原则上,用这些公开的 API 就可以重写实现 `kuro` 命令。
19-
- `github.com/go-kuro/kuro/builder`:各种用于构建 AST 的工具函数和类型。
2020
- `github.com/go-kuro/kuro/parser`:各种 Go 源码解析器,能够将一个 package 解析成 kuro AST。
2121
- `github.com/go-kuro/kuro/ast`:各种 AST 元素定义,与 `go/ast` 在名字上保持一致,但是可以被编辑。
2222
- `github.com/go-kuro/kuro/printer`:将 AST 重新输出成 Go1 可以编译的代码,短期来说可以直接复制 `go/printer` 代码,但长期必须自己重写,从而能够更好的定制,甚至实现基于 AST 的反向代码生成。
23-
- `github.com/go-kuro/kuro/query`:各种 AST 查询语言,类似 CSS selector,支持通过 map/reduce 方式修改,方便未来做更好的并发控制。
2423

2524
考虑到阶段 1 要做的事情实在太多了,这里仅需要完成所有库的接口设计,以及支持实现一个最基本的 macro 用来实现基于 return 的 assert 逻辑。
2625

@@ -48,3 +47,14 @@
4847
## 阶段 4:完成第一个可用版本
4948

5049
清理各种代码,完善 API、文档、测试用例等。
50+
51+
## 规划中但暂无计划的功能
52+
53+
包含一些重要但不是那么必要的功能。
54+
55+
- `github.com/go-kuro/kuro` 的扩展功能:
56+
- `github.com/go-kuro/kuro/builder`:各种用于构建 AST 的工具函数和类型。
57+
- `github.com/go-kuro/kuro/query`:各种 AST 查询语言,类似 CSS selector,支持通过 map/reduce 方式修改,方便未来做更好的并发控制。
58+
- 调试工具:
59+
- 基于 `delve` 或者 `gdb` 的调试器扩展功能,让调试器可以将断点设置在 `kuro` 处理前的源码上。
60+
- 调试 metaprogramming 相关代码。

samples.md

+201-64
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
在这里写了一些理想中的样例代码。由于 API 未定,这里面的 API 调用方式不代表最终设计,会不断修改和调整。
44

5-
## Macro
5+
## Macro 基础
66

77
这里演示如何用 macro 实现一个业务 assert,特点是:
88

@@ -33,14 +33,12 @@ package assert
3333

3434
import (
3535
"fmt"
36-
"go/token"
3736

3837
"github.com/go-kuro/kuro/ast"
39-
"github.com/go-kuro/kuro/builder"
40-
"github.com/go-kuro/kuro/query"
4138
"github.com/go-kuro/macro"
4239
)
4340

41+
// Assert 实现类似 C 的 assert,能够将 cond 真正的源码放到 err 里面去。
4442
func Assert(cond bool) (err error) {
4543
// 如果 cond 满足,则什么也不发生。
4644
if cond {
@@ -51,74 +49,213 @@ func Assert(cond bool) (err error) {
5149
// 一旦使用 macro 能力,会导致每个调用这个 Assert 的代码都被「复制」一份,
5250
// 工作原理就像 C++ template instantiation 一样。
5351
//
54-
// 需要注意,macro.New 中执行的函数其实就是一段普通函数,只是传入了当前 kuro 编译器上下文而已,
55-
// 这个函数内部并不能真正的控制任何的编译器行为,所有函数中的代码都是动态执行的代码。
56-
macro.New(func(ctx macro.Context) {
52+
// 生成了 ast.Stmt 之后,kuro 将这些代码 inline 到当前函数中去。
53+
macro.New(func(ctx macro.Context) ast.Stmt {
5754
// 拿到 cond 参数的 AST 信息,由于这个函数只有一个参数,所以可以直接取数组下标来拿到参数信息。
58-
arg := ctx.Args()[0]
55+
args := ctx.Args()
56+
arg := macro.Var(ctx, args[0]) // 定义一个可以在 macro.Sttm 中使用的「变量」,编译期计算。
5957

60-
// 引用了外面定义的 err,在这里进行了赋值。
61-
// arg 默认的 %v 行为就是打印出源码。
62-
err = fmt.Errorf("assert: failed on %v", arg)
58+
// macro.Parse 不会真的执行,这只是一个编译期的工具函数,用来把输入的任何表达式变成 `ast.Expr`。
59+
//
60+
// 下面这段代码会被 kuro 解析成 AST,并且将其中可以在编译器得到结果的部分替换成常量。
61+
// 比如这里面的 arg,会在编译期得到具体值 "a > 0 && b > 0"。
62+
//
63+
// macro.Parse 会自动解析 func() 的代码并且生成必要的 AST 构建代码,
64+
// 相当于手写了下面的代码:
65+
//
66+
// fmtPkg := builder.Import("fmt")
67+
// return &ast.AssignStmt{
68+
// Lhs: []ast.Expr{ast.NewIdent("err")}, // 这个 err 是引用闭包外的普通变量。
69+
// Rhs: []ast.Expr{
70+
// &ast.CallExpr{
71+
// Fun: fmtPkg.Func("Errorf"),
72+
// Args: []ast.Expr{
73+
// &ast.BasicLit{
74+
// Kind: token.STRING,
75+
// Value: `"assert: failed on %v"`,
76+
// },
77+
// arg, // 注意这里,使用的是闭包外面的变量 arg,编译期会得到具体值。
78+
// },
79+
// },
80+
// },
81+
// }
82+
return macro.Parse(func() {
83+
err = fmt.Errorf("assert: failed on %v", arg)
84+
}).(*ast.FuncLit).Body
6385
})
6486
return
6587
}
88+
```
6689

67-
func init() {
68-
// macro.NewGlobal 可以注册一个全局 AST 处理器。
69-
// 只要使用者引用了当前 package,这个处理器就会被执行,将使用者的 package 代码作为输入
70-
// 这个处理器需要在 package init 或全局变量执行的时候注册,这样 kuro 就可以很容易编译这些处理器,
71-
// 仅需要简单的将当前 package 重新编译成 kuro AST manipulator binary 就可以了。
72-
// 一个 package 可以注册多个 AST 处理器,执行先后顺序按照注册顺序来排列。
73-
macro.NewGlobal(func(ctx macro.Context, pkg ast.Package) {
74-
// 通过 selector 语法选择所有 assert.Assert 的调用。
75-
// 这里选择的是符合指定表达式的最小 stmt。
76-
//
77-
// 得检查 stmt 中调用 assert.Assert 的地方是否处理了返回值,即看看 CallExpr 是不是一个 ExprStmt。
78-
// 如果处理了,则意味着使用者主动拿返回值做事情了,这里就不需要做什么特殊的事情;
79-
// 如果没有,则需要加上错误处理逻辑。
80-
query.Stmts("ExprStmt > CallExpr[[email protected]]", pkg).Map(func(stmt ast.Stmt) (modified ast.Stmt) {
81-
// 这个函数通过返回 modified 来修改 stmt 代码。
82-
83-
fn := query.Parent("FuncDecl", stmt)
84-
85-
// 同时还要看看执行 assert.Assert 的地方是否返回了 error,
86-
// 如果没返回 error,则意味着没法正常 assert,报编译错误。
87-
if !query.Test("Type.Results.List(-1)[Type=error]", fn) {
88-
ctx.Throw(stmt, "assert.Assert is called in a func/method which doesn't return error")
89-
return
90-
}
91-
92-
// 拿到这个 CallExpr,将它作为后续处理的一部分。
93-
assert := query.Exprs("CallExpr").First()
94-
95-
// 拿到函数返回值信息,尽可能的使用返回值名字进行赋值。
96-
err := builder.UseResult(query.Exprs("Type.Results", fn), -1)
97-
98-
// 生成代码。
99-
// if err = assert.Assert(a > 0 && b > 0); err != nil {
100-
// return
101-
// }
102-
modified = &ast.IfStmt{
103-
Init: &ast.AssignStmt{
104-
Lhs: []ast.Expr{err},
105-
Rhs: []ast.Expr{assert},
106-
},
107-
Cond: &ast.BinaryExpr{
108-
X: err,
109-
Op: token.NEQ,
110-
Y: &ast.Ident{
111-
Name: "nil",
112-
},
113-
},
114-
Body: &ast.BlockStmt{
115-
List: []ast.Stmt{
116-
builder.Return(err),
117-
},
118-
},
90+
## 用 Macro 修改函数参数
91+
92+
用 macro 的能力修改函数的入口参数,从而实现一些比较有意思的 DSL。这里演示的是 SQL builder 中的查询条件。
93+
94+
### 使用 SQL DSL 的业务代码
95+
96+
```go
97+
package main
98+
99+
import (
100+
"time"
101+
102+
"url.to/my/framework/orm"
103+
)
104+
105+
type User struct {
106+
Name string
107+
Email string
108+
CreatedAt time.Time
109+
}
110+
111+
func FindUserByID(ctx context.Context, uid UserID) (err error) {
112+
var user User
113+
114+
err = orm.DB(ctx).From("users").Where(
115+
`uid` == orm.Var(uid) && `status` == orm.In(1, 2, 3)
116+
).First(&user)
117+
118+
// 使用这些变量进行业务处理。
119+
120+
return
121+
}
122+
```
123+
124+
### SQL DSL 框架实现
125+
126+
这里仅演示 `Where` 的实现,其他方法不太需要用到 macro。
127+
128+
```go
129+
package orm
130+
131+
import (
132+
"go/token"
133+
"strconv"
134+
"strings"
135+
136+
"github.com/go-kuro/kuro/ast"
137+
"github.com/go-kuro/macro"
138+
)
139+
140+
func (q *Query) Where(cond bool) *Query {
141+
// 虽然这个函数参数只有 cond,但可以通过 macro 能力直接改写函数参数,从而接收更多的输入。
142+
//
143+
// 一些注意事项:
144+
// - 函数的参数和返回值都可以被修改,但是 Recv 不能被修改。
145+
// - 如果函数返回值类型修改后造成调用者无法通过编译,kuro 会报错。
146+
// - 如果这个函数是 Query 实现某种 interface 的一个必要函数,那么 kuro 会直接报错,
147+
// 这是因为 kuro 必然会修改函数的名字,来实现 generic,必然会导致 interface 不再被满足。
148+
149+
// macro.Rewrite 调用之后,整个函数签名和实现都会修改,签名任何的代码都会被忽略。
150+
// 使用者可以用这个特性来实现 static assert 类似的能力,仅作一些编译检查。
151+
macro.Rewrite(func(ctx macro.Context) *ast.FuncLit {
152+
args := ctx.Args()
153+
condExpr := args[0] // 原函数只有一个参数,可以放心的取下标。
154+
whereExpr := parseWhereExpr(condExpr)
155+
156+
// 声明用于 macro.FuncDecl 的变量,这些变量的类型通过 `.(type)` 来指定。
157+
// 如果以后 Go2 普及了,可以使用 Go2 的 trait 语法,即类似于 `macro.Var(*Query)(ctx.Recv())`。
158+
q := macro.Var(ctx, ctx.Recv()).(*Query)
159+
where := macro.Const(ctx, whereExpr).(string) // whereExpr 必须是一个可以赋值给 const 的表达式,否则 kuro 会报错。
160+
161+
return macro.Parse(func(values ...interface{}) *Query {
162+
q.where = append(q.where, &whereExpr{
163+
Where: where,
164+
Values: values,
119165
})
120-
return
166+
return q
121167
})
122168
})
123169
}
170+
171+
// parseWhereExpr 将参数解析成一段 SQL 字符串常量。
172+
//
173+
// 要实现这个 Where 的关键思路是:解析 condExpr 的逻辑表达式,并且将它转成 SQL 逻辑表达式。
174+
// 由于完整实现这个能力会需要花费非常多的代码,这里仅作一些必要的 demo,实现下面简单形式的代码。
175+
//
176+
// `col1` == value1 && `col2` == value2
177+
//
178+
// 因此,暂时忽略 UnaryExpr 以及括号之类的。
179+
func parseWhereExpr(expr ast.Expr) (result ast.Expr) {
180+
binary := expr.(*ast.BinaryExpr)
181+
182+
if isLogic(binary) {
183+
result = parseLogic(expr)
184+
return
185+
}
186+
187+
var op string
188+
189+
switch binary.Op {
190+
case token.LAND: // &&
191+
op = " AND "
192+
case token.LOR: // ||
193+
op = " OR "
194+
default:
195+
panic("orm: unsupported logic op")
196+
}
197+
198+
return &ast.BinaryExpr{
199+
X: &ast.BinaryExpr{
200+
X: parseWhereExpr(binary.X),
201+
Op: token.ADD,
202+
Y: &ast.BasicLit{
203+
Kind: token.STRING,
204+
Value: strconv.Quote(op),
205+
},
206+
},
207+
Op: token.ADD,
208+
Y: parseWhereExpr(binary.Y),
209+
},
210+
}
211+
212+
// isLogic 判断这个 binary 是否是一个查询逻辑了。
213+
// 如果 X 或者 Y 是 BasicLit,则说明是形如 `col1` == value1 的形式,
214+
// 可以直接开始分析结构并生成 SQL 了。
215+
func isLogic(expr *ast.BinaryExpr) bool {
216+
_, ok1 := expr.X.(*ast.BasicLit)
217+
_, ok2 := expr.Y.(*ast.BasicLit)
218+
return ok1 || ok2
219+
}
220+
221+
// parseLogic 解析逻辑表达式,并且返回 SQL 表达式。
222+
// 为了避免示例写太长,这里假定 X 一定是 `col` 形式的常量,Y 一定是 `orm.Var(xxx)` 的函数调用。
223+
func parseLogic(logic *ast.BinaryExpr) (expr ast.Expr, args []ast.Expr) {
224+
var op, value string
225+
226+
switch binary.Op {
227+
case token.EQL: // &&
228+
op = " = "
229+
case token.NEQ: // ||
230+
op = " <> "
231+
default:
232+
panic("orm: unsupported logic op")
233+
}
234+
235+
col := logic.X.(*ast.BasicLit).Value
236+
call := logic.Y.(*ast.CallExpr)
237+
238+
switch call.Fun.(*ast.SelectorExpr).Sel.Name {
239+
case "Var":
240+
value = "?"
241+
case "In":
242+
switch binary.Op {
243+
case token.EQL:
244+
op = " IN "
245+
case token.NEQ:
246+
op = " NOT IN "
247+
}
248+
249+
value = strings.Repeat("?, ", len(call.Args))
250+
value = value[:len(value)-2] // 去掉多余的逗号。
251+
value = "(" + value + ")"
252+
}
253+
254+
expr = &ast.BasicLit{
255+
Kind: token.STRING,
256+
Value: strconv.Quote(col+op+value),
257+
}
258+
args = call.Args
259+
return
260+
}
124261
```

thoughts.md

+21-10
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@
1010

1111
## 使用场景
1212

13-
### 减少重复代码
13+
提供一套类似于 [C# Expression Tree](https://docs.microsoft.com/en-us/dotnet/csharp/expression-trees) 的 API,从而实现 metaprogramming 能力。
1414

15-
Go 里面经常会有很多很重复的代码
15+
考虑到所有接口设计需要兼容当前的 Go compiler,至少在编辑代码阶段,要让 kuro 处理前的代码也能顺利经过 `go build` 编译,从而对 IDE 友好,所以这里很难设计出 C# 一样的机制,通过 Expression Tree 扩展出一种新的 DSL 语言。一个退而求其次的方法是设计出类似 [Rust Macros](https://doc.rust-lang.org/1.43.0/book/ch19-06-macros.html)[Nim Macros](https://nim-lang.org/docs/manual.html#macros) 的卫生宏,仅仅做到函数级的代码生成,在不改变函数返回值的前提下做一些超越 generic 能力的工作
1616

17-
- 判断错误:比如随处可见的 `if err != nil {...}`
18-
- 各种静态和动态 assertion:实现一些编译期 assert,并且使用 `return` 来实现动态 assert,避免使用 panic,并允许框架设计者在返回前插入逻辑和日志。
19-
- 常见的优化与最佳实践:特别是必须用泛型才能实现的优化,方便框架开发者降低使用者的思考负担,提升最终业务代码的质量,比如封装 `chan` 的最佳实践、自动使用 `strings.Builder` 来拼接字符串、减少使用不必要的 `interface{}` 等。
20-
- 常见的 `go generate` 场景:有一些经常会需要但很费事的场景能力,比如实现枚举变量并给每个变量来个名字。
17+
几个典型例子(来源于 C#、Rust 和 Nim):
2118

22-
这些能力都是为 Go 框架开发者实现的,希望在 `kuro` 开发阶段也能用于 `kuro` 代码本身——自己编译自己,这才是 metaprogramming 的本意。
19+
- 实现更强大的 debug 机制,类似 C macro 的变量打印,但是可以更强大([Nim Debug Examples](https://nim-lang.org/docs/manual.html#macros-debug-example))。
20+
- 实现静态数据构建,避免使用 `interface{}`,比如更好的 `Printf`,静态推导参数和拼接字符串。
21+
- 实现更好的 DSL,在符合 Go 语法的前提下设计出更易读的 DSL,比如实现某种程度的符号重载。
2322

24-
### 实现 macro 能力
23+
### 提升代码生成器的使用体验
2524

26-
提供一套类似于 Rust 的 macro API,从而实现 metaprogramming 能力
25+
`go generate` 提供基本的代码生成能力,但是需要使用者手动执行 `go generate` 命令,且不能将代码生成工具(`go generate` 所调用的命令)纳入到 `go.mod` 的依赖管理体系中去
2726

28-
参考 [Rust Macros](https://doc.rust-lang.org/1.43.0/book/ch19-06-macros.html)[Nim Macros](https://nim-lang.org/docs/manual.html#macros)
27+
`kuro` 可以完全解决这个问题,并且通过双向的管理 VCS 上的源码来形成一种新的开发模式,让开发者可以几乎完全忘记代码生成这回事,减少维护成本
2928

3029
## 功能规划
3130

@@ -133,3 +132,15 @@ $ git push # Push to upstream which is controlled by kuro
133132
### STL-like packages
134133

135134
由于 Go2 的 trait 一旦启用,这些 STL-like 库已经可以得到比较好的开发和使用体验,所以看起来已经不太需要重新用 macro 或者其他机制实现了。
135+
136+
### 全局 macro
137+
138+
之前考虑过通过 `macro.NewGlobal` 来注册全局代码生成器,从而能够将整个 package 作为输入来进行代码修改。现在并没有什么语言支持这种全局 macro,所以现在我们实现这种能力可能不是个好想法。
139+
140+
这个能力可能未来还会以某种形式实现,但是暂时带来的问题多于解决的问题,主要体现在:
141+
142+
- 全局 macro 的执行顺序如何确定?是否需要多次执行?
143+
- 如果有多个库注册了全局 macro,`kuro` 很难决策他们的执行顺序。
144+
- 如果一个全局 macro A 生成的代码里存在另一个全局 macro B 需要处理的代码,那么是否应该在执行 A 之后再度执行 B?如果是,假如 B 又生成了 A 需要处理的代码,是否要继续迭代?这个问题很难回答和完美解决。
145+
- 全局 macro 如何保证生成的源码真正可编译?
146+
- 一旦生成的代码不可编译,或者因为 macro 自己生成了错误的代码导致运行时错误,这会极大的增加调试难度,甚至无法调试。

0 commit comments

Comments
 (0)