2
2
3
3
在这里写了一些理想中的样例代码。由于 API 未定,这里面的 API 调用方式不代表最终设计,会不断修改和调整。
4
4
5
- ## Macro
5
+ ## Macro 基础
6
6
7
7
这里演示如何用 macro 实现一个业务 assert,特点是:
8
8
@@ -33,14 +33,12 @@ package assert
33
33
34
34
import (
35
35
" fmt"
36
- " go/token"
37
36
38
37
" github.com/go-kuro/kuro/ast"
39
- " github.com/go-kuro/kuro/builder"
40
- " github.com/go-kuro/kuro/query"
41
38
" github.com/go-kuro/macro"
42
39
)
43
40
41
+ // Assert 实现类似 C 的 assert,能够将 cond 真正的源码放到 err 里面去。
44
42
func Assert (cond bool ) (err error ) {
45
43
// 如果 cond 满足,则什么也不发生。
46
44
if cond {
@@ -51,74 +49,213 @@ func Assert(cond bool) (err error) {
51
49
// 一旦使用 macro 能力,会导致每个调用这个 Assert 的代码都被「复制」一份,
52
50
// 工作原理就像 C++ template instantiation 一样。
53
51
//
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 {
57
54
// 拿到 cond 参数的 AST 信息,由于这个函数只有一个参数,所以可以直接取数组下标来拿到参数信息。
58
- arg := ctx.Args ()[0 ]
55
+ args := ctx.Args ()
56
+ arg := macro.Var (ctx, args[0 ]) // 定义一个可以在 macro.Sttm 中使用的「变量」,编译期计算。
59
57
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
63
85
})
64
86
return
65
87
}
88
+ ```
66
89
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,
119
165
})
120
- return
166
+ return q
121
167
})
122
168
})
123
169
}
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
+ }
124
261
```
0 commit comments