Compilation, Errors, and Debugging
Inspect one language lane at a time so line-level text and code deltas stay readable.
Diff Lane
English
0 modified sections0 code block delta0 anchor delta
Diff Lane
中文
3 modified sections0 code block delta0 anchor delta
modified宏的编译和使用text+2 lines, -2 lines
v1.0.5
Section Text
1
当前编译器约束宏的定义与宏的调用不允许在同一包里。宏包必须首先被编译,然后再编译宏调用的包。在宏调用的包中,不允许出现宏的定义。由于宏需在包中导出给另一个包使用,因此编译器约束宏定义必须使用 `public` 修饰。2
3
下面介绍一个简单的例子。4
5
源码目录结构如下:6
7
8
宏定义放在 _macros_ 子目录下:9
10
<!-- run -macro0 -->11
<!-- cfg="--compile-macro" -->12
13
14
宏调用放在 _src_ 子目录下:15
16
<!-- run -macro0 -->17
18
19
当宏定义文件的编译产物和使用宏的文件不在同一目录下时,需要通过添加 `--import-path` 编译选项来指定宏定义文件编译产物的路径。以下为 Linux 平台的编译命令(具体编译选项会随着 cjc 更新而演进,以最新 cjc 的编译选项为准):20
21
22
在 Linux 平台上,将生成用于包管理的 `macro_define.cjo` 和实际的动态库文件。23
24
若在 Windows 平台:25
26
27
如果宏包还依赖其他动态库,则需要保证宏包在运行态(宏展开依赖宏包内方法的执行)下能够找到这些依赖。在 Linux 下可以通过设置 `LD_LIBRAYR_PATH`(Windows 下设置 `PATH`)环境变量添加所依赖动态库的路径。28
29
> **说明:**30
>31
> 宏替换过程依赖仓颉 runtime ,宏替换过程中仓颉 runtime 的初始化配置采用宏提供的默认配置,配置参数支持使用仓颉 runtime 运维日志进行查询,其中 cjHeapSize 与 cjStackSize 支持用户修改,其余暂不支持,仓颉 runtime 初始化配置可参见[runtime 初始化可选配置](../Appendix/runtime_env.md#runtime初始化可选配置)章节。Code 1 · text
1
// Directory layout.2
root_path3
├── macros4
│ └── m.cj5
├── src6
│ └── demo.cj7
└─ targetCode 2 · cangjie
1
// macros/m.cj2
// In this file, we define the macro Inner, Outer.3
macro package define4
import std.ast.*5
6
public macro Inner(input: Tokens) {7
return input8
}9
10
public macro Outer(input: Tokens) {11
return input12
}13
Code 3 · cangjie
1
// src/demo.cj2
import define.*3
@Outer4
class Demo {5
@Inner var state = 16
@Inner var cnt = 427
}8
9
main() {10
println("test macro")11
}Code 4 · shell
1
# 先编译宏定义文件在指定目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)2
cjc macros/m.cj --compile-macro --output-dir ./target3
4
# 编译使用宏的文件,宏替换完成,产生可执行文件5
cjc src/demo.cj -o demo --import-path ./target --output-dir ./target6
7
# 运行可执行文件8
./target/demoCode 5 · shell
1
# 当前目录: src2
3
# 先编译宏定义文件在指定目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)4
cjc macros/m.cj --compile-macro --output-dir ./target5
6
# 编译使用宏的文件,宏替换完成,产生可执行文件7
cjc src/demo.cj -o demo.exe --import-path ./target --output-dir ./targetv1.1.0
Section Text
1
当前编译器约束宏的定义与宏的调用不允许在同一包里。宏包必须首先被编译,然后再编译宏调用的包。在宏调用的包中,不允许出现宏的定义。由于宏需在包中导出给另一个包使用,因此编译器约束宏定义必须使用 `public` 修饰。2
3
下面介绍一个简单的例子。4
5
源码目录结构如下:6
7
8
宏定义放在 _macros_ 子目录下:9
10
<!-- run -macro0 -->11
<!-- cfg="--compile-macro" -->12
13
14
宏调用放在 _src_ 子目录下:15
16
<!-- run -macro0 -->17
18
19
当宏定义文件的编译产物和使用宏的文件不在同一目录下时,需要通过添加 `--import-path` 编译选项来指定宏定义文件编译产物的路径。以下为 Linux 平台的编译命令(具体编译选项会随着 cjc 更新而演进,以最新 cjc 的编译选项为准):20
21
22
在 Linux 平台上,将生成用于包管理的 `macro_define.cjo` 和实际的动态库文件。23
24
若在 Windows 平台:25
26
27
如果宏包还依赖其他动态库,则需要保证宏包在运行态(宏展开依赖宏包内方法的执行)下能够找到这些依赖。在 Linux 下可以通过设置 `LD_LIBRARY_PATH`(Windows 下设置 `PATH`)环境变量添加所依赖动态库的路径。28
29
> **说明:**30
>31
> 宏替换过程依赖仓颉 runtime ,宏替换过程中仓颉 runtime 的初始化配置采用宏提供的默认配置,配置参数支持使用仓颉 runtime 运维日志进行查询,其中 cjHeapSize 与 cjStackSize 支持用户修改,其余暂不支持。注意所有配置参数在 OpenHarmonyOS 平台下均无效,OpenHarmonyOS 平台下仓颉运行时使用默认值。<!--Del-->仓颉 runtime 初始化配置可参见[runtime 初始化可选配置](../Appendix/runtime_env.md#runtime-初始化可选配置)章节。<!--DelEnd-->Code 1 · text
1
// Directory layout.2
root_path3
├── macros4
│ └── m.cj5
├── src6
│ └── demo.cj7
└─ targetCode 2 · cangjie
1
// macros/m.cj2
// In this file, we define the macro Inner, Outer.3
macro package define4
import std.ast.*5
6
public macro Inner(input: Tokens) {7
return input8
}9
10
public macro Outer(input: Tokens) {11
return input12
}13
Code 3 · cangjie
1
// src/demo.cj2
import define.*3
@Outer4
class Demo {5
@Inner var state = 16
@Inner var cnt = 427
}8
9
main() {10
println("test macro")11
}Code 4 · shell
1
# 先编译宏定义文件在指定目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)2
cjc macros/m.cj --compile-macro --output-dir ./target3
4
# 编译使用宏的文件,宏替换完成,产生可执行文件5
cjc src/demo.cj -o demo --import-path ./target --output-dir ./target6
7
# 运行可执行文件8
./target/demoCode 5 · shell
1
# 当前目录: src2
3
# 先编译宏定义文件在指定目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)4
cjc macros/m.cj --compile-macro --output-dir ./target5
6
# 编译使用宏的文件,宏替换完成,产生可执行文件7
cjc src/demo.cj -o demo.exe --import-path ./target --output-dir ./targetmodifieddiagReport 报错机制text+1 line
v1.0.5
Section Text
1
仓颉标准库 `std.ast` 包提供了自定义报错接口 `diagReport`。方便定义宏的用户,在解析传入 Tokens 时,对错误 Tokens 内容进行自定义报错。2
3
自定义报错接口提供同原生编译器报错一样的输出格式,允许用户报 warning 和 error 两类错误提示信息。4
5
`diagReport` 的函数原型如下:6
7
8
其参数含义如下:9
10
- level: 报错信息等级11
- tokens: 报错信息中所引用源码内容对应的 Tokens12
- message: 报错的主信息13
- hint: 辅助提示信息14
15
参考如下使用示例。16
17
宏定义文件:18
19
<!-- compile.error -macro2 -->20
<!-- cfg="--compile-macro" -->21
22
23
宏调用文件:24
25
<!-- compile.error -macro2 -->26
27
28
编译宏调用文件过程中,会出现如下报错信息:Code 1 · cangjie
1
public func diagReport(level: DiagReportLevel, tokens: Tokens, message: String, hint: String): UnitCode 2 · cangjie
1
// macro_definition.cj2
macro package macro_definition3
4
import std.ast.*5
6
public macro testDef(input: Tokens): Tokens {7
for (i in 0..input.size) {8
if (input[i].kind == IDENTIFIER) {9
diagReport(DiagReportLevel.ERROR, input[i..(i + 1)],10
"This expression is not allowed to contain identifier",11
"Here is the illegal identifier")12
}13
}14
return input15
}Code 3 · cangjie
1
// macro_call.cj2
package macro_calling3
4
import std.ast.*5
import macro_definition.*6
7
main(): Int64 {8
let a = @testDef(1)9
let b = @testDef(a)10
let c = @testDef(1 + a)11
return 012
}Code 4 · text
1
error: This expression is not allowed to contain identifier2
==> call.cj:9:22:3
|4
9 | let b = @testDef(a)5
| ^ Here is the illegal identifier6
|7
8
error: This expression is not allowed to contain identifier9
==> call.cj:10:26:10
|11
10 | let c = @testDef(1 + a)12
| ^ Here is the illegal identifier13
|14
15
2 errors generated, 2 errors printed.v1.1.0
Section Text
1
仓颉标准库 `std.ast` 包提供了自定义报错接口 `diagReport`。方便定义宏的用户,在解析传入 Tokens 时,对错误 Tokens 内容进行自定义报错。2
3
自定义报错接口提供同原生编译器报错一样的输出格式,允许用户报 warning 和 error 两类错误提示信息。4
5
`diagReport` 的函数原型如下:6
7
<!-- code_no_check -->8
9
10
其参数含义如下:11
12
- level: 报错信息等级13
- tokens: 报错信息中所引用源码内容对应的 Tokens14
- message: 报错的主信息15
- hint: 辅助提示信息16
17
参考如下使用示例。18
19
宏定义文件:20
21
<!-- compile.error -macro2 -->22
<!-- cfg="--compile-macro" -->23
24
25
宏调用文件:26
27
<!-- compile.error -macro2 -->28
29
30
编译宏调用文件过程中,会出现如下报错信息:Code 1 · cangjie
1
public func diagReport(level: DiagReportLevel, tokens: Tokens, message: String, hint: String): UnitCode 2 · cangjie
1
// macro_definition.cj2
macro package macro_definition3
4
import std.ast.*5
6
public macro testDef(input: Tokens): Tokens {7
for (i in 0..input.size) {8
if (input[i].kind == IDENTIFIER) {9
diagReport(DiagReportLevel.ERROR, input[i..(i + 1)],10
"This expression is not allowed to contain identifier",11
"Here is the illegal identifier")12
}13
}14
return input15
}Code 3 · cangjie
1
// macro_call.cj2
package macro_calling3
4
import std.ast.*5
import macro_definition.*6
7
main(): Int64 {8
let a = @testDef(1)9
let b = @testDef(a)10
let c = @testDef(1 + a)11
return 012
}Code 4 · text
1
error: This expression is not allowed to contain identifier2
==> call.cj:9:22:3
|4
9 | let b = @testDef(a)5
| ^ Here is the illegal identifier6
|7
8
error: This expression is not allowed to contain identifier9
==> call.cj:10:26:10
|11
10 | let c = @testDef(1 + a)12
| ^ Here is the illegal identifier13
|14
15
2 errors generated, 2 errors printed.modified使用 --debug-macro 输出宏展开结果text+2 lines, -23 lines
v1.0.5
Section Text
1
借助宏在编译期做代码生成时,如果发生错误,处理起来十分棘手,这是开发者经常遇到但一般很难定位的问题。这是因为,开发者写的源码,经过宏的变换后变成了不同的代码片段。编译器抛出的错误信息是基于宏最终生成的代码进行提示的,但这些代码在开发者的源码中没有体现。2
3
为了解决这个问题,仓颉宏提供 debug 模式,在这个模式下,开发者可以从编译器为宏生成的 debug 文件中看到完整的宏展开后的代码,如下所示。4
5
宏定义文件:6
7
<!-- compile -macro3 -->8
<!-- cfg="--compile-macro" -->9
10
11
宏调用文件 `demo.cj`:12
13
<!-- compile -macro3 -->14
<!-- cfg="--debug-macro" -->15
16
17
在编译使用宏的文件时,在选项中,增加 `--debug-macro`,即使用仓颉宏的 _debug_ 模式。18
19
20
> **注意:**21
>22
> 如果使用仓颉的 `CJPM` 项目管理工具进行编译,可在配置文件 `cjpm.toml` 中添加 `--debug-macro` 的编译选项来使用宏的 _debug_ 模式。23
>24
> ```text25
> compile-option = "--debug-macro"26
> ```27
28
在 _debug_ 模式下,会生成临时文件 _demo.cj.macrocall_,对应宏展开的部分如下:29
30
31
如果宏展开后的代码有语义错误,则编译器的错误信息会溯源到宏展开后代码的具体行列号。仓颉宏的 _debug_ 模式有以下注意事项:32
33
- 宏的 _debug_ 模式会重排源码的行列号信息,不适用于某些特殊的换行场景。例如:34
35
```cangjie36
// before expansion37
@M{} - 2 // macro M return 238
39
// after expansion40
// ===== Emmitted my Macro M at line 1 ===41
242
// ===== End of the Emit =====43
- 244
```45
46
这些因换行符导致语义改变的情形,不应使用 _debug_ 模式。47
48
- 不支持宏调用在宏定义内的调试,会编译报错。49
50
```cangjie51
public macro M(input: Tokens) {52
let a = @M2(1+2) // M2 is in macro M, not suitable for debug mode.53
return input + quote($a)54
}55
```56
57
- 不支持带括号宏的调试。58
59
```cangjie60
// main.cj61
62
main() {63
// For macro with parenthesis, newline introduced by debug will change the semantics64
// of the expression, so it is not suitable for debug mode.65
let t = @M(1+2)66
}67
```Code 1 · cangjie
1
macro package define2
3
import std.ast.*4
5
public macro Outer(input: Tokens): Tokens {6
let messages = getChildMessages("Inner")7
8
let getTotalFunc = quote(public func getCnt() {9
)10
for (m in messages) {11
let identName = m.getString("identifierName")12
getTotalFunc.append(Token(TokenKind.IDENTIFIER, identName))13
getTotalFunc.append(quote(+))14
}15
getTotalFunc.append(quote(0))16
getTotalFunc.append(quote(}))17
let funcDecl = parseDecl(getTotalFunc)18
19
let decl = (parseDecl(input) as ClassDecl).getOrThrow()20
decl.body.decls.add(funcDecl)21
return decl.toTokens()22
23
}24
25
public macro Inner(input: Tokens): Tokens {26
assertParentContext("Outer")27
let decl = parseDecl(input)28
setItem("identifierName", decl.identifier.value)29
return input30
}Code 2 · cangjie
1
import define.*2
3
@Outer4
class Demo {5
@Inner var state = 16
@Inner var cnt = 427
}8
9
main(): Int64 {10
let d = Demo()11
println("${d.getCnt()}")12
return 013
}14
Code 3 · shell
1
cjc --debug-macro demo.cj --import-path ./targetCode 4 · cangjie
1
// demo.cj.macrocall2
/* ===== Emitted by MacroCall @Outer in demo.cj:3:1 ===== */3
/* 3.1 */class Demo {4
/* 3.2 */ var state = 15
/* 3.3 */ var cnt = 426
/* 3.4 */ public func getCnt() {7
/* 3.5 */ state + cnt + 08
/* 3.6 */ }9
/* 3.7 */}10
/* 3.8 */11
/* ===== End of the Emit ===== */v1.1.0
Section Text
1
借助宏在编译期做代码生成时,如果发生错误,处理起来十分棘手,这是开发者经常遇到但一般很难定位的问题。这是因为,开发者写的源码,经过宏的变换后变成了不同的代码片段。编译器抛出的错误信息是基于宏最终生成的代码进行提示的,但这些代码在开发者的源码中没有体现。2
3
为了解决这个问题,仓颉宏提供 debug 模式,在这个模式下,开发者可以从编译器为宏生成的 debug 文件中看到完整的宏展开后的代码,如下所示。4
5
宏定义文件:6
7
<!-- compile -macro3 -->8
<!-- cfg="--compile-macro" -->9
10
11
宏调用文件 `demo.cj`:12
13
<!-- compile -macro3 -->14
<!-- cfg="--debug-macro" -->15
16
17
在编译使用宏的文件时,在选项中,增加 `--debug-macro`,即使用仓颉宏的 _debug_ 模式。18
19
20
> **注意:**21
>22
> 如果使用仓颉的 `CJPM` 项目管理工具进行编译,可在配置文件 `cjpm.toml` 中添加 `--debug-macro` 的编译选项来使用宏的 _debug_ 模式。23
>24
> ```text25
> compile-option = "--debug-macro"26
> ```27
28
在 _debug_ 模式下,会生成临时文件 _demo.cj.macrocall_,对应宏展开的部分如下:29
30
<!-- code_no_check -->31
32
33
如果宏展开后的代码有语义错误,则编译器的错误信息会溯源到宏展开后代码的具体行列号。Code 1 · cangjie
1
macro package define2
3
import std.ast.*4
5
public macro Outer(input: Tokens): Tokens {6
let messages = getChildMessages("Inner")7
8
let getTotalFunc = quote(public func getCnt() {9
)10
for (m in messages) {11
let identName = m.getString("identifierName")12
getTotalFunc.append(Token(TokenKind.IDENTIFIER, identName))13
getTotalFunc.append(quote(+))14
}15
getTotalFunc.append(quote(0))16
getTotalFunc.append(quote(}))17
let funcDecl = parseDecl(getTotalFunc)18
19
let decl = (parseDecl(input) as ClassDecl).getOrThrow()20
decl.body.decls.add(funcDecl)21
return decl.toTokens()22
23
}24
25
public macro Inner(input: Tokens): Tokens {26
assertParentContext("Outer")27
let decl = parseDecl(input)28
setItem("identifierName", decl.identifier.value)29
return input30
}Code 2 · cangjie
1
import define.*2
3
@Outer4
class Demo {5
@Inner var state = 16
@Inner var cnt = 427
}8
9
main(): Int64 {10
let d = Demo()11
println("${d.getCnt()}")12
return 013
}14
Code 3 · shell
1
cjc --debug-macro demo.cj --import-path ./targetCode 4 · cangjie
1
// demo.cj.macrocall2
/* ===== Emitted by MacroCall @Outer in demo.cj:3:1 ===== */3
/* 3.1 */class Demo {4
/* 3.2 */ var state = 15
/* 3.3 */ var cnt = 426
/* 3.4 */ public func getCnt() {7
/* 3.5 */ state + cnt + 08
/* 3.6 */ }9
/* 3.7 */}10
/* 3.8 */11
/* ===== End of the Emit ===== */