宏的实现
本章节介绍仓颉宏的定义和使用,仓颉宏可以分为非属性宏和属性宏。同时本章节还会介绍宏出现嵌套时的行为。
非属性宏
非属性宏只接受被转换的代码,不接受其他参数(属性),其定义格式如下:
import std.ast.*
public macro MacroName(args: Tokens): Tokens {
... // Macro body
}
宏的调用格式如下:
@MacroName(...)
宏调用使用 ()
括起来。括号里面可以是任意合法 tokens
,也可以是空。
当宏作用于声明时,一般可以省略括号。参考下面例子
@MacroName func name() {} // Before a FuncDecl
@MacroName struct name {} // Before a StructDecl
@MacroName class name {} // Before a ClassDecl
@MacroName var a = 1 // Before a VarDecl
@MacroName enum e {} // Before a Enum
@MacroName interface i {} // Before a InterfaceDecl
@MacroName extend e <: i {} // Before a ExtendDecl
@MacroName mut prop i: Int64 {} // Before a PropDecl
@MacroName @AnotherMacro(input) // Before a macro call
宏展开过程作用于仓颉语法树,宏展开后,编译器会继续进行后续的编译过程,因此,用户需要保证宏展开后的代码依然是合法的仓颉代码,否则可能引发编译问题。当宏用于声明时,如果省略括号,宏的输入必须是语法合法的声明,IDE 也会提供相应的语法检查和高亮。
下面是几个宏应用的典型示例。
-
示例 1
宏定义文件
macro_definition.cj
macro package macro_definition import std.ast.* public macro TestDef(input: Tokens): Tokens { println("I'm in macro body") return input }
宏调用文件
macro_call.cj
package macro_calling import macro_definition.* main(): Int64 { println("I'm in function body") let a: Int64 = @TestDef(1 + 2) println("a = ${a}") return 0 }
上述代码的编译过程可以参考宏的编译和使用。
我们在用例中添加了打印信息,其中宏定义中的
I'm in macro body
将在编译macro_call.cj
的期间输出,即对宏定义求值。同时,宏调用点被展开,如编译如下代码:let a: Int64 = @TestDef(1 + 2)
编译器将宏返回的
Tokens
更新到调用点的语法树上,得到如下代码:let a: Int64 = 1 + 2
也就是说,可执行程序中的代码实际变为了:
main(): Int64 { println("I'm in function body") let a: Int64 = 1 + 2 println("a = ${a}") return 0 }
a
经过计算得到的值为 3,在打印a
的值时插值为 3。至此,上述程序的运行结果为:I'm in function body a = 3
下面看一个更有意义的用宏处理函数的例子,这个宏 ModifyFunc 宏的作用是给 MyFunc 增加 Composer 参数,并在counter++
前后插入一段代码。
-
示例 2
宏定义文件
macro_definition.cj
// file macro_definition.cj macro package macro_definition import std.ast.* public macro ModifyFunc(input: Tokens): Tokens { println("I'm in macro body") let funcDecl = FuncDecl(input) return quote( func $(funcDecl.identifier)(id: Int64) { println("start ${id}") $(funcDecl.block.nodes) println("end") }) }
宏调用文件
macro_call.cj
package macro_calling import macro_definition.* var counter = 0 @ModifyFunc func MyFunc() { counter++ } func exModifyFunc() { println("I'm in function body") MyFunc(123) println("MyFunc called: ${counter} times") return 0 }
同样的,上述两段代码分别位于不同文件中,先编译宏定义文件
macro_definition.cj
,再编译宏调用macro_call.cj
生成可执行文件。这个例子中,ModifyFunc 宏的输入是一个函数声明,因此可以省略括号:
@ModifyFunc func MyFunc() { counter++ }
经过宏展开后,得到如下代码:
func MyFunc(id: Int64) { println("start ${id}") counter++ println("end") }
MyFunc 会在 main 中调用,它接受的实参也是在 main 中定义的,从而形成了一段合法的仓颉程序。运行时打印如下:
I'm in function body start 123 end MyFunc called: 1 times
属性宏
和非属性宏相比,属性宏的定义会增加一个 Tokens 类型的输入,这个增加的入参可以让开发者输入额外的信息。比如开发者可能希望在不同的调用场景下使用不同的宏展开策略,则可以通过这个属性入参进行标记位设置。同时,这个属性入参也可以传入任意 Tokens,这些 Tokens 可以与被宏修饰的代码进行组合拼接等。下面是一个简单的例子:
// Macro definition with attribute
public macro Foo(attrTokens: Tokens, inputTokens: Tokens): Tokens {
return attrTokens + inputTokens // Concatenate attrTokens and inputTokens.
}
如上面的宏定义,属性宏的入参数量为 2,入参类型为 Tokens
,在宏定义内,可以对 attrTokens
和 inputTokens
进行一系列的组合、拼接等变换操作,最后返回新的 Tokens
。
带属性的宏与不带属性的宏的调用类似,属性宏调用时新增的入参 attrTokens 通过 [] 传入,其调用形式为:
// attribute macro with parentheses
var a: Int64 = @Foo[1+](2+3)
// attribute macro without parentheses
@Foo[public]
struct Data {
var count: Int64 = 100
}
-
宏 Foo 调用,当参数是
2+3
时,与[]
内的属性1+
进行拼接,经过宏展开后,得到var a: Int64 = 1+2+3
。 -
宏 Foo 调用,当参数是 struct Data 时,与
[]
内的属性public
进行拼接,经过宏展开后,得到public struct Data { var count: Int64 = 100 }
关于属性宏,需要注意以下几点:
-
带属性的宏,与不带属性的宏相比,能修饰的 AST 是相同的,可以理解为带属性的宏对可传入参数做了增强。
-
要求属性宏调用时,
[]
内中括号匹配,且可以为空。中括号内只允许对中括号的转义\[
或\]
,该转义中括号不计入匹配规则,其他字符会被作为 Token,不能进行转义。@Foo[[miss one](2+3) // Illegal @Foo[[matched]](2+3) // Legal @Foo[](2+3) // Legal, empty in [] @Foo[\[](2+3) // Legal, use escape for [ @Foo[\(](2+3) // Illegal, only [ and ] allowed in []
-
宏的定义和调用的类型要保持一致:如果宏定义有两个入参,即为属性宏定义,调用时必须加上
[]
,且内容可以为空;如果宏定义有一个入参,即为非属性宏定义,调用时不能使用[]
。
嵌套宏
仓颉语言不支持宏定义的嵌套;有条件地支持在宏定义和宏调用中嵌套宏调用。
宏定义中嵌套宏调用
下面是一个宏定义中包含其他宏调用的例子。
宏包 pkg1
中定义 GetIdent
宏:
macro package pkg1
import std.ast.*
public macro GetIdent(attr:Tokens, input:Tokens):Tokens {
return quote(
let decl = (parseDecl(input) as VarDecl).getOrThrow()
let name = decl.identifier.value
let size = name.size - 1
let $(attr) = Token(TokenKind.IDENTIFIER, name[0..size])
)
}
宏包 pkg2
中定义 Prop
宏,其中嵌套了 GetIdent
宏的调用:
macro package pkg2
import std.ast.*
import pkg1.*
public macro Prop(input:Tokens):Tokens {
let v = parseDecl(input)
@GetIdent[ident](input)
return quote(
$(input)
public prop $(ident): $(decl.declType) {
get() {
this.$(v.identifier)
}
}
)
}
宏调用包 pkg3
中调用 Prop
宏:
package pkg3
import pkg2.*
class A {
@Prop
private let a_: Int64 = 1
}
main() {
let b = A()
println("${b.a}")
}
注意,按照宏定义必须比宏调用点先编译的约束,上述 3 个文件的编译顺序必须是:pkg1 -> pkg2 -> pkg3。pkg2 中的 Prop
宏定义:
public macro Prop(input:Tokens):Tokens {
let v = parseDecl(input)
@GetIdent[ident](input)
return quote(
$(input)
public prop $(ident): $(decl.declType) {
get() {
this.$(v.identifier)
}
}
)
}
会先被展开成如下代码,再进行编译。
public macro Prop(input: Tokens): Tokens {
let v = parseDecl(input)
let decl = (parseDecl(input) as VarDecl).getOrThrow()
let name = decl.identifier.value
let size = name.size - 1
let ident = Token(TokenKind.IDENTIFIER, name[0 .. size])
return quote(
$(input)
public prop $(ident): $(decl.declType) {
get() {
this.$(v.identifier)
}
}
)
}
宏调用中嵌套宏调用
嵌套宏的常见场景,是宏修饰的代码块中,出现了宏调用。一个具体的例子如下:
pkg1
包中定义 Foo
和 Bar
宏:
macro package pkg1
import std.ast.*
public macro Foo(input: Tokens): Tokens {
return input
}
public macro Bar(input: Tokens): Tokens {
return input
}
pkg2
包中定义 AddToMul
宏:
macro package pkg2
import std.ast.*
public macro AddToMul(inputTokens: Tokens): Tokens {
var expr: BinaryExpr = match (parseExpr(inputTokens) as BinaryExpr) {
case Some(v) => v
case None => throw Exception()
}
var op0: Expr = expr.leftExpr
var op1: Expr = expr.rightExpr
return quote(($(op0)) * ($(op1)))
}
pkg3
包中使用上面定义的三个宏:
package pkg3
import pkg1.*
import pkg2.*
@Foo
struct Data {
let a = 2
let b = @AddToMul(2+3)
@Bar
public func getA() {
return a
}
public func getB() {
return b
}
}
main(): Int64 {
let data = Data()
var a = data.getA() // a = 2
var b = data.getB() // b = 6
println("a: ${a}, b: ${b}")
return 0
}
如上代码所示,宏 Foo
修饰了 struct Data
,而在 struct Data
内,出现了宏调用 AddToMul
和 Bar
。这种嵌套场景下,代码变换的规则是:将嵌套内层的宏 (AddToMul
和 Bar
) 展开后,再去展开外层的宏 (Foo
)。允许出现多层宏嵌套,代码变换的规则总是由内向外去依次展开宏。
嵌套宏可以出现在带括号和不带括号的宏调用中,二者可以组合,但用户需要保证没有歧义,且明确宏的展开顺序:
var a = @Foo(@Foo1(2 * 3)+@Foo2(1 + 3)) // Foo1, Foo2 have to be defined.
@Foo1 // Foo2 expands first, then Foo1 expands.
@Foo2[attr: struct] // Attribute macro can be used in nested macro.
struct Data{
@Foo3 @Foo4[123] var a = @Bar1(@Bar2(2 + 3) + 3) // Bar2, Bar1, Foo4, Foo3 expands in order.
public func getA() {
return @Foo(a + 2)
}
}
嵌套宏之间的消息传递
这里指的是宏调用的嵌套。
内层宏可以调用库函数 assertParentContext
来保证内层宏调用一定嵌套在特定的外层宏调用中。如果内层宏调用这个函数时没有嵌套在给定的外层宏调用中,该函数将抛出一个错误。库函数 InsideParentContext
同样用于检查内层宏调用是否嵌套在特定的外层宏调用中,该函数返回一个布尔值。下面是一个简单的例子。
宏定义如下:
public macro Outer(input: Tokens): Tokens {
return input
}
public macro Inner(input: Tokens): Tokens {
assertParentContext("Outer")
return input
}
宏调用如下:
@Outer var a = 0
@Inner var b = 0 // Error, The macro call 'Inner' should with the surround code contains a call 'Outer'.
如上代码所示,Inner
宏在定义时使用了 assertParentContext
函数用于检查其在调用阶段是否位于 Outer
宏中,在代码示例的宏调用场景下,由于 Outer
和 Inner
在调用时不存在这样的嵌套关系,因此编译器将报告一个错误。
内层宏也可以通过发送键/值对的方式与外层宏通信。当内层宏执行时,通过调用标准库函数 setItem
向外层宏发送信息;随后,当外层宏执行时,调用标准库函数 getChildMessages
接收每一个内层宏发送的信息(一组键/值对映射)。下面是一个简单的例子。
宏定义如下:
macro package define
import std.ast.*
public macro Outer(input: Tokens): Tokens {
let messages = getChildMessages("Inner")
let getTotalFunc = quote(public func getCnt() {
)
for (m in messages) {
let identName = m.getString("identifierName")
// let value = m.getString("key") // 接收多组消息
getTotalFunc.append(Token(TokenKind.IDENTIFIER, identName))
getTotalFunc.append(quote(+))
}
getTotalFunc.append(quote(0))
getTotalFunc.append(quote(}))
let funcDecl = parseDecl(getTotalFunc)
let decl = (parseDecl(input) as ClassDecl).getOrThrow()
decl.body.decls.append(funcDecl)
return decl.toTokens()
}
public macro Inner(input: Tokens): Tokens {
assertParentContext("Outer")
let decl = parseDecl(input)
setItem("identifierName", decl.identifier.value)
// setItem("key", "value") // 可以通过不同的key值传递多组消息
return input
}
宏调用如下:
import define.*
@Outer
class Demo {
@Inner var state = 1
@Inner var cnt = 42
}
main(): Int64 {
let d = Demo()
println("${d.getCnt()}")
return 0
}
在上面的代码中,Outer
接收两个 Inner
宏发送来的变量名,自动为类添加如下内容:
public func getCnt() {
state + cnt + 0
}
具体流程为:内层宏 Inner
通过 setItem
向外层宏发送信息;Outer
宏通过 getChildMessages
函数接收到 Inner
发送的一组信息对象(Outer
中可以调用多次 Inner
);最后通过该信息对象的 getString
函数接收对应的值。