☰
仓颉 std.ast 操作指南:对 AST 节点进行增、删、改、查
2026/10/14 5:21:29 网站建设 项目流程
  • 编程语言
  • 语言运行时
  • 标准库
  • 垃圾回收
  • 并发编程

【免费下载链接】cangjie_runtime

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

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

std.ast是仓颉标准库中负责语法解析与语法树建模的包,它把仓颉源码的词法单元(Tokens)解析为抽象语法树(AST)节点对象。本指南围绕官方示例《操作 AST 对象示例》展开,讲解如何获取ClassDecl节点后对其执行增(新增成员函数)、删、改、查等结构化操作,并结合仓库源码说明quote、parseDecl、类型转换与Body.decls容器的工作机制。读完本文,你将能够在仓颉宏或源码分析工具中自由读写 AST 节点。

AST 节点模型:从 Decl 到 ClassDecl

在std.ast中,所有语法树节点都继承自Node(见 ast 包类总览)。声明类节点统一继承自public open class Decl <: Node(见 Decl 定义),类定义、接口定义、函数定义、变量定义、枚举定义、宏定义等都属于Decl节点。本文操作的ClassDecl(类定义节点)正是Decl的一个子类:

  • ClassDecl通过public mut prop body: Body暴露类体,通过public mut prop superTypes: ArrayList<TypeNode>、upperBound等属性描述继承关系(见 ClassDecl 定义);
  • Body表示类、结构体、接口等由{}和内部声明节点组成的结构,其核心成员是public mut prop decls: ArrayList<Decl>,即类体内的声明节点集合(见 Body 定义)。

从源码看,ClassDecl.body实际返回其私有字段classbody_,类型为Body;而Body.decls返回ArrayList<Decl>(对应源码 decls.cj)。这正是本文示例能够对类体做"增"操作的根本原因:decls本身是可变的ArrayList,可以直接add/remove。

操作示例:向类节点新增成员函数

官方示例《操作 AST 对象示例》的完整代码如下(注意:代码是仓库中经过<!-- verify -->标记验证过的可运行示例):

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() } var identifier = classDecl.identifier // 为该节点增加一个成员函数用于获取a的值 var funcdecl = FuncDecl(quote(func getValue() {a})) classDecl.body.decls.add(funcdecl) println("Identifier value is ${identifier.value}") println("ClassDecl body size is ${classDecl.body.decls.size}") 0 }

运行结果:

Identifier value is Data ClassDecl body size is 3

第一步:用 quote 构造 Tokens

quote(...)是仓颉编译器提供的内建语法,用于把一段仓颉源码文本直接转换为Tokens类型(词法单元序列)。它支持插值操作:插值对象只要实现了ToTokens接口(func toTokens(): Tokens)即可被嵌入,Token、Tokens、各整数类型、Float64、Bool、Rune、String等类型均已实现该接口(见 totokens.cj)。因此:

let input: Tokens = quote( class Data { var a: Int32 init(a_: Int32) { a = a_ } } )

等价于先对这段源码做词法分析得到Tokens。除了quote,也可以使用cangjieLex(code: String)函数把字符串转为Tokens,该函数底层通过 FFI 调用编译器词法器CJ_AST_Lex,并在失败时抛出IllegalArgumentException(见 ffi.cj)。

第二步:parseDecl 解析为 Decl 节点

parseDecl(input)将Tokens解析为一个Decl声明节点(见 parseDecl 定义)。其源码签名还带有一个命名参数astKind!: String = "",用于指定特殊声明类型(如"PrimaryCtorDecl"、"PropMemberDecl"),默认解析为普通Decl(见 parse_decl.cj)。

从实现上看,parseDecl通过 FFI 调用编译器侧的CJ_AST_ParseDecl,将结果以 flatbuffers 格式回传,再由generateDeclAST构造成仓颉侧 AST 对象;任何解析失败都会抛出ParseASTException("Failed to parse decl.")(见 parse_decl.cj)。示例中input顶层是一个类定义,因此解析结果是ClassDecl类型。

第三步:as 类型转换与安全取值

parseDecl的返回类型是父类型Decl,而示例需要访问ClassDecl特有的body、identifier等成员,因此先做类型转换:

var classDecl = match (decl as ClassDecl) { case Some(v) => v case None => throw Exception() }

decl as ClassDecl是仓颉的类型检查表达式,转换成功返回Some(v),失败返回None。示例通过match分支处理None情形,保证在decl不是ClassDecl时不会盲目访问成员。随后:

var identifier = classDecl.identifier

identifier是声明节点的标识符词法单元(Token),其value字段保存标识符文本。Decl基类内部以protected var identifier_: Token保存该值(见 Decl 定义)。

第四步:新增成员函数——改 AST 的关键一步

"增"操作的核心是两步:先用quote构造出一个新的FuncDecl节点,再把它加入body.decls容器:

var funcdecl = FuncDecl(quote(func getValue() {a})) classDecl.body.decls.add(funcdecl)

FuncDecl(inputs: Tokens)是FuncDecl的构造方式之一:传入一组Tokens,内部解析并构造函数定义节点。FuncDecl拥有block(函数体)、funcParams(参数列表)、declType(返回类型)等可变属性(见 FuncDecl 定义),因此不仅可以整体添加,也可以先FuncDecl()构造空节点再逐项设置属性。

classDecl.body.decls是ArrayList<Decl>,add后类体声明数从 2 变为 3。这也说明对 AST 的"增"本质上是操作普通的集合容器——这正是std.ast将 AST 节点设计为可读写(mut prop)对象带来的灵活性。

结果验证

Identifier value is Data ClassDecl body size is 3

body.size为 3 对应类体中三个声明:var a: Int32(VarDecl)、init(...)(FuncDecl)、新加入的getValue()(FuncDecl)。若在示例末尾调用classDecl.dump(),还可以看到包含getValue节点的完整树形结构(dump用法见 语法树节点打印示例)。

删:移除 AST 子节点

由于Body.decls是ArrayList<Decl>,"删"同样基于集合操作。例如移除示例中新增的getValue:

classDecl.body.decls.removeAt(classDecl.body.decls.size - 1) // 或按需删除:classDecl.body.decls.remove(funcdecl)

对于其它容器型属性(如ClassDecl.superTypes、FuncDecl.funcParams),同样可以add/remove来增删父类声明与函数参数。需要注意:直接操作节点属性时,std.ast会对部分词法单元属性做类型校验,例如给Body.lBrace设置非{的Token会抛出ASTException(见 Body 定义),源码中通过checkTokenType(v, LCURL)实现(见 decls.cj)。

改:修改节点属性

所有关键属性均为mut prop,可直接赋值完成"改"。例如:

// 修改类名标识符 classDecl.identifier = Token(TokenKind.IDENTIFIER, "NewData") // 修改函数体:为 getValue 换一个表达式块 funcdecl.block = Block(quote(return a))

Block(inputs: Tokens)、Token(kind, value)等构造方式与FuncDecl(inputs)一致,均可从Tokens重建子节点。修改完成后同样可以用dump()或toTokens()把节点重新序列化为词法单元,验证改动是否符合预期。

查:dump 与遍历

除了直接读取identifier.value这类属性,"查"还有两个常用手段:

  • dump():每个 AST 节点都实现了dump,可打印节点整体树形结构,便于实时查看节点层级与属性(见 语法树节点打印示例);
  • traverse(visitor):通过继承Visitor并重写visit函数,可自定义对某类节点的访问行为,例如收集所有未初始化变量(见 自定义访问函数遍历 AST 示例)。

从源码看,ClassDecl.traverse会先访问自身,再依次遍历通用子节点、父类型、泛型约束与类体(见 decls.cj),因此"查"既可以精确到单个属性,也可以全量遍历。

进一步阅读

  • 解析示例:parseDecl / parseProgram:获取Decl、Program节点的完整流程;
  • ast 包函数总览:parseDecl、parseDeclFragment、parseExpr、parseProgram、cangjieLex、diagReport等解析与报错接口;
  • ast 包类总览:Node、Decl、ClassDecl、FuncDecl、Body、Tokens的完整属性与方法;
  • 自定义报错接口示例:在解析Tokens时输出与编译器一致的WARNING/ERROR提示;
  • Macro With Context 示例:在嵌套宏场景下用assertParentContext、setItem、getChildMessages传递上下文信息。

结合本文的增删改查操作,配合上述解析、遍历与报错能力,即可在仓颉宏展开阶段对源码 AST 进行完整的结构化改写。

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

【免费下载链接】cangjie_runtime

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

项目地址:https://gitcode.com/Cangjie/cangjie_runtime
点击查看免费下载
上一篇:DiSky 项目使用教程
下一篇:CodeSnap.nvim 使用教程

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

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

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

立即咨询