TreeSitterKit

本项目旨在简化在 Swift 中使用 tree-sitter 解析编程语言和数据格式的过程。具体而言,它允许将语法规则编写为 Swift 代码,并自动将解析树转换为实现抽象语法的 Swift 数据结构。

动机

考虑一种用于简单算术表达式语言的抽象语法。

indirect enum Expr {
  case num(Int)
  case add(Expr, Expr)
  case mult(Expr, Expr)
}

我们将首先展示直接使用 tree-sitter 解析此语言所需的步骤,然后了解如何通过使用此包来简化此过程。

Tree-sitter 基础知识

tree-sitter 语法通常编写为 javascript 对象。

module.exports = grammar({
  name: 'ExprLang',
  rules: {
    Expr: $ => choice(
      $.Num,
      $.Add,
      $.Mult,
      $.Paren
    ),
    Num: $ => [0-9]+,
    Add: $ => prec(left(1, seq($.Expr, '+', $.Expr))),
    Mult: $ => prec(left(2, seq($.Expr, '*', $.Expr))),
    Paren: $ => seq('(', $.Expr, ')'),
  }
})

tree-sitter 程序用于生成解析器配置,形式为一对 C 源文件,即 *parser.c* 和 *parser.h*。

% tree-sitter generate grammar.js

生成的文件以及声明生成的 *TSLanguage* 结构的头文件必须包含在你的项目中。

#include <tree_sitter/api.h>
TSLanguage *tree_sitter_ExprLang();

使用生成的语言配置的 *TSParser* 类型的实例,可以将字符串转换为 *TSTree* 实例,后者充当 *TSNode* 结构层次结构的容器。每个节点都维护其语法符号的标识以及它所代表的文本的字节范围。

最后,我们需要一种将节点转换为抽象语法实例的方法;下面我们使用上下文参数来区分节点符号并提取源文本的范围。

extension Expr {
  init(node: TSNode, context ctx: Context) {
    assert(ctx.symbol(for: node) == "Expr" && node.count == 1)
    let child = node[0]
    switch ctx.symbol(for: child) {
      case "Num" :
        self = .num(Int(ctx.text(for: child))!)
      case "Add" :
        self = .add(Self(node: child[0], context: ctx), Self(node: child[2], context: ctx))
      case "Mult" :
        self = .mult(Self(node: child[0], context: ctx), Self(node: child[2], context: ctx))
      case "Paren" :
        self = Self(node: child[1], context: ctx)
      default :
        fatalError("unexpected symbol")
    }
  }
}

尽管转换代码很简单,但对于具有许多符号类型或具有带有可选元素的产生式语法来说,它很乏味。更重要的是,它的类型安全无法由编译器保证,因为语法和抽象语法之间没有显式关系。

简化的接口

使用此包,为 *Expr* 类型构建解析器简化为以下代码

import TSKit

@Grammar
struct ExprLang : Grammar {
  typealias Root = Expr
  static var productionRules : [ProductionRule] {
    return [
      ProductionRule(Expr.self, choicesByName: [
        "num": .init(.sym(Int.self)) { value in
          .num(value)
        },
        "add": .init(.prec(.left(1), .seq([.sym(Expr.self), "+", .sym(Expr.self)]))) { lhs, rhs in
          .add(lhs, rhs)
        },
        "mult": .init(.prec(.left(2), .seq([.sym(Expr.self), "*", .sym(Expr.self)]))) { lhs, rhs in
          .mult(lhs, rhs)
        },
        "paren": .init(.seq(["(", .sym(Expr.self), ")"])) { expr in
          expr
        }
      ]),
      ProductionRule(Int.self, .pat("[0-9]+")) { text in
        Int(text)!
      },
    ]
  }
}

这里,Grammar 宏应用于符合 Grammar 协议的结构。该协议要求指定出现在每个解析树根部的符号类型,以及将每个符号类型与语法表达式和该类型的构造函数相关联的产生式规则列表。Swift 的参数包功能使每个产生式规则都可以引用不同的符号类型序列,并且该宏确保语法表达式捕获的数量和类型与构造函数参数的数量和类型相匹配。

该宏生成 TSLanguage 实例,该实例用作解析器配置,以及将解析树节点转换为根类型实例的方法。

static var language : UnsafePointer<TSLanguage>
static func translate(parseTree node: TSNode, in context: ParsingContext) -> Root

Grammar 协议提供了一种便捷的方法,可以将文本转换为其 Root 类型 -- 在解析树包含错误时抛出异常。

static func parse(inputSource src: InputSource) throws -> Root

由于 Swift 宏既不能调用子进程,也不能与文件系统交互,因此该宏依赖于 tree-sitter 的 fork 版本,该版本扩展了可调用接口以生成 Swift 等价的 *parser.c*。

安装

构建此项目需要一个 Apple 平台(由于使用了二进制目标),并且安装了 tree-sitter 和 Rust(为了构建 tree-sitter fork)。克隆项目后,必须从签出目录运行 shell 脚本 build_xcframework.rb,才能创建二进制目标 TreeSitterCLI.xcframework。如果通过 swift package update 更新了 tree-sitter 工作副本,则必须再次运行该脚本。

其他示例

一些示例解析器作为测试用例提供

ExprLang 显示涉及函数调用和各种运算符的算术表达式。

LambdaTests 显示了一个无类型 lambda 演算。

JSONTests 显示了一个 JSON。

TypedLang 显示了一种最小的类型化函数式语言,具有块、闭包和声明。

相关工作

tree-sitter 的 Swift 绑定已经存在于 https://github.com/ChimeHQ/SwiftTreeSitter。该工作将几乎完整的 tree-sitter 运行时 API 暴露给 Swift,但依赖于 tree-sitter 的标准技术,用于将 javascript 语法规范映射到单独编译的 C 代码。这项工作公开了 tree-sitter 功能的最小子集,但允许完全在 Swift 中定义解析器 -- 消除了对 javascript 和混合语言目标的需求,并简化了构建过程。

未来计划

这是一个正在进行中的工作,因此存在许多已知 问题,并且很可能通过实验发现更多。其中最明显的是