资讯动态

仓颉 std.ast 解析 API 实战:将仓颉源码解析为 AST 对象

发布时间:2026/10/15 5:55:47 来源:尧图企业网站定制
编程语言语言运行时标准库垃圾回收并发编程【免费下载链接】cangjie_runtime仓颉编程语言运行时与标准库。项目地址https://gitcode.com/Cangjie/cangjie_runtime点击查看免费下载std.ast是仓颉语言标准库中负责语法解析与语法树建模的核心包它将仓颉源码的词法单元Tokens解析为抽象语法树AST节点对象是编写编译期宏macro时操作源码结构的基础设施。本文以官方文档示例为主线完整演示parseDecl与parseProgram两个核心解析函数的用法与输出并延伸到parseExpr、parsePattern、parseType等兄弟函数以及Fragment系列接口结合开源仓库源码讲解其底层实现原理帮助读者快速掌握从源码文本到 AST 节点的完整链路。解析 API 全景从词法单元到 AST 节点在仓颉宏编程中编译器会把源码片段交给宏函数处理宏函数拿到的是一系列Token组成的Tokens。std.ast包的职责就是把这些词法单元重新组合成语义明确的语法树节点。整个包的核心是位于 stdlib/libs/std/ast 目录下的一组解析函数其完整清单如下函数返回值类型说明parseDecl(Tokens, astKind!)Decl解析一个声明节点parseDeclFragment(Tokens, startFrom!)(Decl, Int64)解析声明节点并返回续解析位置parseExpr(Tokens)Expr解析一个表达式节点parseExprFragment(Tokens, startFrom!)(Expr, Int64)解析表达式节点并返回续解析位置parsePattern(Tokens)Pattern解析一个模式pattern节点parsePatternFragment(Tokens, startFrom!)(Pattern, Int64)解析模式节点并返回续解析位置parseType(Tokens)TypeNode解析一个类型节点parseTypeFragment(Tokens, startFrom!)(TypeNode, Int64)解析类型节点并返回续解析位置parseProgram(Tokens)Program解析单个仓颉源文件的完整内容cangjieLex(String)/cangjieLex(String, Bool)Tokens将源码字符串做词法分析得到Tokens其中Fragment系列函数返回一个元组第一个元素是解析得到的节点第二个元素Int64是继续解析的下标用于从一段Tokens中间位置开始解析、并知道下一次该从哪里继续非常适合逐段扫描完整源文件的场景。从源码结构看这些解析函数共享同一套底层框架。以 parse_expr.cj 中的parseExpr为例public func parseExpr(input: Tokens): Expr { let node unsafe { try { parse(input, { p: CPointerUInt8 return CJ_AST_ParseExpr(match (MACRO_OBJECT.get()) { case Some(ptr) ptr case None CPointerUnit() }, p, CPointerInt64()) }) } catch (e: Exception) { throw ParseASTException(Failed to parse expr.) } } var astNode generateExprAST(node.GetRootAsExpr()) return astNode }可以看到解析分两步完成调用原生解析器通过 ffi.cj 中声明的foreign接口CJ_AST_ParseExpr、CJ_AST_ParseDecl、CJ_AST_ParseTopLevel等调用 C 侧仓颉编译器前端cjc frontend的Parser对序列化后的 token 字节流做真正的语法分析反序列化为 AST 对象原生解析结果以 FlatBuffers 序列化格式返回generateExprAST、generateDeclAST等函数将其还原为std.ast包中的各类节点对象如 parse_node.cj 中的generateNode会根据astKind字段分发到对应的构造逻辑。在 C 侧native/ast_api.cpp 的ParseWithError函数展示了底层解析的真实过程将 token 重新格式化为源码字符串创建DiagnosticEngine与SourceManager然后按ParseKindEXPR、DECL、PROGRAM、PATTERN调用Parser的不同入口ParseExprLibast、ParseDecl、ParseTopLevel、ParsePattern。解析失败时ParseASTException会被抛出并携带错误信息——这也是所有parse*函数统一的行为约定。输入前提Tokens 从哪里来所有parse*函数的输入都是Tokens而非字符串。获取Tokens有两种途径quote(...)编译器内建语法把写在括号里的仓颉代码片段在编译期直接转换为Tokens对象宏函数中通常用它书写待解析的源码模板cangjieLex(code)运行时函数将普通字符串做词法分析得到Tokens。它最终调用 C 侧CJ_AST_Lex见 ffi.cj默认会去掉末尾的END终止符若传入truncated: false则保留。在宏展开阶段MACRO_OBJECT一个线程局部变量见 ffi.cj持有当前宏调用的上下文指针解析时会被传递给原生接口用于定位源码位置、生成带位置信息的Token。这就是为什么dump()输出的每个Token都带有pos: 行号: 列号。示例一用 parseDecl 将类声明解析为 Decl 对象这是文档中的第一个完整示例目标是解析如下类Dataclass Data { var a: Int32 init(a_: Int32) { a a_ } }示例程序使用quote把类源码包成Tokens交给parseDecl得到Decl节点再做一次类型转换拿到具体的ClassDecl最后调用dump()打印节点结构import std.ast.* main() { let input: Tokens quote( class Data { var a: Int32 init(a_: Int32) { a a_ } } ) let decl parseDecl(input) // 获取一个 Decl 声明节点 var classDecl match (decl as ClassDecl) { // decl 的具体类型为 ClassDecl, 将其进行类型转化。 case Some(v) v case None throw Exception() } classDecl.dump() }运行结果节点树结构ClassDecl { -keyword: Token { value: class kind: CLASS pos: 5: 9 } -identifier: Token { value: Data kind: IDENTIFIER pos: 5: 15 } -body: Body { -decls: 0, VarDecl { -keyword: Token { value: var kind: VAR pos: 6: 13 } -identifier: Token { value: a kind: IDENTIFIER pos: 6: 17 } -declType: PrimitiveType { -keyword: Token { value: Int32 kind: INT32 pos: 6: 20 } } } -decls: 1, FuncDecl { -keyword: Token { value: init kind: INIT pos: 7: 13 } -identifier: Token { value: init kind: IDENTIFIER pos: 7: 13 } -funcParams: 0, FuncParam { -identifier: Token { value: a_ kind: IDENTIFIER pos: 7: 18 } -colon: Token { value: : kind: COLON pos: 7: 20 } -paramType: PrimitiveType { -keyword: Token { value: Int32 kind: INT32 pos: 7: 22 } } } -block: Block { -nodes: 0, AssignExpr { -leftExpr: RefExpr { -identifier: Token { value: a kind: IDENTIFIER pos: 8: 17 } } -assign: Token { value: kind: ASSIGN pos: 8: 19 } -rightExpr: RefExpr { -identifier: Token { value: a_ kind: IDENTIFIER pos: 8: 21 } } } } } } }输出解读节点、Token 与位置信息这份输出清晰地展示了 AST 的三层信息节点层次ClassDecl包含BodyBody的decls列表中依次是VarDecl成员变量a和FuncDecl构造器initFuncDecl又包含FuncParam与BlockBlock内的语句是AssignExpr。这种层次关系完全对应仓颉语法结构Token 细节每个Token打印value词面值与kind词法种类如CLASS、IDENTIFIER、VAR、INIT、INT32、COLON、ASSIGN位置信息pos: 行: 列给出该 token 在quote代码片段中的确切位置此处quote内的代码从第 5 行开始与示例注释中当前行号为 5的假设一致。从源码看这种树状打印格式由各节点类的dump实现产生见 nodes.cj 中Node.dump()的定义缩进与字段名排版则由 common.cj 中的getTokenIndent、dumpNodes等辅助函数统一处理。dump的用途正如 dump.md 所强调节点类型多、属性各异靠dump可以实时查看完整结构而无需反复查手册。parseDecl 的 astKind 参数解析特殊声明parseDecl的完整签名见 ast_package_funcs.md为public func parseDecl(input: Tokens, astKind!: String ): DeclastKind!用于指定待解析节点的特殊类型目前仅支持两个取值PrimaryCtorDecl解析主构造器。例如Point(var x: Int32, var y: Int32) {}这类没有func关键字的声明不指定astKind会因缺少关键字而解析失败PropMemberDecl解析prop声明的 getter 与 setter 函数get() { ... }、set(v) { ... }解析结果类型为FuncDecl。对应实现位于 parse_decl.cj它根据astKind分发到不同的原生入口CJ_AST_ParsePrimaryConstructor、CJ_AST_ParsePropMemberDecl或默认的CJ_AST_ParseDecl。官方文档还给出了两个配套示例。第一个解析prop的 getter/setterimport std.ast.* main() { let getter quote( get() { _val } ) let setter quote( set(v) { _val v }) let getterDecl parseDecl(getter, astKind: PropMemberDecl) let setterDecl parseDecl(setter, astKind: PropMemberDecl) println((getterDecl as FuncDecl).getOrThrow().block.toTokens()) println((setterDecl as FuncDecl).getOrThrow().block.toTokens()) }输出{ _val } { _val v }第二个解析主构造器import std.ast.* main() { let ctor quote( Point(var x: Int32, var y: Int32) {} ) let ctorDecl parseDecl(ctor, astKind: PrimaryCtorDecl) println(ctorDecl is PrimaryCtorDecl) println(ctorDecl.toTokens()) }输出true Point(var x: Int32, var y: Int32) { }限制说明parseDecl不支持解析FuncParam类型parseDeclFragment除了FuncParam外还不支持PropDecl与PrimaryCtorDecl。这些约束在函数注释中有明确说明使用前需要留意。可验证的节点种类parseDecl能产出的声明节点种类可在 parse_node.cj 的DECL_LIST中看到包括main_decl、func_decl、macro_decl、class_decl、interface_decl、enum_decl、struct_decl、type_alias_decl、primary_ctor_decl、var_decl、prop_decl、extend_decl、var_with_pattern_decl、macro_expand_decl等而generateDeclASTparse_decl.cj则根据序列化数据中的astKind字符串把每个声明还原为对应的节点对象未知种类会抛出ParseASTException(Unsupported decl node kind: ...)。示例二用 parseProgram 将完整源文件解析为 Program 对象第二个示例处理的是整个源文件场景。待解析的仓颉文件test_macro_define.cj内容如下/** * 此 cj 文件作为输入参数用于测试 parseProgram() **/ macro package m internal import std.ast.* internal import base as mybase public macro M(input: Tokens) { return input }示例程序先用std.fs的File与std.io的StringReader读取文件内容为字符串再经cangjieLex得到Tokens最后交给parsePrograminternal import std.ast.* internal import std.fs.* internal import std.io.* main() { let path: String ./test_macro_define.cj let file: File File(path, Read) let reader: StringReaderFile StringReader(file) let code: String reader.readToEnd() // 读取源码文件内容为字符串 let tokens: Tokens cangjieLex(code) let fileNode parseProgram(tokens) // 解析为 Program 节点 fileNode.dump() return 0 }运行结果Program { -packageHeader: PackageHeader { -keywordM: Token { value: macro kind: MACRO } -keywordP: Token { value: package kind: PACKAGE } -packageIdentifier: Token { value: m kind: IDENTIFIER } } -importLists: 0, ImportList { -modifier: Token { value: internal kind: INTERNAL } -keywordI: Token { value: import kind: IMPORT } -importKind: All prefixPath: 0: Token { value: std kind: IDENTIFIER } prefixPath: 1: Token { value: ast kind: IDENTIFIER } -identifier: Token { value: * kind: MUL } } -importLists: 1, ImportList { -modifier: Token { value: internal kind: INTERNAL } -keywordI: Token { value: import kind: IMPORT } -importKind: Alias -identifier: Token { value: base kind: IDENTIFIER } -importAlias 0: Token { value: as kind: AS } -importAlias 1: Token { value: mybase kind: IDENTIFIER } } -decls: 0, MacroDecl { -modifiers: 0, Modifier { -keyword: Token { value: public kind: PUBLIC } } -keyword: Token { value: macro kind: MACRO } -identifier: Token { value: M kind: IDENTIFIER } -funcParams: 0, FuncParam { -identifier: Token { value: input kind: IDENTIFIER } -colon: Token { value: : kind: COLON } -paramType: RefType { -identifier: Token { value: Tokens kind: IDENTIFIER } } } -block: Block { -nodes: 0, ReturnExpr { -keyword: Token { value: return kind: RETURN } -expr: RefExpr { -identifier: Token { value: input kind: IDENTIFIER } } } } } }Program 节点的内部结构Program节点对应仓颉源文件的整体结构从输出可以看出它包含四个组成部分packageHeader包声明头本例为macro package m因此keywordM是MACRO关键字、keywordP是PACKAGE、packageIdentifier是包名mimportLists导入列表本例有两条——internal import std.ast.*全量导入importKind: All与internal import base as mybase别名导入importKind: AliasimportAlias记录了as与mybasedecls顶层声明列表本例是public macro M(input: Tokens)解析为MacroDecl其修饰符public记录在Modifier中参数input: Tokens解析为带RefType参数类型的FuncParam函数体return input解析为ReturnExpr文件中没有features指令因此输出中未出现FeaturesDirective字段。从实现看parse_node.cj 中的createProgram依次构建FeaturesDirective、PackageHeader、ImportList列表与Decl列表最后组装为Program(pkgHeader, imports, decls, ftrDirective)parseProgram本身则调用原生入口CJ_AST_ParseTopLevel对应 C 侧Parser::ParseTopLevel()。使用 parseProgram 的重要注意点官方 API 文档对parseProgram有一条关键提示宏展开产生的仓颉代码不允许包含包声明与导入语句。如果输入源码包含package或import解析得到的Program节点中同样会携带packageHeader与importLists信息正如上面的输出所示因此该节点不能直接在宏函数中作为Tokens返回否则展开后的代码会带上非法的包声明/导入。这一点在 ast_package_funcs.md 的parseProgram条目中有明确说明。节点打印工具dump 的定位与输出规则两个示例都使用dump()观察解析结果这里补充几点实用规则dump()是Node基类提供的方法nodes.cj所有 AST 节点均可调用无需记忆各节点特有的打印接口输出的缩进以两个空格为基本单位NUM_SPACES 2见 common.cj列表字段以字段名: 下标形式标注每个Token打印value、kind、pos三项位置无效如编译期合成、无源码对应的 token时不打印pos空 tokenILLEGAL则整条省略这是getTokenIndent的判断逻辑common.cj决定的。对于交互式调试把dump()换成toTokens()Node实现的ToTokens接口可以反向把节点还原成Tokens再配合Tokens.toString()输出源码文本形成源码 → 节点 → 源码的闭环。其余 parse 系列函数速览除parseDecl与parseProgram外同族函数在宏编程中同样常用parseExpr(Tokens): Expr解析表达式如a b、foo(x)、{ ... }代码块等。可产出的表达式种类在 parse_node.cj 的EXPR_LIST中有完整列举call_expr、assign_expr、binary_expr、lambda_expr、match_expr、if_expr、macro_expand_expr等 40 余种parsePattern(Tokens): Pattern解析模式匹配中的模式如match表达式里的case分支模式可产出var_pattern、const_pattern、tuple_pattern、enum_pattern、type_pattern、wildcard_pattern等见 parse_node.cjparseType(Tokens): TypeNode解析类型节点如Int32、ArrayString、函数类型等可产出ref_type、primitive_type、func_type、tuple_type、option_type等见 parse_node.cj各函数的Fragment变体parseExprFragment、parsePatternFragment、parseTypeFragment、parseDeclFragment额外接收startFrom!: Int64 0起始下标返回(节点, Int64)续解析下标适合在长Tokens中分段连续解析——底层通过tokenCounter回传本次消耗的 token 数见 ffi.cj。这些函数在解析失败时统一抛出ParseASTException错误信息来自原生诊断引擎输入内存分配失败时可能抛出IllegalMemoryException接口契约与异常语义的完整定义可参考 ast_package_exceptions.md 与 ast_package_funcs.md。典型应用场景与实战建议std.ast的解析 API 主要服务于两类场景宏展开中的源码分析宏函数收到Tokens后先用parseDecl/parseExpr等还原出结构化的 AST再遍历、改写节点最后用toTokens()输出新的源码。由于宏展开时MACRO_OBJECT上下文可用解析出的Token会携带真实源码位置配合diagReport还能向编译过程输出带位置的诊断信息WARNING / ERROR整文件级处理当需要分析或转换一个完整的.cj文件时用std.fs读取文件、cangjieLex词法分析、parseProgram得到Program节点packageHeader/importLists/decls三个维度都结构化可用。实战中建议注意三点理解输入前提parse*的输入是Tokens而非字符串字符串需先经cangjieLex或直接使用quote构造留意类型判断parseDecl返回的Decl是抽象基类实际节点类型可能是ClassDecl、FuncDecl、MacroDecl等通常需要match (decl as XxxDecl)做类型收窄后再操作字段示例一即演示了这种模式遵守输出约束parseProgram的结果若含包声明/导入不能直接作为宏展开结果返回parseDecl/parseDeclFragment对FuncParam、PropDecl、PrimaryCtorDecl等类型存在解析限制使用前对照 API 文档确认。通过本文的两个完整示例与源码级剖析读者已经掌握了从源码文本 →Tokens→ AST 节点 → 结构打印的完整解析链路可以在此基础上继续学习 operate.md节点操作、traverse.md节点遍历与 context.md宏上下文通信进一步构建完整的仓颉宏编程能力。赞分享编程语言语言运行时标准库垃圾回收并发编程【免费下载链接】cangjie_runtime仓颉编程语言运行时与标准库。项目地址https://gitcode.com/Cangjie/cangjie_runtime点击查看免费下载相关推荐仓颉源码 AST 解析实战使用 std.ast 将仓颉代码解析为语法树对象仓颉源码 AST 解析实战使用 std.ast 将仓颉代码解析为语法树对象 本指南以仓颉标准库 std.ast 包为核心讲解如何将一段仓颉源码解析为抽象语法文档知识库教程OpenHarmony仓颉 std.ast 解析实战将源码 Tokens 解析为 AST 节点并打印语法树结构仓颉 std.ast 解析实战将源码 Tokens 解析为 AST 节点并打印语法树结构 在仓颉Cangjie语言的标准库中 std.ast 包提供了完人工智能AI 技能文档开发工具仓颉 std.ast 操作指南对 AST 对象进行增删改查的完整实战仓颉 std.ast 操作指南对 AST 对象进行增删改查的完整实战 本文以仓颉标准库 std.ast 包中操作 AST 对象的官方示例 stdlib/编程语言语言运行时标准库垃圾回收并发编程上一篇从 Koalas 迁移到 pandas API on SparkPySpark 迁移指南与 API 变化详解下一篇Vito快速上手指南10分钟搭建你的第一个生产环境创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑