哪个网站可以发宝贝链接做宣传,什么公司做网站最好,小程序直播功能,代账行业门户网站开发使用新版本 #xff08;2024-07-19 16:10发布的#xff09; 1、宏的简介
宏可以理解为一种特殊的函数。一般的函数在输入的值上进行计算#xff0c;然后输出一个新的值#xff0c;而宏的输入和输出都是程序本身。在输入一段程序#xff08;或程序片段#xff0c;例如表达… 使用新版本 2024-07-19 16:10发布的 1、宏的简介
宏可以理解为一种特殊的函数。一般的函数在输入的值上进行计算然后输出一个新的值而宏的输入和输出都是程序本身。在输入一段程序或程序片段例如表达式输出一段新的程序这段输出的程序随后用于编译和执行。为了把宏的调用和函数调用区分开来我们在调用宏时使用 加上宏的名称。
让我们从一个简单的例子开始假设我们想在调试过程中打印某个表达式的值同时打印出表达式本身。
let x 3
let y 2
dprint(x) // 打印 x 3
dprint(x y) // 打印 x y 5显然dprint 不能被写为常规的函数由于函数只能获得输入的值不能获得输入的程序片段。但是我们可以将 dprint 实现为一个宏。一个基本的实现如下
macro package defineimport std.ast.*public macro dprint(input: Tokens): Tokens {let inputStr input.toString()let result quote(print($(inputStr) )println($(input)))return result
}在解释每行代码之前我们先测试这个宏可以达到预期的效果。首先在当前目录下创建一个 macros 文件夹并在 macros 文件夹中创建 dprint.cj 文件将以上内容复制到 dprint.cj 文件中。另外在当前目录下创建 main.cj包含以下测试代码
import define.*main() {let x 3let y 2dprint(x)dprint(x y)
}请注意得到的目录结构如下
// Directory layout.
src
|-- macros
| -- dprint.cj
-- main.cj在当前目录src下运行编译命令
cjc macros/*.cj --compile-macro
cjc main.cj -o main然后运行 ./main可以看到如下输出
x 3
x y 5让我们依次查看代码的每个部分 第 1 行macro package define 宏必须声明在独立的包中不能和其他 public 函数一起含有宏的包使用 macro package 来声明。这里我们声明了一个名为 define 的宏包。 第 2 行import std.ast.* 实现宏需要的数据类型例如 Tokens 和后面会讲到的语法节点类型位于仓颉标准库的 ast 包中因此任何宏的实现都需要首先引入 ast 包。 第 3 行public macro dprint(input: Tokens): Tokens 在这里我们声明一个名为 dprint 的宏。由于这个宏是一个非属性宏之后我们会解释这个概念它接受一个类型为 Tokens 的参数。该输入代表传给宏的程序片段。宏的返回值也是一个程序片段。 第 4 行let inputStr input.toString() 在宏的实现中首先将输入的程序片段转化为字符串。在前面的测试案例中inputStr 成为 “x” 或 “x y” 第 5-7 行let result quote(...) 这里 quote 表达式是用于构造 Tokens 的一种表达式它将括号内的程序片段转换为 Tokens。在 quote 的输入中可以使用插值 $(...) 来将括号内的表达式转换为 Tokens然后插入到 quote 构建的 Tokens 中。对于以上代码$(inputStr) 插入 inputStr 字符串的值包含字符串两端的引号$(input) 插入 input即输入的程序片段。因此如果输入的表达式是 x y那么形成的Tokens为
print(x y )
println(x y)第 8 行return result 最后我们将构造出来的代码返回这两行代码将被编译运行时将输出 x y 5。
回顾 dprint 宏的定义我们看到 dprint 使用 Tokens 作为入参并使用 quote 和插值构造了另一个 Tokens 作为返回值。为了使用仓颉宏我们需要详细了解 Tokens、quote 和插值的概念下面我们将分别介绍它们。
2、Tokens 相关类型和 quote 表达式
2.1 Token 类型
宏操作的基本类型是 Tokens代表一个程序片段。Tokens 由若干个 Token 组成每个 Token 可以理解为用户可操作的词法单元。一个 Token 可能是一个标识符例如变量名等、字面量例如整数、浮点数、字符串、关键字或运算符。每个 Token 包含它的类型、内容和位置信息。
Token 的类型取值为 enum TokenKind 中的元素。TokenKind 的可用值详见《仓颉编程语言库 API》文档。通过提供 TokenKind 和对于标识符和字面量Token 的字符串可以直接构造任何 Token。具体的构造函数如下
public struct Token {public let kind: TokenKindpublic let pos: Positionpublic let value: Stringpublic var delimiterNum: UInt16 1public init()public init(kind: TokenKind)public init(kind: TokenKind, value: String)
}下面给出一些Token构造的例子
import std.ast.*let tk1 Token(TokenKind.ADD) // 运算符
let tk2 Token(TokenKind.FUNC) // func关键字
let tk3 Token(TokenKind.IDENTIFIER, x) // x标识符
let tk4 Token(TokenKind.INTEGER_LITERAL, 3) // 整数字面量
let tk5 Token(TokenKind.STRING_LITERAL, xyz) // 字符串字面量2.2 Tokens 类型
一个 Tokens 代表由多个 Token 组成的序列。我们可以通过 Token 数组直接构造 Tokens。下面是 3 种基本的构造 Tokens 实例的方式
Tokens() // 构造空列表
Tokens(tks: ArrayToken)
Tokens(tks: ArrayListToken)此外Tokens 类型支持以下函数
size返回 Tokens 中包含 Token 的数量get(index: Int64)获取指定下标的 Token 元素[]获取指定下标的 Token 元素拼接两个 Tokens或者直接拼接 Tokens 和 Tokendump()打印包含的所有 Token供调试使用toString()打印 Tokens 对应的程序片段
在下面的案例中我们使用构造函数直接构造 Token 和 Tokens然后打印详细的调试信息
import std.ast.*let tks Tokens(ArrayToken([Token(TokenKind.INTEGER_LITERAL, 1),Token(TokenKind.ADD),Token(TokenKind.INTEGER_LITERAL, 2)
]))
main() {println(tks)tks.dump()
}预期输出如下具体的位置信息可能不同 在 dump 信息中包含了每个 Token 的类型description和值token_literal_value最后打印每个 Token 的位置信息。
2.3 quote 表达式和插值
在大多数情况下直接构造和拼接 Tokens 会比较繁琐。因此仓颉语言提供了 quote 表达式来从代码模版来构造 Tokens。之所以说是代码模版因为在 quote 中可以使用 $(...) 来插入上下文中的表达式。插入的表达式的类型需要支持被转换为 Tokens具体来说实现了 ToTokens 接口。在标准库中以下类型实现了 ToTokens 接口
所有的节点类型节点将在语法节点中讨论Token 和 Tokens 类型所有基础数据类型整数、浮点数、Bool、Rune和StringArrayT 和 ArrayListT这里对 T 的类型有限制并根据 T 的类型不同输出不同的分隔符详细请见《仓颉编程语言库 API》文档。
下面的例子展示 Array 和基础数据类型的插值。
import std.ast.*let intList ArrayInt64([1, 2, 3, 4, 5])
let float: Float64 1.0
let str: String Hello
let tokens quote(arr $(intList)x $(float)s $(str)
)main() {println(tokens)
}更多插值的用法可以参考 使用 quote 插值语法节点。
3、语法节点
在仓颉语言的编译过程中首先通过词法分析将代码转换成 Tokens然后对 Tokens 进行语法解析得到一个语法树。每个语法树的节点可能是一个表达式、声明、类型、模式等。仓颉 ast 库提供了每种节点对应的类它们之间具有适当的继承关系。其中主要的抽象类如下
Node所有语法节点的父类TypeNode所有类型节点的父类Expr所有表达式节点的父类Decl所有声明节点的父类Pattern所有模式节点的父类
具体节点的类型众多具体细节请参考 《仓颉编程语言库 API》文档。在下面的案例中我们主要使用以下节点
BinaryExpr二元运算表达式FuncDecl函数的声明
3.1 节点的解析
通过 ast 库基本上每种节点都可以从 Tokens 解析。有两种调用解析的方法。
3.1.1 使用解析表达式和声明的函数。
parseExpr(input: Tokens): Expr将输入的 Tokens 解析为表达式parseExprFragment(input: Tokens, startFrom!: Int64 0): (Expr, Int64)将输入 Tokens 的一个片段解析为表达式片段从 startFrom 索引开始解析可能只消耗从索引 startFrom 开始的片段的一部分并返回第一个未被消耗的 Token 的索引如果消耗了整个片段返回值为 input.sizeparseDecl(input: Tokens, astKind!: String )将输入的 Tokens 解析为声明astKind 为额外的设置具体请见《仓颉编程语言库 API》文档。parseDeclFragment(input: Tokens, startFrom!: Int64 0): (Decl, Int64)将输入 Tokens 的一个片段解析为声明startFrom 参数和返回索引的含义和 parseExpr 相同。
我们通过代码案例展示这些函数的使用
let tks1 quote(a b)
let tks2 quote(a b, x y)
let tks3 quote(func f1(x: Int64) { return x 1 }
)
let tks4 quote(func f1(x: Int64) { return x 1 }func f2(x: Int64) { return x 2 }
)let binExpr1 parseExpr(tks1)
println(binExpr1 is BinaryExpr: ${binExpr1 is BinaryExpr})
let (binExpr2, mid) parseExprFragment(tks2)
let (binExpr3, end) parseExprFragment(tks2, startFrom: mid 1) // 跳过逗号
println(size ${tks2.size}, mid ${mid}, end ${end})
let funcDecl1 parseDecl(tks3)
println(funcDecl1 is FuncDecl: ${funcDecl1 is FuncDecl})
let (funcDecl2, mid2) parseDeclFragment(tks4)
let (funcDecl3, end2) parseDeclFragment(tks4, startFrom: mid2)
println(size ${tks4.size}, mid ${mid2}, end ${end2})输出结果是
binExpr1 is BinaryExpr: true
size 7, mid 3, end 7
funcDecl1 is FuncDecl: true
size 29, mid 15, end 293.1.2 使用构造函数进行解析
大多数节点类型都支持 init(input: Tokens) 构造函数将输入的 Tokens 解析为相应类型的节点例如
import std.ast.*let binExpr BinaryExpr(quote(a b))
let funcDecl FuncDecl(quote(func f1(x: Int64) { return x 1 }))如果解析失败将抛出异常。这种解析方式适用于类型已知的代码片段解析后不需要再手动转换成具体的子类型。
3.2 节点的组成部分
从 Tokens 解析出节点之后我们可以查看节点的组成部分。作为例子我们列出 BinaryExpr 和 FuncDecl 的组成部分关于其他节点的更详细的解释请见《仓颉编程语言库 API》文档。
BinaryExpr 节点 leftExpr: Expr运算符左侧的表达式op: Token运算符rightExpr: Expr运算符右侧的表达式 FuncDecl 节点部分 identifier: Token函数名funcParams: ArrayListFuncParam参数列表declType: TypeNode返回值类型block: Block函数体 FuncParam节点部分 identifier: Token参数名paramType: TypeNode参数类型 Block节点部分nodes: ArrayListNode块中的表达式和声明
每个组成部分都是 public mut prop因此可以被查看和更新。我们通过一些例子展示更新的结果。
3.2.1 BinaryExpr 案例
let binExpr BinaryExpr(quote(x * y))
binExpr.leftExpr BinaryExpr(quote(a b))
println(binExpr.toTokens())binExpr.op Token(TokenKind.ADD)
println(binExpr.toTokens())首先通过解析获得 binExpr 为节点 x * y图示如下 */ \x y第二步我们将左侧的节点即 x替换为 a b因此获得的语法树如下 */ \ y/ \a b当输出这个语法树的时候我们必须在 a b 周围添加括号得到 (a b) * y如果输出a b * y含义为先做乘法再做加法与语法树的含义不同。ast 库具备在输出语法树时自动添加括号的功能。
第三步我们将语法树根部的运算符从 * 替换为 因此得到语法树如下 / \ y/ \a b这个语法树可以输出为 a b y因为加法本身就是左结合的不需要在左侧添加括号。
3.2.1 FuncDecl 案例
let funcDecl FuncDecl(quote(func f1(x: Int64) { x 1 }))
funcDecl.identifier Token(TokenKind.IDENTIFIER, foo)
println(Number of parameters: ${funcDecl.funcParams.size})
funcDecl.funcParams[0].identifier Token(TokenKind.IDENTIFIER, a)
println(Number of nodes in body: ${funcDecl.block.nodes.size})
let binExpr (funcDecl.block.nodes[0] as BinaryExpr).getOrThrow()
binExpr.leftExpr parseExpr(quote(a))
println(funcDecl.toTokens())在这个案例中我们首先通过解析构造出了一个 FuncDecl 节点然后分别修改了该节点的函数名、参数名以及函数体中表达式的一部分。输出结果是
3.3 使用 quote 插值语法节点
任何 AST 节点都可以在 quote 语句中插值部分 AST 节点的 ArrayList 列表也可以被插值主要对应实际情况中会出现这类节点列表的情况。插值直接通过 $(node) 表达即可其中 node 是任意节点类型的实例。
下面我们通过一些案例展示节点的插值。
var binExpr BinaryExpr(quote(1 2))
let a quote($(binExpr))
let b quote($binExpr)
let c quote($(binExpr.leftExpr))
let d quote($binExpr.leftExpr) // 注意输出
println(a: ${a.toTokens()})
println(b: ${b.toTokens()})
println(c: ${c.toTokens()})
println(d: ${d.toTokens()})一般来说插值运算符后面的表达式使用小括号限定作用域例如 $(binExpr)。但是当后面只跟单个标识符的时候小括号可省略即可写为 $binExpr。因此在案例中 a 和 b 都在 quote 中插入了 binExpr节点结果为 1 2。然而如果插值运算符后面的表达式更复杂不加小括号可能造成作用域出错。例如表达式 binExpr.leftExpr 求值为 1 2 的左表达式即 1因此 c 正确赋值为 1。但 d 中的插值被解释为 ($binExpr).leftExpr因此结果是 1 2.leftExpr。为了明确插值的作用域我们推荐在插值运算符中使用小括号。
下面的案例展示节点列表ArrayList的插值。
import std.ast.*
import std.collection.*main() {var incrs ArrayListNode()for (i in 1..5) {incrs.append(parseExpr(quote(x $(i))))}var foo quote(func foo(n: Int64) {let x n$(incrs)x})println(foo)
}在这个案例中我们创建了一个节点列表 incrs包含表达式 x 1…x 5。对 incrs 的插值将节点依次列出在每个节点后换行。这适用于插入需要依次执行的表达式和声明的情况。
下面的案例展示在某些情况下需要在插值周围添加括号以保证正确性。
var binExpr1 BinaryExpr(quote(x y))
var binExpr2 BinaryExpr(quote($(binExpr1) * z)) // 错误得到 x y * z
println(binExpr2: ${binExpr2.toTokens()})
println(binExpr2.leftExpr: ${binExpr2.leftExpr.toTokens()})
println(binExpr2.rightExpr: ${binExpr2.rightExpr.toTokens()})
var binExpr3 BinaryExpr(quote(($(binExpr1)) * z)) // 正确得到 (x y) * z
println(binExpr3: ${binExpr3.toTokens()})首先我们构建了表达式 x y然后将该表达式插入到模版 $(binExpr1) * z 中。这里的意图是得到一个先计算 x y然后再乘 z 的表达式但是插值的结果是 x y * z先做 y * z然后再加 x。这是因为插值不会自动添加括号以保证被插入的表达式的原子性这和前一阶介绍的 leftExpr 的替换不同。因此需要在 $(binExpr1) 周围添加小括号保证得到正确的结果。
4、宏的实现
本章节介绍仓颉宏的定义和使用仓颉宏可以分为非属性宏和属性宏。同时本章节还会介绍宏出现嵌套时的行为。
4.1 非属性宏
非属性宏只接受被转换的代码不接受其他参数属性其定义格式如下
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_definitionimport std.ast.*public macro testDef(input: Tokens): Tokens {println(Im in macro body)return input
}宏调用文件 main.cj
package macro_callingimport macro_definition.*main(): Int64 {println(Im in function body)let a: Int64 testDef(1 2)println(a ${a})return 0
}上述代码的编译过程可以参考宏的编译和使用。
我们在用例中添加了打印信息其中宏定义中的 Im in macro body 将在编译 macro_call.cj 的期间输出即对宏定义求值。同时宏调用点被展开如编译如下代码
let a: Int64 testDef(1 2)编译器将宏返回的 Tokens 更新到调用点的语法树上得到如下代码
let a: Int64 1 2也就是说可执行程序中的代码实际变为了
main(): Int64 {println(Im in function body)let a: Int64 1 2println(a ${a})return 0
}a 经过计算得到的值为 3在打印 a 的值时插值为 3。至此上述程序的运行结果为
Im in function body
a 3下面看一个更有意义的用宏处理函数的例子这个宏 ModifyFunc 宏的作用是给 MyFunc 增加 Composer 参数并在counter前后插入一段代码。
宏定义文件 macro_definition.cj
// file macro_definition.cj
macro package macro_definitionimport std.ast.*public macro ModifyFunc(input: Tokens): Tokens {println(Im in macro body)let funcDecl FuncDecl(input)return quote(func $(funcDecl.identifier)(id: Int64) {println(start ${id})$(funcDecl.block.nodes)println(end)})
}宏调用文件 main.cj
package macro_callingimport macro_definition.*var counter 0ModifyFunc
func MyFunc() {counter
}func exModifyFunc() {println(Im 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})counterprintln(end)
}MyFunc 会在 main 中调用它接受的实参也是在 main 中定义的从而形成了一段合法的仓颉程序。运行时打印如下
4.2 属性宏
和非属性宏相比属性宏的定义会增加一个 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](23)// attribute macro without parentheses
Foo[public]
struct Data {var count: Int64 100
}宏 Foo 调用当参数是 23 时与 [] 内的属性 1 进行拼接经过宏展开后得到 var a: Int64 123。
宏 Foo 调用当参数是 struct Data 时与 [] 内的属性 public 进行拼接经过宏展开后得到
public struct Data {var count: Int64 100
}关于属性宏需要注意以下几点 带属性的宏与不带属性的宏相比能修饰的 AST 是相同的可以理解为带属性的宏对可传入参数做了增强。 要求属性宏调用时[] 内中括号匹配且可以为空。中括号内只允许对中括号的转义 \[ 或 \]该转义中括号不计入匹配规则其他字符会被作为 Token不能进行转义。
Foo[[miss one](23) // Illegal
Foo[[matched]](23) // Legal
Foo[](23) // Legal, empty in []
Foo[\[](23) // Legal, use escape for [
Foo[\(](23) // Illegal, only [ and ] allowed in []宏的定义和调用的类型要保持一致如果宏定义有两个入参即为属性宏定义调用时必须加上 []且内容可以为空如果宏定义有一个入参即为非属性宏定义调用时不能使用 []。
4.3 嵌套宏
仓颉语言不支持宏定义的嵌套有条件地支持在宏定义和宏调用中嵌套宏调用。
4.3.1 宏定义中嵌套宏调用
下面是一个宏定义中包含其他宏调用的例子。
宏包 pkg1 中定义 getIdent 宏
macro package pkg1import std.ast.*public macro getIdent(attr:Tokens, input:Tokens):Tokens {return quote(let decl (parseDecl(input) as VarDecl).getOrThrow()let name decl.identifier.valuelet size name.size - 1let $(attr) Token(TokenKind.IDENTIFIER, name[0..size]))
}宏包 pkg2 中定义 Prop 宏其中嵌套了 getIdent 宏的调用
macro package pkg2import 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 pkg3import pkg2.*
class A {Propprivate 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.valuelet size name.size - 1let ident Token(TokenKind.IDENTIFIER, name[0 .. size])return quote($(input)public prop $(ident): $(decl.declType) {get() {this.$(v.identifier)}})
}4.3.2 宏调用中嵌套宏调用
嵌套宏的常见场景是宏修饰的代码块中出现了宏调用。一个具体的例子如下
pkg1 包中定义 Foo 和 Bar 宏
macro package pkg1import std.ast.*public macro Foo(input: Tokens): Tokens {return input
}public macro Bar(input: Tokens): Tokens {return input
}pkg2 包中定义 addToMul 宏
macro package pkg2import std.ast.*public macro addToMul(inputTokens: Tokens): Tokens {var expr: BinaryExpr match (parseExpr(inputTokens) as BinaryExpr) {case Some(v) vcase None throw Exception()}var op0: Expr expr.leftExprvar op1: Expr expr.rightExprreturn quote(($(op0)) * ($(op1)))
}pkg3 包中使用上面定义的三个宏
package pkg3import pkg1.*
import pkg2.*
Foo
struct Data {let a 2let b addToMul(23)Barpublic func getA() {return a}public func getB() {return b}
}main(): Int64 {let data Data()var a data.getA() // a 2var b data.getB() // b 6println(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)}
}4.3.3 嵌套宏之间的消息传递
这里指的是宏调用的嵌套。
内层宏可以调用库函数 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 defineimport 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 1Inner 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 函数接收对应的值。
5、编译、报错与调试
5.1 宏的编译和使用
当前编译器约束宏的定义与宏的调用不允许在同一包里。宏包必须首先被编译然后再编译宏调用的包。在宏调用的包中不允许出现宏的定义。由于宏需在包中导出给另一个包使用因此编译器约束宏定义必须使用 public 修饰。
下面介绍一个简单的例子。
源码目录结构如下
// Directory layout.
src
-- macros|-- m.cj-- demo.cj宏定义放在 _macros_子目录下
// macros/m.cj
// In this file, we define the macro Inner, Outer.
macro package define
import std.ast.*public macro Inner(input: Tokens) {return input
}public macro Outer(input: Tokens) {return input
}宏调用代码如下
// demo.cj
import define.*
Outer
class Demo {Inner var state 1Inner var cnt 42
}main() {println(test macro)0
}以下为 Linux 平台的编译命令具体编译选项会随着 cjc 更新而演进以最新 cjc 的编译选项为准
# 当前目录: src
# 先编译宏定义文件在当前目录产生默认的动态库文件允许指定动态库的路径但不能指定动态库的名字
cjc macros/m.cj --compile-macro
# 编译使用宏的文件宏替换完成产生可执行文件
cjc demo.cj -o demo
# 运行可执行文件
./demo在 Linux 平台上将生成用于包管理的 macro_define.cjo 和实际的动态库文件。
若在 Windows 平台
# 当前目录: src
# 先编译宏定义文件在当前目录产生默认的动态库文件允许指定动态库的路径但不能指定动态库的名字
cjc macros/m.cj --compile-macro
# 编译使用宏的文件宏替换完成产生可执行文件
cjc demo.cj -o demo.exe5.2 并行宏展开
可以在编译宏调用文件时添加 --parallel-macro-expansion 选项启用并行宏展开的能力。编译器会自动分析宏调用之间的依赖关系无依赖关系的宏调用可以并行执行如上述例子中的两个 Inner 就可以并行展开如此可以缩短整体编译时间。 如果宏函数依赖一些全局变量使用并行宏展开会存在风险。 macro package define
import std.ast.*
import std.collection.*var Counts HashMapString, Int64()public macro Inner(input: Tokens) {for (t in input) {if (t.value.size 0) {continue}// 统计所有有效token value的出现次数if (!Counts.contains(t.value)) {Counts[t.value] 0}Counts[t.value] Counts[t.value] 1}return input
}public macro B(input: Tokens) {return input
}参考上述代码如果 Inner 的宏调用出现在多处并且启用了并行宏展开选项则访问全局变量 Counts 就可能存在冲突导致最后获取的结果不正确。
建议不要在宏函数中使用全局变量如果必须使用要么关闭并行宏展开选项或者可以通过仓颉线程锁对全局变量进行保护。
5.3 diagReport 报错机制
仓颉 ast 包提供了自定义报错接口 diagReport。方便定义宏的用户在解析传入 tokens 时对错误 tokens 内容进行自定义报错。
自定义报错接口提供同原生编译器报错一样的输出格式允许用户报 warning 和 error 两类错误提示信息。
diagReport 的函数原型如下
public func diagReport(level: DiagReportLevel, tokens: Tokens, message: String, hint: String): Unit其参数含义如下
level: 报错信息等级tokens: 报错信息中所引用源码内容对应的 tokensmessage: 报错的主信息hint: 辅助提示信息
参考如下使用示例。
宏定义文件
// macro_definition.cj
macro package macro_definitionimport std.ast.*public macro testDef(input: Tokens): Tokens {for (i in 0..input.size) {if (input[i].kind IDENTIFIER) {diagReport(DiagReportLevel.ERROR, input[i..(i 1)],This expression is not allowed to contain identifier,Here is the illegal identifier)}}return input
}宏调用文件
// macro_call.cj
package macro_callingimport std.ast.*
import macro_definition.*main(): Int64 {let a testDef(1)let b testDef(a)let c testDef(1 a)return 0
}编译宏调用文件过程中会出现如下报错信息
5.4 使用 --debug-macro 输出宏展开结果
借助宏在编译期做代码生成时如果发生错误处理起来十分棘手这是开发者经常遇到但一般很难定位的问题。这是因为开发者写的源码经过宏的变换后变成了不同的代码片段。编译器抛出的错误信息是基于宏最终生成的代码进行提示的但这些代码在开发者的源码中没有体现。
为了解决这个问题仓颉宏提供 debug 模式在这个模式下开发者可以从编译器为宏生成的 debug 文件中看到完整的宏展开后的代码如下所示。
宏定义文件
macro package defineimport 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)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)return input
}宏调用文件 macro_calling.cj
import define.*Outer
class Demo {Inner var state 1Inner var cnt 42
}main(): Int64 {let d Demo()println(${d.getCnt()})return 0
}在编译宏调用的文件时在选项中增加 --debug-macro即使用仓颉宏的 debug 模式。
cjc --debug-macro macro_calling.cj在 debug 模式下会生成临时文件 macro_calling.cj.macrocall_对应宏展开如下
/*** Created on 2024/7/30*/
package ohos_app_cangjie_entry.studyimport std.ast.*
import ohos_app_cangjie_entry.study.macro_definition.*/* Emitted by MacroCall Outer in macro_calling.cj:9:1 */
/* 9.1 */class Demo {
/* 9.2 */ var state 1
/* 9.3 */ var cnt 42
/* 9.4 */ public func getCnt() {
/* 9.5 */ state cnt 0
/* 9.6 */ }
/* 9.7 */}
/* 9.8 */
/* End of the Emit */main(): Int64 {let d Demo()println(${d.getCnt()})return 0
}如果宏展开后的代码有语义错误则编译器的错误信息会溯源到宏展开后代码的具体行列号。仓颉宏的 debug 模式有以下注意事项
宏的 debug 模式会重排源码的行列号信息不适用于某些特殊的换行场景。比如
// before expansion
M{} - 2 // macro M return 2// after expansion
// Emmitted my Macro M at line 1
2
// End of the Emit
- 2这些因换行符导致语义改变的情形不应使用 debug 模式。
不支持宏调用在宏定义内的调试会编译报错。
public macro M(input: Tokens) {let a M2(12) // M2 is in macro M, not suitable for debug mode.return input quote($a)
}不支持带括号宏的调试。
// main.cjmain() {// For macro with parenthesis, newline introduced by debug will change the semantics// of the expression, so it is not suitable for debug mode.let t M(12)0
}6、宏包定义和导入
仓颉宏的定义需要放在由 macro package 声明的包中被 macro package 限定的包仅允许宏定义对外可见其他声明包内可见。 重导出的声明也允许对外可见关于包管理和重导出的相关概念请参见包的导入章节。 // file define.cj
macro package define // 编译 define.cjo 携带 macro 属性
import std.ast.*public func A() {} // Error, 宏包不允许定义外部可见的非宏定义此处需报错public macro M(input: Tokens): Tokens { // macro M 外部可见return input
}需要特殊说明的是在 macro package 中允许其它 macro package 和非 macro package 符号被重导出在非 macro package 中仅允许非 macro package 符号被重导出。
参考如下示例
在宏包 A 中定义宏 M1
macro package A
import std.ast.*public macro M1(input: Tokens): Tokens {return input
}编译命令如下
cjc A.cj --compile-macro在非宏包 B 中定义一个 public 函数 f1。注意在非 macro package 中无法重导出 macro package 的符号
package B
// public import A.* // Error, it is not allowed to re-export a macro package in a package.public func f1(input: Int64): Int64 {return input
}编译命令如下这里选择使用 --output-type 选项将 B 包编译成到动态库关于 cjc 编译选项介绍可以参考cjc 编译选项章节。
cjc B.cj --output-typedylib -o libB.so在宏包 C 中定义宏 M2依赖了 A 包和 B 包的内容。可以看到 macro package 中可以重导出 macro package 和非 macro package 的符号
macro package C
public import A.* // correct: macro package is allowed to re-export in a macro package.
public import B.* // correct: non-macro package is also allowed to re-export in a macro package.
import std.ast.*public macro M2(input: Tokens): Tokens {return M1(input) Token(TokenKind.NL) quote(f1(1))
}编译命令如下注意这里需要显式链接 B 包动态库
cjc C.cj --compile-macro -L. -lB在 main.cj 中使用 M2 宏
import C.*main() {M2(let a 1)
}编译命令如下
cjc main.cj -o main -L. -lBmain.cj中 M2 宏展开后的结果如下
import C.*main() {let a 1f1(1)
}可以看到 main.cj 中出现了来自于 B 包的符号 f1。宏的编写者可以在 C 包中重导出 B 包里的符号这样宏的使用者仅需导入宏包就可以正确的编译宏展开后的代码。如果在 main.cj 中仅使用 import C.M2 导入宏符号则会报 undeclared identifier ‘f1’ 的错误信息。
7、内置编译标记
仓颉语言提供了一些预定义的编译标记可以通过这些编译标记控制仓颉编译器的编译行为。
7.1 源码位置
仓颉提供了几个内置编译标记用于在编译时获取源代码的位置。
sourcePackage() 展开后是一个 String 类型的字面量内容为当前宏所在的源码的包名sourceFile() 展开后是一个 String 类型的字面量内容为当前宏所在的源码的文件名sourceLine() 展开后是一个 Int64 类型的字面量内容为当前宏所在的源码的代码行
这几个编译标记可以在任意表达式内部使用只要能符合类型检查规则即可。示例如下
func test1() {let s: String sourceFile() // The value of s is the current source file name
}func test2(n!: Int64 sourceLine()) { /* at line 5 */// The default value of n is the source file line number of the definition of test2println(n) // print 5
}7.2 条件编译
条件编译使用 When 标记是一种在程序代码中根据特定条件选择性地编译不同代码段的技术。条件编译的作用主要体现在以下几个方面
平台适应支持根据当前的编译环境选择性地编译代码用于实现跨平台的兼容性。功能选择支持根据不同的需求选择性地启用或禁用某些功能用于实现功能的灵活配置。例如选择性地编译包含或排除某些功能的代码。调试支持支持调试模式下编译相关代码用于提高程序的性能和安全性。例如在调试模式下编译调试信息或记录日志相关的代码而在发布版本中将其排除。性能优化支持根据预定义的条件选择性地编译代码用于提高程序的性能。
关于条件编译的具体内容可以参考条件编译章节这里不再额外展开。
7.3 FastNative
为了提升与 C 语言互操作的性能仓颉提供 FastNative 标记用于优化对 C 函数的调用。值得注意的是 FastNative 只能用于 foreign 声明的函数。
使用示例如下
FastNative
foreign func strlen(str: CPointerUInt8): UIntNative开发者在使用 FastNative 修饰 foreign 函数时应确保对应的 C 函数满足以下两点要求
函数的整体执行时间不宜太长。例如:不允许函数内部存在很大的循环不允许函数内部产生阻塞行为如调用 sleep、wait 等函数。函数内部不能调用仓颉方法。
8、实用案例重要
8.1 快速幂的计算
我们通过一个简单的例子展示使用宏进行编译期求值生成优化代码的应用。在计算幂 n ^ e 的时候如果 e 是一个比较大的整数可以通过重复取平方而不是迭代相乘的方式加速计算。这个算法可以直接使用 while 循环实现例如
func power(n: Int64, e: Int64) {var result 1var vn nvar ve ewhile (ve 0) {if (ve % 2 1) {result * vn}ve / 2if (ve 0) {vn * vn}}result
}然而这个实现需要每次对 e 的值进行分析在循环和条件判断中多次对 ve 进行判断和更新。此外实现只支持 n 的类型为Int64的情况如果要支持其他类型的 n还要处理如何表达 result 1 的问题。如果我们预先知道 e 的具体值可以将这个代码写的更简单。例如如果知道 e 的值为 10我们可以展开整个循环如下
func power_10(n: Int64) {var vn nvn * vn // vn n ^ 2var result vn // result n ^ 2vn * vn // vn n ^ 4vn * vn // vn n ^ 8result * vn // result n ^ 10result
}当然手动编写这些代码非常繁琐我们希望在给定 e 的值之后自动将这些代码生成出来。宏允许我们做到这一点。我们先看使用案例
public func power_10(n: Int64) {power[10](n)
}这个宏展开的代码是根据.macrocall文件
public func power_10(n: Int64) {/* Emitted by MacroCall power in main.cj:20:5 *//* 20.1 */var _power_vn n/* 20.2 */_power_vn * _power_vn/* 20.3 */var _power_result _power_vn/* 20.4 */_power_vn * _power_vn/* 20.5 */_power_vn * _power_vn/* 20.6 */_power_result * _power_vn/* 20.7 */_power_result
/* End of the Emit */
}下面我们看宏 power 的实现。
macro package defineimport std.ast.*
import std.convert.*public macro power(attrib: Tokens, input: Tokens) {let attribExpr parseExpr(attrib)if (let Some(litExpr) - attribExpr as LitConstExpr) {let lit litExpr.literalif (lit.kind ! TokenKind.INTEGER_LITERAL) {diagReport(DiagReportLevel.ERROR, attrib,Attribute must be integer literal,Expected integer literal)}var n Int64.parse(lit.value)var result quote(var _power_vn $(input))var flag falsewhile (n 0) {if (n % 2 1) {if (!flag) {result quote(var _power_result _power_vn)flag true} else {result quote(_power_result * _power_vn)}}n / 2if (n 0) {result quote(_power_vn * _power_vn)}}result quote(_power_result)return result} else {diagReport(DiagReportLevel.ERROR, attrib,Attribute must be integer literal,Expected integer literal)}return input
}这段代码的解释如下
首先确认输入的属性 attrib 是一个整数字面量否则通过 diagReport 报错。将这个字面量解析为整数 n。设 result 为当前积累的输出代码首先添加 var _power_vn 的声明。这里为了避免变量名冲突我们使用不易造成冲突的名字 _power_vn。下面进入 while 循环布尔变量 flag 表示 var _power_result 是否已经被初始化。其余的代码结构和之前展示的 power 函数的实现类似但区别是我们使用 while 循环和 if 判断在编译时决定生成的代码是什么而不是在运行时做这些判断。最后生成由 _power_result * _power_vn 和 _power_vn * _power_vn 适当组合的代码。最后添加返回 _power_result 的代码。
将这段代码放到 macros/power.cj 文件中并在 main.cj 添加如下测试
public func power_10(n: Int64) {power[10](n)
}main() {let a 3println(power_10(a)) // 59049
}8.2 Memoize 宏
Memoize记忆化是动态规划算法的常用手段。它将已经计算过的子问题的结果存储起来当同一个子问题再次出现时可以直接查询表来获取结果从而避免重复的计算提高算法的效率。
通常 Memoize 的使用需要开发者手动实现存储和提取的功能。通过宏我们可以自动化这一过程。首先让我们先看一下宏使用的效果
Memoize[true]
func fib(n: Int64): Int64 {if (n 0 || n 1) {return n}return fib(n - 1) fib(n - 2)
}main() {let start DateTime.now()let f35 fib(35)let end DateTime.now()println(fib(35): ${f35})println(execution time: ${(end - start).toMicroseconds()} us)
}在以上代码中fib 函数采用简单的递归方式实现。如果没有 Memoize[true] 标注这个函数的运行时间将随着 n 指数增长。例如如果在前面的代码中去掉 Memoize[true] 这一行或者把 true 改为 false则 main 函数的运行结果为 恢复 Memoize[true]运行结果为 相同的答案和大幅缩短的计算时间表明Memoize 的使用确实实现了记忆化。
现在让我们理解 Memoize 的原理。首先展示对以上 fib 函数进行宏展开的结果来自 .macrocall 文件但是为了提高可读性整理了格式。
import std.collection.*var _memoize_fib_map HashMapInt64, Int64()func fib(n: Int64): Int64 {if (_memoize_fib_map.contains(n)) {return _memoize_fib_map.get(n).getOrThrow()}let _memoize_eval_result { if (n 0 || n 1) {return n}return fib(n - 1) fib(n - 2)}()_memoize_fib_map.put(n, _memoize_eval_result)return _memoize_eval_result
}上述代码的执行流程如下
首先定义 _memoize_fib_map 为一个从 Int64 到 Int64 的哈希表这里第一个 Int64 对应 fib 的唯一参数的类型第二个 Int64 对应 fib 返回值的类型。其次在函数体中检查入参是否在 _memoize_fib_map 中如果是则立即返回哈希表中存储的值。否则使用 fib 原来的函数体得到计算结果。这里使用了不带参数的匿名函数使 fib 的函数体不需要任何改变并且能够处理任何从 fib 函数退出的方式包括中间的 return返回最后一个表达式等。最后把计算结果存储到 _memoize_fib_map 中然后将计算结果返回。
有了这样一个“模版”之后下面宏的实现就不难理解了。我们给出完整的代码如下。
public macro Memoize(attrib: Tokens, input: Tokens) {if (attrib.size ! 1 || attrib[0].kind ! TokenKind.BOOL_LITERAL) {diagReport(DiagReportLevel.ERROR, attrib,Attribute must be a boolean literal (true or false),Expected boolean literal (true or false) here)}let memoized (attrib[0].value true)if (!memoized) {return input}let fd FuncDecl(input)if (fd.funcParams.size ! 1) {diagReport(DiagReportLevel.ERROR, fd.lParen fd.funcParams.toTokens() fd.rParen,Input function to memoize should take exactly one argument,Expect only one argument here)}let memoMap Token(TokenKind.IDENTIFIER, _memoize_ fd.identifier.value _map)let arg1 fd.funcParams[0]return quote(var $(memoMap) HashMap$(arg1.paramType), $(fd.declType)()func $(fd.identifier)($(arg1)): $(fd.declType) {if ($(memoMap).contains($(arg1.identifier))) {return $(memoMap).get($(arg1.identifier)).getOrThrow()}let _memoize_eval_result { $(fd.block.nodes) }()$(memoMap).put($(arg1.identifier), _memoize_eval_result)return _memoize_eval_result})
}首先对属性和输入做合法性检查。属性必须是布尔字面量如果为 false 则直接返回输入。否则检查输入必须能够解析为函数声明FuncDecl并且必须包含正好一个参数。下面产生哈希表的变量取不容易造成冲突的变量名。最后通过 quote 模版生成返回的代码其中用到哈希表的变量名以及唯一参数的名称、类型和输入函数的返回类型。
8.3 一个 dprint 宏的扩展
本节一开始使用了一个打印表达式的宏作为案例但这个宏一次只能接受一个表达式。我们希望扩展这个宏使其能够接受多个表达式由逗号分开。我们展示如何使用 parseExprFragment 来实现这个功能。
宏的实现如下
public macro dprint2(input: Tokens) {let exprs ArrayListExpr()var index: Int64 0while (true) {let (expr, nextIndex) parseExprFragment(input, startFrom: index)exprs.append(expr)if (nextIndex input.size) {break}if (input[nextIndex].kind ! TokenKind.COMMA) {diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex1],Input must be a comma-separated list of expressions,Expected comma)}index nextIndex 1 // 跳过逗号}let result quote()for (expr in exprs) {result.append(quote(print($(expr.toTokens().toString()) )println($(expr))))}return result
}使用案例
let x 3
let y 2
dprint2(x, y, x y)在宏的实现中使用 while 循环从索引 0 开始依次解析每个表达式。变量 index 保存当前解析的位置。每次调用 parseExprFragment 时从当前位置开始并返回解析后的位置以及解析得到的表达式。如果解析后的位置到达了输入的结尾则退出循环。否则检查到达的位置是否是一个逗号如果不是逗号报错并退出如果是逗号跳过这个逗号并开始下一轮的解析。在得到表达式的列表后依次输出每个表达式。
8.4 一个简单的 DSL
在这个案例中我们展示如何使用宏实现一个简单的 DSLDomain Specific Language领域特定语言。LINQLanguage Integrated Query语言集成查询是微软 .NET 框架的一个组成部分它提供了一种统一的数据查询语法允许开发者使用类似 SQL 的查询语句来操作各种数据源。在这里我们仅展示一个最简单的 LINQ 语法的支持。
我们希望支持的语法为
from variable in list where condition select expression其中variable 是一个标识符list、condition 和 expression 都是表达式。因此实现宏的策略是先后提取标识符和表达式同时检查中间的关键字是正确的。最后生成由提取部分组成的查询结果。
宏的实现如下
public macro linq(input: Tokens) {let syntaxMsg Syntax is \from attrib in table where cond select expr\if (input.size 0 || input[0].value ! from) {diagReport(DiagReportLevel.ERROR, input[0..1], syntaxMsg,Expected keyword \from\ here.)}if (input.size 1 || input[1].kind ! TokenKind.IDENTIFIER) {diagReport(DiagReportLevel.ERROR, input[1..2], syntaxMsg,Expected identifier here.)}let attribute input[1]if (input.size 2 || input[2].value ! in) {diagReport(DiagReportLevel.ERROR, input[2..3], syntaxMsg,Expected keyword \in\ here.)}var index: Int64 3let (table, nextIndex) parseExprFragment(input, startFrom: index)if (nextIndex input.size || input[nextIndex].value ! where) {diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex1], syntaxMsg,Expected keyword \where\ here.)}index nextIndex 1 // 跳过wherelet (cond, nextIndex2) parseExprFragment(input, startFrom: index)if (nextIndex2 input.size || input[nextIndex2].value ! select) {diagReport(DiagReportLevel.ERROR, input[nextIndex2..nextIndex21], syntaxMsg,Expected keyword \select\ here.)}index nextIndex2 1 // 跳过selectlet (expr, nextIndex3) parseExprFragment(input, startFrom: index)return quote(for ($(attribute) in $(table)) {if ($(cond)) {println($(expr))}})
}使用案例
linq(from x in 1..10 where x % 2 1 select x * x)这个例子从 1, 2, … 10 列表中筛选出奇数然后返回所有奇数的平方。输出结果为 可以看到宏的实现的很大部分用于解析并校验输入的 tokens这对宏的可用性至关重要。实际的 LINQ 语言以及大多数 DSL的语法更加复杂需要一整套解析的机制通过识别不同的关键字或连接符来决定下一步解析的内容。