宏
-
宏和函数类似,只不过它操作的是语法树,输入语法树输出修改过的语法树。这个输出的语法树会被插入到程序中
-
仓颉目前提供的是过程宏,它在
词法分析阶段做宏展开。后续还会有Late-stage宏、模板宏等 -
仓颉的过程宏接受一个 token 序列作为输入,对其进行处理和变换后,输出另一个 token 序列
-
宏操作的基本类型是 Tokens,代表一个程序片段。Tokens 是由多个 Token 组成的序列,
每个 Token 包含它的类型、内容和位置信息- 通过 Token 数组构造 Tokens
- Token 的类型取值为 enum TokenKind 中的元素
- 通过提供 TokenKind 和(对于标识符和字面量)Token 的字符串,可以直接构造任何 Token
-
使用Token和Tokens构造Tokens比较麻烦,可以使用quote 表达式来从代码模版构造 Tokens
-
在 quote 中可以使用 $(...) 来插入上下文中的表达式。
-
插入的表达式的类型需要支持被转换为 Tokens(具体来说,实现了 ToTokens 接口),包括以下方面
-
所有的节点类型
-
仓颉编译过程中,首先通过词法分析将代码转换成 Tokens,然后对 Tokens 进行语法解析,得到一个语法树
-
每个语法树的节点可能是一个表达式、声明、类型、模式等
-
仓颉 ast 库提供了每种节点对应的类,它们之间具有适当的继承关系
-
Node:所有语法节点的父类
-
TypeNode:所有类型节点的父类
-
Expr:所有表达式节点的父类
-
Decl:所有声明节点的父类
-
Pattern:所有模式节点的父类
-
有两种方式解析节点
- 通过函数,有用于解析表达式的函数(parseExpr,parseExprFragment)和用于解析声明的函数(parseDecl,parseDeclFragment)两种
- 通过对应节点类型的构造器,将输入的 Tokens 解析为相应类型的节点
-
-
Token 和 Tokens 类型
-
所有基础数据类型:整数、浮点数、Bool、Rune和String
-
Array<T> 和 ArrayList<T>,这里对 T 的类型有限制,并根据 T 的类型不同,输出不同的分隔符
-
quote 表达式中
不允许出现不匹配的小括号,通过\转义的小括号,不计入小括号的匹配规则。 -
当 $ 表示一个普通 Token,而非用于代码插值时,需要通过\进行转义。
-
除以上情况外,quote 表达式中出现\会编译报错
-
-
-
非属性宏 只接受被转换的代码,不接受其他参数(属性)
-
定义格式:
public macro 宏名称(input: Tokens): Tokens -
调用格式:
@宏名称(输入tokens)-
宏调用使用 () 括起来。括号里面可以是任意合法 Tokens,也可以是空
-
当宏作用于声明时,一般可以省略括
-
输入的内容有要求
- 必须是由
合法的 Token 组成的序列 - 若存在不匹配的小括号则必须使用转义符号 "" 对其进行转义
- 若希望 "@" 作为输入的 Token 则必须使用转义符号 "" 对其进行转义
- 必须是由
-
-
-
属性宏 的定义比非属性宏多增加一个 Tokens 类型的输入,这个参数在第一个位置
- 定义格式:
public macro 宏名称(args: Tokens, input: Tokens): Tokens - 调用格式:
@宏名称[参数tokens](输入tokens)
- 定义格式:
-
属性宏和非属性宏,可以修饰同样的AST。只是属性宏对传入参数做了增强。中括号和小括号的输入内容的规则也和非属性宏一样
-
宏的定义和调用的类型要保持一致- 如果宏定义有两个入参,即为属性宏定义,调用时必须加上 [],且内容可以为空;
- 如果宏定义有一个入参,即为非属性宏定义,调用时不能使用 []
-
-
有条件地支持在宏定义和宏调用中嵌套宏调用
- 可以在宏定义中调用另一个宏
- 可以在宏调用的类型中,给其内部变量调用另一个宏
- 内层宏可以通过assertParentContext断言自己处于另一个宏中,也可以使用InsideParentContext检查自己是否处于另一个宏中
- 内层宏可以通过setItem给外层宏传递消息,外层宏通过getChildMessages获取内层宏传递的消息。因为内存宏可能有多个,所以返回的是个数组
-
编译器对宏的定义和宏的调用有一些要求
宏的定义与宏的调用不允许在同一包里宏定义必须使用 public 修饰宏定义的包必须首先被编译(--compile-macro),然后再编译宏调用的包在宏调用的包中,不允许出现宏的定义- 可以在编译宏调用文件时添加
--parallel-macro-expansion选项,启用并行宏展开的能力 - 可以使用自定义报错接口 diagReport对宏定义过程中的错误进行编译器提示。支持warning和error两类错误信息
- 可以使用
--debug-macro选项让编译器生成宏展开的代码,一般以.macrocall文件结尾
-
宏包的定义和导出有一些规则
- 宏定义包的声明以 macro package 开头
- 宏定义包中仅允许宏定义对外可见(public),其他声明包内可见(internal)
- 在 macro package 中允许其它 macro package 和非 macro package 符号被重导出
- 在非 macro package 中仅允许非 macro package 符号被重导出
- 重导出语法: public cj_exercise.macros.stringify
// 宏必须声明在独立的包中(不能和其他 public 函数一起)
// 含有宏的包使用 macro package 来声明
macro package cj_exercise.macrodefines
import cj_exercise.anothermacro.*
import std.ast.*
// 原始tokens
public macro orgTokens(input: Tokens): Tokens {
input
}
// 宏类似函数,只不过它的输入和输出都是Tokens。 宏调用的时候需要在前面加上@
// 宏操作的基本类型是 Tokens,代表一个程序片段
// Tokens 由若干个 Token 组成,每个 Token 可以理解为用户可操作的词法单元
// 每个 Token 包含它的类型、内容和位置信息
// 通过提供 TokenKind 和(对于标识符和字面量)Token 的字符串,可以直接构造任何 Token
public macro dprint(input: Tokens): Tokens {
// 将输入的程序片段转化为字符串
let inputStr = input.toString()
// quote 表达式是用于构造 Tokens 的一种表达式,它将括号内的程序片段转换为 Tokens
let result = quote(
print($(inputStr) + " = ")
println($(input))
)
return result
}
public macro tokensDemo(input: Tokens): Tokens {
let tks = Tokens(
Array<Token>(
[
Token(TokenKind.IDENTIFIER, '1'),
Token(TokenKind.ADD),
Token(TokenKind.INTEGER_LITERAL, '2')
]
)
)
println('tks size ${tks.size}')
println('tks[0] ${tks.get(0).value}')
println('tks[0] ${tks[0].value}')
tks.dump()
println('tks.toString ${tks.toString()}')
return input
}
public macro quoteDemo(input: Tokens): Tokens {
// quote 中可以使用 $(...) 来插入上下文中的表达式。插入的表达式的类型需要支持被转换为 Tokens(具体来说,实现了 ToTokens 接口)
let intList = Array<Int>([1, 2, 3, 4, 5])
let float: Float64 = 1.0
let str: String = '仓颉'
let tokens = quote(
arr = $(intList)
x = $(float)
s = $(str)
)
println(tokens)
return input
}
public macro tokenDemo(input: Tokens): Tokens {
// Node:所有语法节点的父类
// TypeNode:所有类型节点的父类
// Expr:所有表达式节点的父类
// Decl:所有声明节点的父类
// Pattern:所有模式节点的父类
// 使用 解析表达式和声明的 函数
// parseExpr(input: Tokens): Expr:将输入的 Tokens 解析为表达式
// parseExprFragment(input: Tokens, startFrom!: Int64 = 0): (Expr, Int64):将输入 Tokens 的一个片段解析为表达式,片段从 startFrom 索引开始,解析可能只消耗从索引 startFrom 开始的片段的一部分,并返回第一个未被消耗的 Token 的索引(如果消耗了整个片段,返回值为 input.size)
// parseDecl(input: Tokens, astKind!: String = ""):将输入的 Tokens 解析为声明,astKind 为额外的设置,具体请见《仓颉编程语言库 API》文档。
// parseDeclFragment(input: Tokens, startFrom!: Int64 = 0): (Decl, Int64):将输入 Tokens 的一个片段解析为声明,startFrom 参数和返回索引的含义和 parseExpr 相同
let tks1 = quote(a + b)
let binExpr1 = parseExpr(tks1)
println("binExpr1 is BinaryExpr: ${binExpr1 is BinaryExpr}")
let tks2 = quote(a + b, x + y)
let (binExpr2, mid) = parseExprFragment(tks2)
let (binExpr3, end) = parseExprFragment(tks2, startFrom: mid + 1) // 跳过逗号
println("size = ${tks2.size}, mid = ${mid}, end = ${end}")
let tks3 = quote(
func f1(x: Int64) { return x + 1 }
)
let funcDecl1 = parseDecl(tks3)
println("funcDecl1 is FuncDecl: ${funcDecl1 is FuncDecl}")
let tks4 = quote(
func f1(x: Int64) { return x + 1 }
func f2(x: Int64) { return x + 2 }
)
let (funcDecl2, mid2) = parseDeclFragment(tks4)
let (funcDecl3, end2) = parseDeclFragment(tks4, startFrom: mid2)
println("size = ${tks4.size}, mid = ${mid2}, end = ${end2}")
// 使用构造函数进行解析
// let binExpr = BinaryExpr(quote(a + b))
// let funcDecl = FuncDecl(quote(func f1(x: Int64) { return x + 1 }))
// Token 的每个组成部分都是 public mut prop
// let binExpr = BinaryExpr(quote(x * y))
// binExpr.leftExpr = BinaryExpr(quote(a + b))
// // ast 库具备在输出语法树时自动添加括号的功能
// println(binExpr.toTokens().toString())
// binExpr.op = Token(TokenKind.ADD)
// println(binExpr.toTokens())
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())
return input
}
public macro quoteCorrectDemo(input: Tokens): Tokens {
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()}")
return input
}
// 使用宏之前,需要先编译宏 cjc ./src/macrodefines/*.cj --compile-macro
// 非属性宏
// 非属性宏只接受被转换的代码,不接受其他参数(属性)
// 当宏作用于声明时,一般可以省略括号
public macro noneAttributeDemo(input: Tokens): Tokens {
// 宏展开过程作用于仓颉语法树,宏展开后,编译器会继续进行后续的编译过程
// 在编译 macro_call.cj 的期间输出,即对宏定义求值
println("在宏内部")
return input
}
// 属性宏
// 属性宏的定义会增加一个 Tokens 类型的输入
// 属性宏调用时新增的入参 attrTokens 通过 [] 传入
public macro attributeDemo(attr: Tokens, input: Tokens): Tokens {
println('${attr} ${input}')
return attr + input
}
// 仓颉语言不支持宏定义的嵌套;有条件地支持在宏定义和宏调用中嵌套宏调用
// 宏定义中嵌套宏调用
public macro embedInvokeDemo(input: Tokens): Tokens {
let v = parseDecl(input)
@getIdent[ident](input)
return quote(
$(input)
public prop $(ident): $(decl.declType) {
get() {
this.$(v.identifier)
}
}
)
}
// 宏调用嵌套宏调用
// 将嵌套内层的宏(addToMul 和 Bar)展开后,再去展开外层的宏(Foo)。
// 允许出现多层宏嵌套,代码变换的规则总是由内向外去依次展开宏
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)))
}
// 嵌套宏之间可以传递消息
// 内层宏可以调用库函数 assertParentContext 来保证内层宏调用一定嵌套在特定的外层宏调用中
// 库函数 InsideParentContext 同样用于检查内层宏调用是否嵌套在特定的外层宏调用中,该函数返回一个布尔值
public macro Outer(input: Tokens): Tokens {
// Outer 宏通过 getChildMessages 函数接收到 Inner 发送的一组信息对象(Outer 中可以调用多次 Inner)
let messages = getChildMessages("Inner")
let getTotalFunc = quote(public func getCnt() {
)
for (m in messages) {
// 通过该信息对象的 getString 函数接收对应的值
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")
if (insideParentContext('Outer')) {
println('Inner宏处于Outer宏内部')
} else {
println('Inner宏未处于Outer宏内部')
}
// 内层宏也可以通过发送键/值对的方式与外层宏通信
// 当内层宏执行时,通过调用标准库函数 setItem 向外层宏发送信息;
// 随后,当外层宏执行时,调用标准库函数 getChildMessages 接收每一个内层宏发送的信息(一组键/值对映射
let decl = parseDecl(input)
// 内层宏 Inner 通过 setItem 向外层宏发送信息
setItem("identifierName", decl.identifier.value)
return input
}
public macro diagDemo(input: Tokens): Tokens {
for (i in 0..input.size) {
println(input[i].toTokens())
if (input[i].kind == IDENTIFIER) {
diagReport(DiagReportLevel.ERROR, input[i..(i + 1)], "diagDemo修饰的表达式中不允许包含标识符", "不合法的标志符")
}
}
return input
}
// 可使用的宏
macro package cj_exercise.usablemacro
import std.ast.{Token, Tokens, ToTokens, TokenKind, Expr, LitConstExpr, parseExpr, parseExprFragment, diagReport,
DiagReportLevel, FuncDecl}
import std.convert.Parsable
import std.collection.ArrayList
// @power[10](n)
public macro power(attrib: Tokens, input: Tokens) {
let attribExpr = parseExpr(attrib)
// 确认输入的属性 attrib 是一个整数字面量,否则通过 diagReport 报错
if (let Some(litExpr) <- attribExpr as LitConstExpr) {
let lit = litExpr.literal
if (lit.kind != TokenKind.INTEGER_LITERAL) {
diagReport(DiagReportLevel.ERROR, attrib, "属性必须是整型字面量", "期望整型字面量")
}
// 将这个字面量解析为整数 n
var n = Int64.parse(lit.value)
// 设 result 为当前积累的输出代码,首先添加 var _power_vn 的声明
// 这里)换行,是为了最终生成的文件代码换行
var result = quote(var _power_vn = $(input)
)
// 布尔变量 flag 表示 var _power_result 是否已经被初始化
var flag = false
while (n > 0) {
// 奇数和偶数都会走进这里。所以_power_result一定有
// 奇数 首次就会进这里
// 偶数 倒数第二次得到的是1,也会走到这里
// 如果整除2过程中 得到计数,就会走进这里
if (n % 2 == 1) {
if (!flag) {
// 代码 var _power_result = _power_vn
result += quote(var _power_result = _power_vn
)
flag = true
} else {
// 代码 _power_result *= _power_vn
result += quote(_power_result *= _power_vn
)
}
}
n /= 2
if (n > 0) {
// 平方
result += quote(_power_vn *= _power_vn
)
}
}
// 添加返回 _power_result 的代码
result += quote(_power_result)
return result
} else {
diagReport(DiagReportLevel.ERROR, attrib, "属性必须是整型字面量", "期望整型字面量")
}
return input
}
// @Memoize[true]
public macro Memoize(attrib: Tokens, input: Tokens) {
// 对属性和输入做合法性检查。属性必须是布尔字面量
if (attrib.size != 1 || attrib[0].kind != TokenKind.BOOL_LITERAL) {
diagReport(DiagReportLevel.ERROR, attrib, "属性必须是bool字面量(true 或 false)",
"期望 bool字面量(true 或 false)")
}
let memoized = (attrib[0].value == "true")
// 如果为 false 则直接返回输入
if (!memoized) {
return input
}
let fd = FuncDecl(input)
// 检查输入必须能够解析为函数声明(FuncDecl),并且必须包含正好一个参数
if (fd.funcParams.size != 1) {
diagReport(DiagReportLevel.ERROR, fd.lParen + fd.funcParams.toTokens() + fd.rParen, "修饰的函数必须只有一个参数",
"期望函数只有一个参数")
}
// HashMap变量名。 _memoize_原始func名字_map
let memoMap = Token(TokenKind.IDENTIFIER, "_memoize_" + fd.identifier.value + "_map")
// 函数参数
let arg1 = fd.funcParams[0]
return quote(
// 创建HashMap, Key为func参数类型,Value为func返回类型
var $(memoMap) = HashMap<$(arg1.paramType), $(fd.declType)>()
// 重新实现原有函数。原有函数名称、原有函数参数、原有函数返回值类型
func $(fd.identifier)($(arg1)): $(fd.declType) {
// 取参数作为key所对应的值,如果有就返回
if ($(memoMap).contains($(arg1.identifier))) {
return $(memoMap).get($(arg1.identifier)).getOrThrow()
}
// 将原有函数的实现包装到闭包中,立即执行
let _memoize_eval_result = { =>
$(fd.block.nodes)
}()
// 将执行结果保存到HashMap中
$(memoMap).put($(arg1.identifier), _memoize_eval_result)
return _memoize_eval_result
}
)
}
// @dprint(x, y, x + y)
public macro dprint(input: Tokens) {
let exprs = ArrayList<Expr>()
// 变量 index 保存当前解析的位置
var index: Int64 = 0
// 使用 while 循环从索引 0 开始依次解析每个表达式
while (true) {
// 每次调用 parseExprFragment 时,从当前位置开始,并返回解析后的位置(以及解析得到的表达式)
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..nextIndex + 1], "必须是逗号分割的多个表达式", "期望逗号")
}
index = nextIndex + 1 // 跳过逗号
}
// 在得到表达式的列表后,依次输出每个表达式
let result = quote()
for (expr in exprs) {
result.append(
quote(
print($(expr.toTokens().toString()) + " = ")
println($(expr))
))
}
return result
}
// @linq(from x in 1..=10 where x % 2 == 1 select x * x)
public macro linq(input: Tokens) {
// from <variable> in <list> where <condition> select <expression>
// variable 是一个标识符,list、condition 和 expression 都是表达式
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, "期望 from 关键字")
}
if (input.size <= 1 || input[1].kind != TokenKind.IDENTIFIER) {
diagReport(DiagReportLevel.ERROR, input[1..2], syntaxMsg, "期望一个标识符")
}
let attribute = input[1]
if (input.size <= 2 || input[2].value != "in") {
diagReport(DiagReportLevel.ERROR, input[2..3], syntaxMsg, "期望 in 关键字")
}
var index: Int64 = 3
let (table, nextIndex) = parseExprFragment(input, startFrom: index)
if (nextIndex == input.size || input[nextIndex].value != "where") {
diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex + 1], syntaxMsg, "期望 where 关键字")
}
index = nextIndex + 1 // 跳过where
let (cond, nextIndex2) = parseExprFragment(input, startFrom: index)
if (nextIndex2 == input.size || input[nextIndex2].value != "select") {
diagReport(DiagReportLevel.ERROR, input[nextIndex2..nextIndex2 + 1], syntaxMsg, "期望 select 关键字")
}
index = nextIndex2 + 1 // 跳过select
let (expr, nextIndex3) = parseExprFragment(input, startFrom: index)
return quote(
for ($(attribute) in $(table)) {
if ($(cond)) {
println($(expr))
}
}
)
}
public macro stringify(input: Tokens): Tokens {
let inputStr = input.toString()
let result = quote(
{ =>
print($(inputStr) + " = ")
println($(input))
$(input)
}())
return result
}
public macro stringify2(input: Tokens): Tokens {
let inputStr = input.toString()
let ret = input
return quote(
println($(inputStr) + ' = ' + ($(input)).toString())
)
}
参考资料
- 仓颉编程语言开发指南 developer.huawei.com/consumer/cn…
- 仓颉编程语言白皮书 developer.huawei.com/consumer/cn…
- 仓颉编程语言语言规约 developer.huawei.com/consumer/cn…
- 白皮书中的宏 developer.huawei.com/consumer/cn…