☰
CJASTHelper是如何把AST还原成源码的?ToCangjiePass实现原理深度剖析
2026/10/9 16:45:15 网站建设 项目流程

CJASTHelper是如何把AST还原成源码的?ToCangjiePass实现原理深度剖析

【免费下载链接】CJASTHelperCangjie Abstract Syntax Tree Helper项目地址: https://gitcode.com/Cangjie-SIG/CJASTHelper

🌟CJASTHelper是基于 Cangjie 开源编译器前端的AST 操作与源码转换工具,其中ToCangjiePass负责把解析、语义分析后的AST(抽象语法树)还原成可读的 Cangjie 源码。本文将带你完整看懂它的实现原理:访客模式如何遍历 AST、100 多种节点如何逐一打印、以及编译器“解糖”后的代码如何被逆向还原成你熟悉的语法。

为什么需要“AST 还原源码”?

Cangjie 编译器前端处理源码时,会经历多个阶段:

parse → (desugared-parse) → macro → sema → desugared-sema

每个阶段的 AST 形态都不同(详见 doc/usage.md):

  • 语法糖被展开(如x[i]变成x.[](i)调用)
  • 属性访问变成 getter/setter 调用
  • 构造函数调用变成init()调用

CJASTHelper 的--dump-source选项能在任意阶段输出当时的源码,方便你观察编译器内部发生了什么。核心引擎就是本文主角——ToCangjiePass 源码还原 Pass。

整体架构:Pass 插件 + 访客模式

CJASTHelper 采用插件化 Pass 管线(架构总览见 doc/design.md)。to-cangjie这个 Pass 以动态库cjast_to_source_pass的形式加载,注册关系定义在 config/passes.json 中:

{ "group": "to-source", "names": ["to-cangjie", "to-java"], "lib": "cjast_to_source_pass", "dependencies": ["desugar"] }

类继承结构非常清晰:

类职责位置
PassPass 基类,定义Run(AstNode&)入口Pass.h
ToSourcePass持有输出文件流 +Printer打印器 + 访客ToSourcePass.h
ToCangjiePass真正实现全部节点打印逻辑ToCangjiePass.h
ConstAstVisitor按节点类型分发 Before/Visit/After 回调ConstAstVisitor.h

ToSourcePass::Run()就两件事:创建输出目录,然后Traverse(node, visitor)开始遍历(ToSourcePass.h)。

核心机制一:100+ 节点处理器的批量注册

Cangjie 的 AST 有上百种节点(声明、类型、模式、表达式……)。ToCangjiePass 通过宏EXPAND4批量注册每个节点的Visit处理器,例如 ToCangjiePass.cpp:

  • VarDecl、FuncDecl、ClassDecl等声明节点
  • RefType、OptionType、FuncType等类型节点
  • IfExpr、MatchExpr、LambdaExpr等表达式节点

每个Visit函数的套路高度一致:打印关键字 → 递归打印子节点 → 补充前缀/后缀。比如IfExpr的还原(ToCangjiePass.cpp):

  1. 打印if,再打印条件表达式并包裹()
  2. 打印{,缩进后递归打印 then 块,再打印}
  3. 如果有 else 分支,打印else { ... }

配合Printer的Indent/Unindent能力,输出的源码天然带正确缩进。

核心机制二:Before 钩子拦截“解糖节点”

真正精彩的还原逻辑藏在Before钩子里。对 9 种会被解糖的节点(MainDecl、AssignExpr、UnaryExpr、BinaryExpr、CallExpr、RefExpr、SubscriptExpr、OptionType、MacroDecl,见 ToCangjiePass.h),Pass 提供了Before重写——它在默认遍历之前执行,有机会“劫持”打印过程。

以赋值表达式为例(ToCangjiePass.cpp):

  • 节点没有desugarExpr(未解糖)→ 返回Cont(),走正常 Visit 打印
  • 有desugarExpr且开启解糖模式 → 直接打印解糖结果,返回Skip()
  • 有desugarExpr但要求还原源码 → 识别出x.[](i, y)这种“成员函数调用”结构,反向还原为x[i] = v

VisitResult::Skip()是关键:它告诉遍历器“这个节点我已处理完,别再走默认逻辑了”。这就是逆向解糖的实现骨架。

核心机制三:语义级还原(sema 阶段专属)

语义分析后的 AST 丢失了大量原始语法信息,还原难度最大。ToCangjiePass 用三个“尝试”函数链解决(ToCangjiePass.cpp):

还原场景解糖/语义后形态还原为实现函数
构造函数调用init(args)调用A(args)TryPrintInitCall
属性读写x.$aget()/x.$aset(v)x.a/x.a = vTryRecoverPropCallExpr
运算符重载a.+(b)/a.[](x)a + b/a[x]TryRecoverOverloadCallExpr

其中运算符还原还会贴心地保留一条/* Desugared ... */注释,记录解糖后的原始形态,方便对照。

更复杂的for-in循环还原则展示了“模板匹配”功力:编译器把for (x in start..stop:step)解糖成var $iter-i = ...+while循环的四条语句块,Pass 断言块结构后重新拼装回for-in形态(ToCangjiePass.cpp)。

还有个精巧的辅助设施——解糖变量名字表desugaredVarId:编译器生成的$iter-compiler这类临时变量在还原时会被规范化(去$、-转_、重名加序号),并记录“原节点 → 新名字”的映射,后续所有引用处通过查表统一替换(ToCangjiePass.cpp)。

配置驱动:Desugar 与 Sema 双开关

所有还原行为由 PassConfig 的两个标志位控制:

  • Desugar 模式:打印“解糖后”的源码(看编译器内部形态)
  • Sema 模式:基于语义信息补齐类型标注、泛型实参、丢失的修饰符/注解

--dump-source=parse与--dump-source=desugared-sema的区别,本质就是这两个开关的组合。当 sema 阶段节点丢失了显式类型时,Pass 会用语义类型Ty兜底补全(ToCangjiePass.cpp)。

上手体验:一条命令还原源码

构建完成后(Linux/macOS 用bash build.sh -t Release -b),最常用的命令:

mkdir -p out ./build/bin/cjah --dump-source=desugared-sema --output-type=dylib main.cj --output-dir out # 产物:out/main_source.cj

还可以叠加--filter-decls=func,class只还原顶层函数和类。完整选项见 doc/usage.md。

总结

ToCangjiePass 的实现可以浓缩为三层设计:

  1. 遍历层:ConstAstVisitor按AstKind分发,宏批量注册 100+ 节点处理器
  2. 打印层:Printer负责缩进与前缀/后缀包裹,让还原结果天然可读
  3. 还原层:Before钩子 +Skip拦截 + 模板匹配,把解糖/语义化后的节点逆向还原为原始语法

这种“访客 + 钩子 + 模式匹配”的组合,是编译器工具中 AST→源码 反序列化问题的经典解法,对想实现类似 unparser 的同学很有参考价值。

📚 相关源码入口:src/passes/to_source/(还原 Pass 全实现)、src/passes/desugar/(解糖 Pass)、test/test_ci.cpp(管线回归测试)。

【免费下载链接】CJASTHelperCangjie Abstract Syntax Tree Helper项目地址: https://gitcode.com/Cangjie-SIG/CJASTHelper

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

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

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

立即咨询