仓颉编程语言:语法节点

仓颉语言AST解析与操作

在仓颉语言的编译过程中,首先通过词法分析将代码转换成 Tokens,然后对 Tokens 进行语法解析,得到一个语法树。每个语法树的节点可能是一个表达式、声明、类型、模式等。仓颉 ast 库提供了每种节点对应的类,它们之间具有适当的继承关系。其中,主要的抽象类如下:

  • Node:所有语法节点的父类
  • TypeNode:所有类型节点的父类
  • Expr:所有表达式节点的父类
  • Decl:所有声明节点的父类
  • Pattern:所有模式节点的父类

具体节点的类型众多,具体细节请参考 《仓颉编程语言库 API》文档。在下面的案例中,我们主要使用以下节点:

  • BinaryExpr:二元运算表达式
  • FuncDecl:函数的声明

节点的解析

通过 ast 库,基本上每种节点都可以从 Tokens 解析。有两种调用解析的方法。

使用解析表达式和声明的函数。

以下函数用于从 Tokens 解析任意的表达式或任意的声明:

  • parseExpr(input: Tokens): Expr:将输入的 Tokens 解析为表达式
  • parseExprFragment(input: Tokens, startFrom!: Int64 = 0): (Expr, Int64):将输入 Tokens 的一个片段解析为表达式,片段从 startFrom 索引开始,解析可能只消耗从索引 startFrom 开始的片段的一部分,并返回第一个未被消耗的 Token 的索引(如果消耗了整个片段,返回值为 input.size)
  • parseDecl(input: Tokens, astKind!: String = ""):将输入的 Tokens 解析为声明,astKind 为额外的设置,具体请见《仓颉编程语言库 API》文档。
  • parseDeclFragment(input: Tokens, startFrom!: Int64 = 0): (Decl, Int64):将输入 Tokens 的一个片段解析为声明,startFrom 参数和返回索引的含义和 parseExpr 相同。

我们通过代码案例展示这些函数的使用:

let tks1 = quote(a + b)
let tks2 = quote(a + b, x + y)
let tks3 = quote(
    func f1(x: Int64) { return x + 1 }
)
let tks4 = quote(
    func f1(x: Int64) { return x + 1 }
    func f2(x: Int64) { return x + 2 }
)

let binExpr1 = parseExpr(tks1)
println("binExpr1 is BinaryExpr: ${binExpr1 is BinaryExpr}")
let (binExpr2, mid) = parseExprFragment(tks2)
let (binExpr3, end) = parseExprFragment(tks2, startFrom: mid + 1) // 跳过逗号
println("size = ${tks2.size}, mid = ${mid}, end = ${end}")
let funcDecl1 = parseDecl(tks3)
println("funcDecl1 is FuncDecl: ${funcDecl1 is FuncDecl}")
let (funcDecl2, mid2) = parseDeclFragment(tks4)
let (funcDecl3, end2) = parseDeclFragment(tks4, startFrom: mid2)
println("size = ${tks4.size}, mid = ${mid2}, end = ${end2}")

输出结果是:

binExpr1 is BinaryExpr: true
size = 7, mid = 3, end = 7
funcDecl1 is FuncDecl: true
size = 29, mid = 15, end = 29

使用构造函数进行解析

大多数节点类型都支持 init(input: Tokens) 构造函数,将输入的 Tokens 解析为相应类型的节点,例如:

let binExpr = BinaryExpr(quote(a + b))
let funcDecl = FuncDecl(quote(func f1(x: Int64) { return x + 1 }))

如果解析失败将抛出异常。这种解析方式适用于类型已知的代码片段,解析后不需要再手动转换成具体的子类型。

节点的组成部分

从 Tokens 解析出节点之后,我们可以查看节点的组成部分。作为例子,我们列出 BinaryExpr 和 FuncDecl 的组成部分,关于其他节点的更详细的解释请见《仓颉编程语言库 API》文档。

  • BinaryExpr 节点:
    • leftExpr: Expr:运算符左侧的表达式
    • op: Token:运算符
    • rightExpr: Expr:运算符右侧的表达式
  • FuncDecl 节点(部分):
    • identifier: Token:函数名
    • funcParams: ArrayList<FuncParam>:参数列表
    • declType: TypeNode:返回值类型
    • block: Block:函数体
  • FuncParam节点(部分):
    • identifier: Token:参数名
    • paramType: TypeNode:参数类型
  • Block节点(部分):
    • nodes: ArrayList<Node>:块中的表达式和声明

每个组成部分都是 public mut prop,因此可以被查看和更新。我们通过一些例子展示更新的结果。

BinaryExpr 案例

let binExpr = BinaryExpr(quote(x * y))
binExpr.leftExpr = BinaryExpr(quote(a + b))
println(binExpr.toTokens())

binExpr.op = Token(TokenKind.ADD)
println(binExpr.toTokens())

输出结果是:

(a + b) * y
a + b + y

首先,通过解析,获得 binExpr 为节点 x * y,图示如下:

    *
  /   \
 x     y

第二步,我们将左侧的节点(即 x)替换为 a + b,因此,获得的语法树如下:

      *
    /   \
   +     y
  / \
 a   b

当输出这个语法树的时候,我们必须在 a + b 周围添加括号,得到 (a + b) * y(如果输出a + b * y,含义为先做乘法,再做加法,与语法树的含义不同)。**ast 库具备在输出语法树时自动添加括号的功能**。

第三步,我们将语法树根部的运算符从 * 替换为 +,因此得到语法树如下:

      +
    /   \
   +     y
  / \
 a   b

这个语法树可以输出为 a + b + y,因为加法本身就是左结合的,不需要在左侧添加括号。

FuncDecl 案例

let funcDecl = FuncDecl(quote(func f1(x: Int64) { x + 1 }))
funcDecl.identifier = Token(TokenKind.IDENTIFIER, "foo")
println("Number of parameters: ${funcDecl.funcParams.size}")
funcDecl.funcParams[0].identifier = Token(TokenKind.IDENTIFIER, "a")
println("Number of nodes in body: ${funcDecl.block.nodes.size}")
let binExpr = (funcDecl.block.nodes[0] as BinaryExpr).getOrThrow()
binExpr.leftExpr = parseExpr(quote(a))
println(funcDecl.toTokens())

在这个案例中,我们首先通过解析构造出了一个 FuncDecl 节点,然后分别修改了该节点的函数名、参数名,以及函数体中表达式的一部分。输出结果是:

Number of parameters: 1
Number of nodes in body: 1
func foo(a: Int64) {
    a + 1
}

使用 quote 插值语法节点

任何 AST 节点都可以在 quote 语句中插值,部分 AST 节点的 ArrayList 列表也可以被插值(主要对应实际情况中会出现这类节点列表的情况)。插值直接通过 $(node) 表达即可,其中 node 是任意节点类型的实例。

下面,我们通过一些案例展示节点的插值。

var binExpr = BinaryExpr(quote(1 + 2))
let a = quote($(binExpr))
let b = quote($binExpr)
let c = quote($(binExpr.leftExpr))
let d = quote($binExpr.leftExpr)
println("a: ${a.toTokens()}")
println("b: ${b.toTokens()}")
println("c: ${c.toTokens()}")
println("d: ${d.toTokens()}")

输出结果是:

a: 1 + 2
b: 1 + 2
c: 1
d: 1 + 2.leftExpr

一般来说,插值运算符后面的表达式使用小括号限定作用域,例如 $(binExpr)。但是当后面只跟单个标识符的时候,小括号可省略,即可写为 $binExpr。因此,在案例中 a 和 b 都在 quote 中插入了 binExpr节点,结果为 1 + 2。然而,如果插值运算符后面的表达式更复杂,不加小括号可能造成作用域出错。例如,表达式 binExpr.leftExpr 求值为 1 + 2 的左表达式,即 1,因此 c 正确赋值为 1。但 d 中的插值被解释为 ($binExpr).leftExpr,因此结果是 1 + 2.leftExpr。为了明确插值的作用域,我们推荐在插值运算符中使用小括号。

下面的案例展示节点列表(ArrayList)的插值。

var incrs = ArrayList<Node>()
for (i in 1..=5) {
    incrs.append(parseExpr(quote(x += $(i))))
}
var foo = quote(
    func foo(n: Int64) {
        let x = n
        $(incrs)
        x
    })
println(foo)

输出结果是:

func foo(n: Int64) {
    let x = n
    x += 1
    x += 2
    x += 3
    x += 4
    x += 5
    x
}

在这个案例中,我们创建了一个节点列表 incrs,包含表达式 x += 1,...,x += 5。对 incrs 的插值将节点依次列出,在每个节点后换行。这适用于插入需要依次执行的表达式和声明的情况。

下面的案例展示在某些情况下,需要在插值周围添加括号,以保证正确性。

var binExpr1 = BinaryExpr(quote(x + y))
var binExpr2 = BinaryExpr(quote($(binExpr1) * z))       // 错误:得到 x + y * z
println("binExpr2: ${binExpr2.toTokens()}")
println("binExpr2.leftExpr: ${binExpr2.leftExpr.toTokens()}")
println("binExpr2.rightExpr: ${binExpr2.rightExpr.toTokens()}")
var binExpr3 = BinaryExpr(quote(($(binExpr1)) * z))     // 正确:得到 (x + y) * z
println("binExpr3: ${binExpr3.toTokens()}")

输出结果是:

binExpr2: x + y * z
binExpr2.leftExpr: x
binExpr2.rightExpr: y * z
binExpr3: (x + y) * z

首先,我们构建了表达式 x + y,然后将该表达式插入到模版 $(binExpr1) * z 中。这里的意图是得到一个先计算 x + y,然后再乘 z 的表达式,但是,插值的结果是 x + y * z,先做 y * z,然后再加 x。这是因为插值不会自动添加括号以保证被插入的表达式的原子性(这和前一阶介绍的 leftExpr 的替换不同)。因此,需要在 $(binExpr1) 周围添加小括号,保证得到正确的结果。

<think> 根据用户需求,用户希望了解如何使用仓颉编程语言实现将正规表达式转换为NFA(非确定有限自动机)的方法。参考引用[1]中提供了一段仓颉代码片段,展示了NFA的数据结构表示。我们将以此为基础,详细解释实现步骤。 由于正规表达式转NFA通常涉及编译原理中的算法,我们将按照以下步骤进行: 1. 理解正规表达式的语法和运算规则(连接、选择、闭包等) 2. 设计NFA的数据结构(参考引用中的结构) 3. 实现正规表达式的解析(通常需要词法分析语法分析) 4. 根据解析结果递归构建NFA(使用Thompson算法等) 引用[1]中已经展示了仓颉语言中NFA的数据结构: - 定义了一个结构体`FAnode`,其中包含一个静态数组(长度为27),每个元素是一个动态数组(`ArrayList<Int64>`)。这个结构表示NFA的一个状态节点,数组的每个位置可能对应一个输入字符(例如26个字母加上ε转换),动态数组存储从当前状态通过该字符能到达的下一个状态集合。 - 使用`ArrayList<FAnode>`来存储整个NFA的状态列表。 下面我们详细说明实现过程: ### 1. NFA数据结构设计 根据引用[1],我们使用以下数据结构: ```仓颉 import std.collection.* struct FAnode { // 使用长度为27的数组,索引0-25对应字母a-z,索引26对应ε转换 var transitions = Array(27, {_ => ArrayList<Int64>()}) } var nfa = ArrayList<FAnode>() // 存储所有状态 var stateCount: Int64 = 0 // 状态数量 ``` ### 2. 正规表达式解析 需要将输入的正规表达式字符串解析成抽象语法树(AST)。这里我们假设正规表达式由以下元素组成: - 单个字符(如a, b, c) - 连接(如ab) - 选择(如a|b) - 闭包(如a*) - 括号(用于分组,如(a|b)*c) 解析过程涉及词法分析语法分析,这里我们使用递归下降法。 ### 3. 构建NFA(Thompson算法) Thompson算法的核心是为每个子表达式构建NFA片段,然后根据运算符组合这些片段。下面给出各基本操作的构建方法: #### 单个字符(例如:a) ```仓颉 // 创建两个新状态 func createState() -> Int64 { nfa.append(FAnode{}) return stateCount++ } func symbolNFA(symbol: Char) -> (start: Int64, end: Int64) { let start = createState() let end = createState() let index = symbol - 'a' // 假设符号是小写字母,否则需要处理 nfa[start].transitions[index].add(end) return (start, end) } ``` #### 连接(例如:ab) ```仓颉 func concatNFA(first: (start: Int64, end: Int64), second: (start: Int64, end: Int64)) -> (start: Int64, end: Int64) { // 将第一个的结束状态通过ε转换连接到第二个的开始状态 nfa[first.end].transitions[26].add(second.start) // 索引26代表ε return (first.start, second.end) } ``` #### 选择(例如:a|b) ```仓颉 func unionNFA(nfa1: (start: Int64, end: Int64), nfa2: (start: Int64, end: Int64)) -> (start: Int64, end: Int64) { let start = createState() let end = createState() // 从新开始状态ε跳转到两个分支的开始 nfa[start].transitions[26].add(nfa1.start) nfa[start].transitions[26].add(nfa2.start) // 从两个分支的结束状态ε跳转到新结束状态 nfa[nfa1.end].transitions[26].add(end) nfa[nfa2.end].transitions[26].add(end) return (start, end) } ``` #### 闭包(例如:a*) ```仓颉 func starNFA(nfaExpr: (start: Int64, end: Int64)) -> (start: Int64, end: Int64) { let start = createState() let end = createState() // 从新开始状态ε跳转到原NFA的开始状态和结束状态(0次或多次) nfa[start].transitions[26].add(nfaExpr.start) nfa[start].transitions[26].add(end) // 从原结束状态ε跳转到原开始状态(循环)和结束状态 nfa[nfaExpr.end].transitions[26].add(nfaExpr.start) nfa[nfaExpr.end].transitions[26].add(end) return (start, end) } ``` ### 4. 完整流程 1. 将正规表达式字符串解析为抽象语法树(AST)。 2. 递归遍历AST,对每个节点应用上述构建函数,自底向上构建整个NFA。 3. 返回最终NFA的开始状态和结束状态。 ### 示例:正规表达式 `(a|b)*c` 构建步骤: 1. 构建`a`的NFA:`nfa_a = symbolNFA('a')` 2. 构建`b`的NFA:`nfa_b = symbolNFA('b')` 3. 构建`a|b`:`nfa_union = unionNFA(nfa_a, nfa_b)` 4. 构建`(a|b)*`:`nfa_star = starNFA(nfa_union)` 5. 构建`c`:`nfa_c = symbolNFA('c')` 6. 连接:`nfa_final = concatNFA(nfa_star, nfa_c)` 最终得到的`nfa_final.start`为整个NFA的起始状态,`nfa_final.end`为接受状态。 ### 注意事项 1. 需要处理ε转换(索引26)。 2. 状态编号从0开始,通过`createState`函数递增。 3. 该实现假设输入字符为小写字母,若包含其他字符需扩展数组大小并调整索引计算。 以上即为使用仓颉编程语言实现正规表达式转NFA的核心方法。具体实现时还需考虑错误处理、内存管理以及性能优化等问题。 §§相关问题§§ 1. 仓颉编程语言中如何处理正规表达式中的转义字符? 2. 如何将NFA转换为DFA? 3. 在构建NFA时如何避免状态爆炸? 4. 仓颉语言在实现编译原理算法时有哪些优势? </think>### 仓颉编程语言实现正规表达式转NFA的方法 在编译原理中,将正规表达式转换为非确定有限自动机(NFA)是词法分析的关键步骤。以下是基于仓颉编程语言的实现方法(参考引用中数据结构): #### 一、核心数据结构设计 ```仓颉 import std.collection.* // 定义NFA状态节点:27个转换表(a-z + ε) struct FAnode { var transitions = Array(27, {_ => ArrayList<Int64>()}) } // NFA整体结构 var nfa = ArrayList<FAnode>() // 存储所有状态 var stateCount: Int64 = 0 // 状态计数器 ``` - **索引说明**:`transitions[0-25]`对应字母a-z,`transitions[26]`对应ε转换 - **动态数组**:`ArrayList<Int64>`存储可到达的状态ID #### 二、关键算法实现 1. **基础符号构建**(如字符a) ```仓颉 func buildSymbolNFA(symbol: Char) -> (start: Int64, end: Int64) { let start = stateCount++ nfa.append(FAnode{}) let end = stateCount++ nfa.append(FAnode{}) let idx = symbol.toInt() - 'a'.toInt() // 计算字母索引 nfa[start].transitions[idx].add(end) return (start, end) } ``` 2. **并运算**(如a|b) ```仓颉 func unionNFA(nfa1: (start: Int64, end: Int64), nfa2: (start: Int64, end: Int64)) -> (start: Int64, end: Int64) { let newStart = stateCount++ nfa.append(FAnode{}) let newEnd = stateCount++ nfa.append(FAnode{}) // 添加ε转换 nfa[newStart].transitions[26].add(nfa1.start) nfa[newStart].transitions[26].add(nfa2.start) nfa[nfa1.end].transitions[26].add(newEnd) nfa[nfa2.end].transitions[26].add(newEnd) return (newStart, newEnd) } ``` 3. **闭包运算**(如a*) ```仓颉 func closureNFA(subNFA: (start: Int64, end: Int64)) -> (start: Int64, end: Int64) { let newStart = stateCount++ nfa.append(FAnode{}) let newEnd = stateCount++ nfa.append(FAnode{}) // 构建循环结构 nfa[newStart].transitions[26].add(subNFA.start) nfa[newStart].transitions[26].add(newEnd) nfa[subNFA.end].transitions[26].add(subNFA.start) nfa[subNFA.end].transitions[26].add(newEnd) return (newStart, newEnd) } ``` #### 三、完整工作流程 1. **输入处理**:解析正规表达式(如 `(a|b)*c`) 2. **递归构建**: ```仓颉 let nfaA = buildSymbolNFA('a') let nfaB = buildSymbolNFA('b') let nfaUnion = unionNFA(nfaA, nfaB) let nfaClosure = closureNFA(nfaUnion) let nfaC = buildSymbolNFA('c') let finalNFA = concatNFA(nfaClosure, nfaC) // 连接操作 ``` 3. **输出NFA**:获得起始状态`finalNFA.start`和接受状态`finalNFA.end` #### 四、注意事项 1. **ε转换处理**:索引26专门用于空转移 2. **状态管理**:`stateCount`确保状态ID唯一性 3. **内存优化**:动态数组(`ArrayList`)避免预先分配过大空间 4. **扩展性**:可通过增加`transitions`数组长度支持更多符号 > 此方法采用Thompson构造算法,时间复杂度为$O(n)$(n为正规表达式长度),生成的NFA具有最小ε转移量[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值