- 编程语言
- 语言运行时
- 标准库
- 垃圾回收
- 并发编程
【免费下载链接】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: CPointer<UInt8> => return CJ_AST_ParseExpr(match (MACRO_OBJECT.get()) { case Some(ptr) => ptr case None => CPointer<Unit>() }, p, CPointer<Int64>()) }) } 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,然后按ParseKind(EXPR、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 对象
这是文档中的第一个完整示例,目标是解析如下类Data:
class 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包含Body,Body的decls列表中依次是VarDecl(成员变量a)和FuncDecl(构造器init),FuncDecl又包含FuncParam与Block,Block内的语句是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/setter:
import 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等;而generateDeclAST(parse_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,最后交给parseProgram:
internal 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: StringReader<File> = 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是包名m;importLists:导入列表,本例有两条——internal import std.ast.*(全量导入,importKind: All)与internal import base as mybase(别名导入,importKind: Alias,importAlias记录了as与mybase);decls:顶层声明列表,本例是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,空 token(ILLEGAL)则整条省略,这是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.cj);parseType(Tokens): TypeNode:解析类型节点,如Int32、Array<String>、函数类型等;可产出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
仓颉编程语言运行时与标准库。
相关推荐
仓颉源码 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/
编程语言语言运行时标准库垃圾回收并发编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考