CJASTHelper访问者核心设计:ConstAstVisitor与MutAstVisitor如何实现灵活的AST遍历
【免费下载链接】CJASTHelperCangjie Abstract Syntax Tree Helper项目地址: https://gitcode.com/Cangjie-SIG/CJASTHelper
CJASTHelper 是一款基于 Cangjie 编译器前端的 AST(抽象语法树)操作与源码转换工具,其访问者(Visitor)框架通过 ConstAstVisitor 与 MutAstVisitor 两个核心类,把"只读分析"和"可修改遍历"两种需求抽象为统一、可扩展的遍历管线。这篇文章带你快速看懂这套访问者核心设计:三阶段回调、跳过子树机制、自底向上的结果合并,以及它们在解糖(desugar)与源码还原 Pass 中的实际应用 🎯
为什么要用访问者模式做 AST 遍历
CJASTHelper 的主流程由 AstHelper 组织:先复用前端完成解析,拿到 AST 树,再由 PassManager 对每个包运行配置的 Pass 链(见 doc/design.md)。而每个 Pass 要做的,本质上都是沿着 AST 树逐节点处理——打印节点、检查解糖标记、替换子树……
访问者模式的价值在于:
- 遍历逻辑与处理逻辑分离:树怎么走(递归、跳过、回溯)由框架统一负责,Pass 只关心"遇到某类节点时做什么";
- 按节点种类精准注册:Cangjie 前端定义了成百上千种
AstKind(声明、表达式、类型、模式……),如果为每种节点都写一个虚函数,子类将被迫实现海量空方法。CJASTHelper 改为只注册真正关心的节点种类,未注册的种类自动走默认递归逻辑; - 读写分离更安全:只读分析(如打印源码)用 const 引用遍历,天然不可误改树结构;需要修改树的场景(如解糖替换)才使用可变版本,并额外提供结果合并通道。
三大核心角色:VisitorBase、ConstAstVisitor 与 MutAstVisitor
整个访问者框架只有 3 个头文件 + 3 个实现文件,结构非常清晰:
| 角色 | 文件 | 职责 |
|---|---|---|
ConstAstVisitorBase/MutAstVisitorBase | VisitorBase.h | 抽象接口:定义 Before/Visit/After 三阶段虚函数 |
Traverse/MutTraverse | VisitorBase.cpp | 递归遍历入口,驱动三阶段回调 |
ConstAstVisitor | ConstAstVisitor.h | 只读访问者:按节点种类注册回调 |
MutAstVisitor | MutAstVisitor.h | 可变访问者:多一个 Merge 结果合并回调 |
三阶段回调:BeforeVisit → Visit → AfterVisit
每个节点都会被"拜访"三次(见 Traverse 实现):
- BeforeVisit:进入节点前调用,可决定"继续"还是"跳过";
- Visit:节点被访问时调用,执行实际处理逻辑;
- AfterVisit:访问结束后调用,适合做收尾或回溯。
这个"进入—处理—离开"的结构,让 Pass 既能处理"展开前"的状态,也能处理"子树遍历完之后"的状态,覆盖前序/中序/后序各种典型需求。
VisitResult:一行控制"跳过子树"
遍历控制权交给用户的关键是 VisitResult 结构体:BeforeVisit返回VisitResult::Cont()表示继续,返回VisitResult::Skip()则整棵子树被剪掉,直接进入下一个节点。这在"已处理过某段子树、不想重复下钻"的场景里非常实用,也避免了不必要的递归开销。
ConstAstVisitor:只读遍历如何做到极简
ConstAstVisitor 面向"只看不改"的场景,核心设计是注册表 + 默认递归:
- 它内部持有一个
CallbackManager<AstKind, tuple<BeforeFunc, VisitFunc, AfterFunc>>(定义见 CallbackManger.h),把"节点种类 → 回调函数"映射成一张表; - 通过 RegBefore / RegVisit / RegAfter 三个方法,只为关心的
AstKind注册回调,比如 ToCangjiePass 只为MainDecl、AssignExpr等少数解糖节点注册了 Before 处理(见 ToCangjiePass.cpp 中的注册宏); - 未注册的种类走默认逻辑:DefaultVisit 会通过
AstNodeHelper::GetChildren取出所有子节点并递归Traverse,保证整棵树不会漏。
这个设计带来一个很直观的效果:Pass 作者只需写"我要处理的节点",遍历骨架由框架免费赠送。源码还原 Pass(to-cangjie / to-java)正是靠它把整棵(解糖后的)AST 重新打印成源码文件,而注册表机制让上百种节点类型中只需覆盖少数几种特例。
MutAstVisitor:可修改遍历 + 自底向上的结果合并
当需要修改 AST时(比如解糖节点替换回原始写法),只读访问者就不够了。MutAstVisitor 在 Const 版基础上做了三个关键增强:
- 非 const 引用传递节点:三阶段回调都接收
AstNode&,回调内部可以直接改写节点甚至替换子树; - ValuedResult:带数据的遍历结果:ValuedResult 继承自 VisitResult,额外携带一个可携带
int(计数)或OwnedPtr<AstNode>(替换用的新节点)的数据槽,让子节点的结论能自底向上汇总给父节点; - MergeResult 第四阶段回调:DefaultVisit 在遍历完所有子节点后,收集每个子节点的
ValuedResult,再调用 MergeResult 把子结果合并进父结果——这就是"后序处理"的标准化入口。
CounterAstVisitor 与 ReplaceAstVisitor:两种现成的合并策略
MutAstVisitor.h 里还附赠了两个常用的派生类,分别实现 CounterAstVisitor::DefaultMergeResult 和 ReplaceAstVisitor::DefaultMergeResult:
- CounterAstVisitor(计数型):把各子节点返回的整数值累加到父节点——适合"统计问题节点数量"这类需求;
- ReplaceAstVisitor(替换型):把子节点返回的新节点(
OwnedNodeValue)按顺序替换掉当前节点的子树——子节点没有返回新节点的位置保留原样。这是"整树替换"的基石。
也就是说,派生一个 Pass 时,你往往只需注册几个 Visit 回调,合并策略直接继承现成实现即可。
实战:DesugarPass 如何组合这些能力
解糖 Pass(DesugarPass.cpp)是理解这套设计最好的例子,它用同一个MutAstVisitor派生出三种模式(配置见 config/passes.json):
| Pass | 合并策略 | 做法 |
|---|---|---|
check-desugar | 计数型 | Visit 中发现节点带解糖标记就res.Set(1),自底向上累加,最终得到"解糖节点总数"(实现) |
replace-desugar | 替换型 | Visit 中把解糖后的子节点std::move进结果,再由 ReplaceAstVisitor 的合并逻辑整树换回(实现) |
recover-desugar | 直接改写 | Visit 中重置节点的解糖字段,恢复原始状态(实现) |
三种模式共享 RegVisitor 里对OPTION_TYPE、TRAIL_CLOSURE_EXPR的注册回调——注册一次,三处复用,正是注册表机制灵活性的体现。而 to-source Pass 则走另一条路线:用ConstAstVisitor纯只读地遍历并输出源码(ToCangjiePass.h)。
新手上手清单
✅读文档:先通读 doc/design.md 的"模块划分"一节,确认 visitor 模块在core中的位置。
✅精读三个文件:VisitorBase.h(接口与结果类型)→ ConstAstVisitor.h(只读注册表)→ MutAstVisitor.h(可变 + 合并),约 400 行读完整个框架。
✅选对访问者:只做分析/打印 →ConstAstVisitor;要改树 →MutAstVisitor(配合 Counter/Replace 两个现成子类)。
✅少写多注册:优先用RegVisit只覆盖特例节点,让默认递归帮你走完整棵树,别重复实现遍历骨架。
这套"三阶段回调 + 种类注册表 + 结果合并"的访问者设计,让 CJASTHelper 的每个 Pass 都能以极低的样板代码接入统一遍历管线——理解它,也就掌握了本项目最核心的扩展方式 🚀
【免费下载链接】CJASTHelperCangjie Abstract Syntax Tree Helper项目地址: https://gitcode.com/Cangjie-SIG/CJASTHelper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考