编译、报错与调试
宏的编译和使用
当前编译器约束宏的定义与宏的调用不允许在同一包里。宏包必须首先被编译,然后再编译宏调用的包。在宏调用的包中,不允许出现宏的定义。由于宏需在包中导出给另一个包使用,因此编译器约束宏定义必须使用 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 = 1
@Inner 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.exe
说明:
宏替换过程依赖仓颉 runtime ,宏替换过程中仓颉 runtime 的初始化配置采用宏提供的默认配置,配置参数支持使用仓颉 runtime 运维日志进行查询,其中 cjHeapSize 与 cjStackSize 支持用户修改,其余暂不支持,仓颉 runtime 初始化配置可参见runtime 初始化可选配置章节。
并行宏展开
可以在编译宏调用文件时添加 --parallel-macro-expansion
选项,启用并行宏展开的能力。编译器会自动分析宏调用之间的依赖关系,无依赖关系的宏调用可以并行执行,如上述例子中的两个 @Inner
就可以并行展开,如此可以缩短整体编译时间。
注意:
如果宏函数依赖一些全局变量,使用并行宏展开会存在风险。
macro package define
import std.ast.*
import std.collection.*
var Counts = HashMap<String, 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
就可能存在冲突,导致最后获取的结果不正确。
建议不要在宏函数中使用全局变量,如果必须使用,要么关闭并行宏展开选项,或者可以通过仓颉线程锁对全局变量进行保护。
diagReport 报错机制
仓颉 ast 包提供了自定义报错接口 diagReport
。方便定义宏的用户,在解析传入 tokens 时,对错误 tokens 内容进行自定义报错。
自定义报错接口提供同原生编译器报错一样的输出格式,允许用户报 warning 和 error 两类错误提示信息。
diagReport
的函数原型如下:
public func diagReport(level: DiagReportLevel, tokens: Tokens, message: String, hint: String): Unit
其参数含义如下:
- level: 报错信息等级
- tokens: 报错信息中所引用源码内容对应的 tokens
- message: 报错的主信息
- hint: 辅助提示信息
参考如下使用示例。
宏定义文件:
// macro_definition.cj
macro package macro_definition
import 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_calling
import std.ast.*
import macro_definition.*
main(): Int64 {
let a = @testDef(1)
let b = @testDef(a)
let c = @testDef(1 + a)
return 0
}
编译宏调用文件过程中,会出现如下报错信息:
error: This expression is not allowed to contain identifier
==> call.cj:9:22:
|
9 | let b = @testDef(a)
| ^ Here is the illegal identifier
|
error: This expression is not allowed to contain identifier
==> call.cj:10:26:
|
10 | let c = @testDef(1 + a)
| ^ Here is the illegal identifier
|
2 errors generated, 2 errors printed.
使用 --debug-macro 输出宏展开结果
借助宏在编译期做代码生成时,如果发生错误,处理起来十分棘手,这是开发者经常遇到但一般很难定位的问题。这是因为,开发者写的源码,经过宏的变换后变成了不同的代码片段。编译器抛出的错误信息是基于宏最终生成的代码进行提示的,但这些代码在开发者的源码中没有体现。
为了解决这个问题,仓颉宏提供 debug 模式,在这个模式下,开发者可以从编译器为宏生成的 debug 文件中看到完整的宏展开后的代码,如下所示。
宏定义文件:
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")
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
}
宏调用文件 demo.cj
:
import define.*
@Outer
class Demo {
@Inner var state = 1
@Inner var cnt = 42
}
main(): Int64 {
let d = Demo()
println("${d.getCnt()}")
return 0
}
在编译使用宏的文件时,在选项中,增加 --debug-macro
,即使用仓颉宏的 debug 模式。
cjc --debug-macro demo.cj
在 debug 模式下,会生成临时文件 demo.cj.macrocall,对应宏展开的部分如下:
// demo.cj.macrocall
/* ===== Emitted by MacroCall @Outer in demo.cj:3:1 ===== */
/* 3.1 */class Demo {
/* 3.2 */ var state = 1
/* 3.3 */ var cnt = 42
/* 3.4 */ public func getCnt() {
/* 3.5 */ state + cnt + 0
/* 3.6 */ }
/* 3.7 */}
/* 3.8 */
/* ===== End of the Emit ===== */
如果宏展开后的代码有语义错误,则编译器的错误信息会溯源到宏展开后代码的具体行列号。仓颉宏的 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(1+2) // M2 is in macro M, not suitable for debug mode. return input + quote($a) }
-
不支持带括号宏的调试。
// main.cj main() { // 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(1+2) 0 }