☰
仓颉 std.ast 宏上下文编程指南:assertParentContext 嵌套校验与 setItem/getChildMessages 消息通信
2026/10/14 3:08:26 网站建设 项目流程
  • 编程语言
  • 语言运行时
  • 标准库
  • 垃圾回收
  • 并发编程

【免费下载链接】cangjie_runtime

仓颉编程语言运行时与标准库。

项目地址:https://gitcode.com/Cangjie/cangjie_runtime
点击查看免费下载

仓颉(Cangjie)的宏系统支持在编译期对源码进行语法树级变换,而std.ast包提供的 "Macro With Context"(带上下文的宏)能力,让嵌套宏之间不仅能感知彼此的调用关系,还能在展开阶段直接交换数据。本文以 stdlib/doc/libs/std/ast/ast_samples/context.md 为骨架,结合std.ast的 API 文档与 ffi.cj 源码实现,完整讲解assertParentContext的嵌套校验、setItem与getChildMessages的消息传递机制,并给出可直接运行的编译示例与排错要点。

一、背景:什么是带上下文的宏(Macro With Context)

在嵌套宏场景下,一个宏调用(内层宏)可能出现在另一个宏调用(外层宏)的调用参数内部。std.ast为这类场景提供了专门的一组上下文函数,用于在宏展开时获取调用上下文信息,主要包括:

  • assertParentContext(String):断言当前宏调用嵌套在指定外层宏调用内,否则报编译错误;
  • insideParentContext(String):以布尔值形式返回嵌套关系检查结果;
  • setItem(key, value)(重载Bool/Int64/String/Tokens):内层宏向外层宏发送键值对消息;
  • getChildMessages(String):外层宏获取指定内层宏发送的全部消息。

这些函数有一个共同的使用限制(见 ast_package_overview.md):只能作为函数被直接调用,不能赋值给变量,不能作为实参或返回值使用。从实现上看,它们依赖宏展开器在展开过程中维护的"当前宏对象"(MACRO_OBJECT线程局部对象)来工作,因此脱离宏展开过程调用是没有意义的。

二、嵌套宏上下文校验:assertParentContext

2.1 完整示例与编译命令

宏定义文件macro_definition.cj:

// macro_definition.cj macro package macro_definition import std.ast.* public macro Outter(input: Tokens): Tokens { return input } public macro Inner(input: Tokens): Tokens { assertParentContext("Outter") return input }

宏调用文件macro_call.cj:

// macro_call.cj package macro_calling import macro_definition.* main() { @Outter var a = 0 @Inner var b = 0 // error: The macro call 'Inner' should with the surround code contains a call 'Outter'. }

编译命令:

# 先编译宏定义文件 cjc macro_definition.cj --compile-macro # 编译使用宏的文件 cjc macro_call.cj -o demo

Inner宏在定义时通过assertParentContext("Outter")声明"调用我时,我必须以@Outter宏调用为外围上下文"。由于示例调用中@Outter与@Inner是平级的两条语句、不存在嵌套关系,编译期会直接报出错误:

error: The macro call 'Inner' should with the surround code contains a call 'Outter'.

2.2 源码实现视角

assertParentContext的实现位于 stdlib/libs/std/ast/ffi.cj(第 282 行附近):

public func assertParentContext(parentMacroName: String): Unit { unsafe { if (let Some(macroObjPtr) <- MACRO_OBJECT.get()) { var pStr = LibC.mallocCString(parentMacroName) ... var flag = CJ_CheckParentContext(macroObjPtr, pStr, true) ... if (!flag) { throw MacroContextException("Parent macro context check failed for macro '${parentMacroName}'.") } } } }

可以看到,校验最终由运行时接口CJ_CheckParentContext完成:它会遍历宏调用上下文链,确认名为parentMacroName的外层宏调用确实包裹着当前宏。校验失败时抛出的异常类型是 MacroContextException,最终由编译器以标准错误形式呈现给用户。

2.3 需要条件判断时使用 insideParentContext

如果不想直接报错,而是想根据嵌套关系做分支处理,可以使用insideParentContext(String),它返回Bool(true表示当前宏嵌套在给定的外层宏调用内),实现同样位于 ffi.cj(第 304 行附近),其内部调用CJ_CheckParentContext(macroObjPtr, pStr, false),区别仅在于不抛出异常而是返回结果。典型用法:

public macro inner(input: Tokens): Tokens { if (insideParentContext("outer")) { // 处于 outer 宏内时的逻辑 } else { // 未处于 outer 宏内时的逻辑 } return input }

三、内层宏向外层宏发送消息:setItem

assertParentContext解决的是"嵌套关系校验",而setItem解决的是"嵌套宏之间的数据通信"。

3.1 完整示例

宏定义文件macro_definition.cj:

// macro_definition.cj macro package macro_definition import std.ast.* public macro Outter(input: Tokens): Tokens { let messages = getChildMessages("Inner") for (m in messages) { let value1 = m.getString("key1") // get value: "value1" let value2 = m.getString("key2") // get value: "value2" println("value1: ${value1} value2: ${value2}") } return input } public macro Inner(input: Tokens): Tokens { assertParentContext("Outter") setItem("key1", "value1") setItem("key2", "value2") return input }

宏调用文件macro_call.cj:

// macro_call.cj import macro_definition.* main() { @Outter( @Inner var cnt = 0 ) println(cnt) }

编译命令:

# 先编译宏定义文件 cjc macro_definition.cj --compile-macro # 编译使用宏的文件 cjc macro_call.cj -o demo

编译阶段输出:

value1: value1 value2: value2

在这个例子中:内层宏Inner在展开时通过setItem("key1", "value1")与setItem("key2", "value2")把两个键值对"暂存"到外层宏的上下文中;随后外层宏Outter展开时调用getChildMessages("Inner")拿到Inner发送的一组信息对象(每个调用Inner的位置对应一条消息),再通过信息对象的getString按 key 取出对应值。

3.2 setItem 的四种重载

setItem支持四种值类型,对应四个重载(见 ast_package_funcs.md):

重载发送类型检索接口
setItem(String, String)字符串getString(key)
setItem(String, Int64)整数getInt64(key)
setItem(String, Bool)布尔getBool(key)
setItem(String, Tokens)词法单元序列getTokens(key)

从源码看(ffi.cj 第 331~396 行),每种重载都会把值序列化后连同类型标签交给运行时接口CJ_SetItemInfo,类型标签用常量区分:

let ITEM_TYPE_STRING: UInt8 = 1 let ITEM_TYPE_INT64: UInt8 = 2 let ITEM_TYPE_BOOL: UInt8 = 3 let ITEM_TYPE_TOKENS: UInt8 = 4

例如发送Tokens时,实现会先调用value.toBytes()将词法单元序列化,再以字节指针形式传递;外层宏读取时再反序列化为Tokens。因此setItem不仅能传简单值,还能把一段代码(如quote(1 + 1))完整地传出去。

四、外层宏接收消息:getChildMessages 与 MacroMessage

4.1 接收语义

getChildMessages(children: String): ArrayList<MacroMessage>用于获取名为children的内层宏发送的全部消息。关键语义:

  • 一个内层宏调用 = 一条MacroMessage:如果Outter内调用了多次Inner,getChildMessages会返回与调用次数等长的消息列表;
  • 每条MacroMessage内部是一个键值对映射,用getString/getInt64/getBool/getTokens按 key 取值;
  • 没有消息不是错误:从实现看(ffi.cj 第 559 行附近),当没有对应消息时返回空列表ArrayList<MacroMessage>(),外层宏可直接遍历;
  • 若按 key 取不到对应类型的信息,getString等接口会抛出Exception(如Macro item '${key}' does not exist.,或类型不匹配时提示has a different type: expected 'String')。

4.2 MacroMessage 的完整接口

MacroMessage类(见 ast_package_classes.md)除上述四个 getter 外,还提供:

  • hasItem(key: String): Bool:判断该条消息中是否存在指定 key,可在取值前做防御性检查。

一个使用hasItem的典型防御写法(参考 ast_package_exceptions.md 中的示例):

public macro outer(input: Tokens) { let messages = getChildMessages("inner") if (messages.size > 0 && !messages[0].hasItem("key1")) { throw MacroContextException("No such item") } return input }

4.3 多值多类型综合示例

下面是在官方文档示例基础上整理的一个同时传递Int64与Bool的完整场景(宏定义与宏调用分别编译):

// 宏定义 macro package M import std.ast.* import std.collection.ArrayList public macro inner(input: Tokens) { setItem("Int64FromInner", 100) // 发送 Int64 setItem("TrueFromInner", true) // 发送 Bool return input } public macro outer(input: Tokens) { let messages = getChildMessages("inner") let msg = messages[0] println("Message from inner-macro: ${msg.getInt64("Int64FromInner")}") println("Bool message: ${msg.getBool("TrueFromInner")}") return input }
// 宏调用 import M.* @outer(@inner(var a = 1)) main() { }

编译运行后输出:

Message from inner-macro: 100 Bool message: true

五、编译与调试流程

上下文宏涉及两类编译过程:

  1. 编译宏定义:使用--compile-macro选项编译宏定义文件,例如cjc macro_definition.cj --compile-macro;
  2. 编译宏调用方:直接编译使用宏的文件并输出可执行程序,例如cjc macro_call.cj -o demo。

如需观察宏展开过程与中间结果,官方 API 文档中的示例还使用了--debug-macro选项(如cjc macro_call.cj --debug-macro),配合std.ast的语法树节点dump函数(示例见 ast_samples/dump.md)可以输出 AST 节点的完整结构,便于定位上下文宏代码中Tokens变换是否正确。

需要注意:宏展开后的代码不允许出现包声明和包导入语句(详见 parseProgram 的说明),因此消息传递、上下文校验等逻辑都应封装在宏函数内部完成,最终return input返回干净的可编译Tokens。

六、常见错误与排查要点

现象原因与处理
编译报错The macro call 'Inner' should with the surround code contains a call 'Outter'.Inner的assertParentContext("Outter")校验失败,说明调用处不存在@Outter包裹;检查调用代码的嵌套结构(参考 context.md 第一组示例)。
抛出Macro item 'key' does not exist.getString等取值接口按 key 找不到对应项;先调用hasItem(key)判断,或检查setItem的 key 是否拼写一致。
抛出Macro item 'key' has a different type: expected 'String'.setItem发送的类型与取值接口不匹配;setItem(String, Int64)发的值必须用getInt64取。
getChildMessages返回空列表内层宏未发送消息或内层宏名与参数不一致;文档明确"没有消息不是错误",可按空列表处理。
上下文函数无法在宏外使用assertParentContext等函数依赖宏展开上下文(MACRO_OBJECT),只能在宏展开过程中直接调用。

总结

通过assertParentContext/insideParentContext与setItem/getChildMessages的组合,仓颉宏作者可以在编译期实现"嵌套关系约束 + 父子宏数据交换"两类能力:前者在宏定义处声明依赖的外层宏,让错误在编译期提前暴露;后者以内层宏调用为粒度,把字符串、整数、布尔甚至整段Tokens代码安全地传递给外层宏处理。配合--compile-macro、--debug-macro与dump调试手段,即可构建结构清晰、约束严谨的嵌套宏体系。相关 API 的完整签名与更多示例可继续参阅 ast_package_overview.md、ast_package_funcs.md 与源码实现 stdlib/libs/std/ast/ffi.cj。

  • 编程语言
  • 语言运行时
  • 标准库
  • 垃圾回收
  • 并发编程

【免费下载链接】cangjie_runtime

仓颉编程语言运行时与标准库。

项目地址:https://gitcode.com/Cangjie/cangjie_runtime
点击查看免费下载

相关推荐

上一篇:Sinon.JS 完整指南:JavaScript 测试的 Spies、Stubs 与 Mocks 实战与源码解析
下一篇:Nx 23.1.0 Gradle 迁移指南:将 dev.nx.gradle.project-graph 升级到 0.1.23

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询