├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ └── xcschemes
│ ├── TSKit.xcscheme
│ ├── TSKitTests.xcscheme
│ └── TreeSitterKit.xcscheme
├── Documents
└── journal-daspoon.md
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── TSCommon
│ ├── Exception.swift
│ └── swiftgen.swift
├── TSKit
│ ├── Expression.swift
│ ├── Extensions
│ │ ├── Array-ext.swift
│ │ ├── BinaryInteger-ext.swift
│ │ ├── StaticString.swift
│ │ ├── String-ext.swift
│ │ ├── UnsafeBufferPointer.swift
│ │ └── UnsafePointer.swift
│ ├── Grammar.swift
│ ├── GrammarPlugin.swift
│ ├── InputSource.swift
│ ├── ParsingContext.swift
│ ├── Precedence.swift
│ ├── ProductionRule.swift
│ ├── Punctuation.swift
│ ├── StringInputSource.swift
│ ├── SyntaxError.swift
│ ├── TSFieldMapEntry.swift
│ ├── TSInput.swift
│ ├── TSInputEncoding.swift
│ ├── TSLanguage.swift
│ ├── TSLexMode.swift
│ ├── TSLookaheadIterator.swift
│ ├── TSNode.swift
│ ├── TSParseActionEntry.swift
│ ├── TSParser.swift
│ ├── TSQuery.swift
│ ├── TSQueryCursor.swift
│ ├── TSSymbolMetadata.swift
│ ├── TSSymbolType.swift
│ ├── TSTree.swift
│ ├── TSTreeCursor.swift
│ └── Token.swift
├── TSMacros
│ ├── ExpansionError.swift
│ ├── ExpressionRep.swift
│ ├── Extensions
│ │ ├── Foundation-ext.swift
│ │ ├── Swift-ext.swift
│ │ └── SwiftSyntax-ext.swift
│ ├── GrammarMacro.swift
│ ├── GrammarRep.swift
│ ├── Plugin.swift
│ ├── PrecedenceRep.swift
│ ├── ProductionRuleRep.swift
│ ├── PunctuationRep.swift
│ ├── RawExpression.swift
│ ├── Signature.swift
│ └── TokenRep.swift
└── TSPlayground
│ ├── App.swift
│ ├── CodeSession.swift
│ ├── CodeView.swift
│ ├── IssuesView.swift
│ ├── MyGrammar.swift
│ ├── ParseTreeView.swift
│ ├── Preferences.swift
│ ├── TSKit-ext.swift
│ └── Utilities
│ ├── AdjustableDivider.swift
│ ├── Foundation-ext.swift
│ ├── OulineView.swift
│ └── SplitView.swift
├── Tests
├── ExprLang
│ ├── ExprLang.swift
│ └── ExprLangTests.swift
├── TSKit
│ ├── JsonTests.swift
│ ├── LambdaTests.swift
│ ├── ParserGenTests.swift
│ ├── StaticStringTests.swift
│ ├── StringTests.swift
│ └── UnsafePointerTests.swift
└── TypedLang
│ ├── TypedLang.swift
│ └── TypedLangTests.swift
└── build_xcframework.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | #*#
3 | .DS_Store
4 | /.build
5 | /Packages
6 | xcuserdata/
7 | DerivedData/
8 | .swiftpm/configuration/registries.json
9 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
10 | .netrc
11 | XCFrameworks
12 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/TSKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
43 |
49 |
50 |
56 |
57 |
58 |
59 |
61 |
62 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/TSKitTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
16 |
18 |
24 |
25 |
26 |
27 |
28 |
38 |
39 |
43 |
44 |
45 |
46 |
52 |
53 |
55 |
56 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/TreeSitterKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
48 |
54 |
55 |
56 |
58 |
64 |
65 |
66 |
68 |
74 |
75 |
76 |
78 |
84 |
85 |
86 |
88 |
94 |
95 |
96 |
98 |
104 |
105 |
106 |
108 |
114 |
115 |
116 |
118 |
124 |
125 |
126 |
127 |
128 |
138 |
139 |
145 |
146 |
152 |
153 |
154 |
155 |
157 |
158 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 David Spooner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-syntax",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-syntax.git",
7 | "state" : {
8 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
9 | "version" : "509.1.1"
10 | }
11 | },
12 | {
13 | "identity" : "tree-sitter",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/daspoon/tree-sitter",
16 | "state" : {
17 | "revision" : "47bfa8bab3d7a8ac34a1aa11ccc22c79f1894ec6",
18 | "version" : "0.1.7"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 | import CompilerPluginSupport
5 |
6 | let package = Package(
7 | name: "TreeSitterKit",
8 | platforms: [.macOS(.v13), .iOS(.v16), .macCatalyst(.v16)],
9 | products: [
10 | .library(name: "TSKit", targets: ["TSKit"]),
11 | .executable(name: "TSPlayground", targets: ["TSPlayground"]),
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/daspoon/tree-sitter", from: "0.1.7"),
15 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
16 | ],
17 | targets: [
18 | // The library of code shared by TSKit and TSMacros
19 | .target(name: "TSCommon",
20 | dependencies: [
21 | "TreeSitterCLI",
22 | ]
23 | ),
24 | // The exported library providing a Swift interface for tree-sitter.
25 | .target(name: "TSKit",
26 | dependencies: [
27 | .product(name: "TreeSitter", package: "tree-sitter"),
28 | "TSCommon",
29 | "TSMacros",
30 | ]
31 | ),
32 | // The definitions of macros exported by TSKit.
33 | .macro(name: "TSMacros",
34 | dependencies: [
35 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
36 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
37 | "TSCommon",
38 | ]
39 | ),
40 | .executableTarget(name: "TSPlayground", dependencies: [
41 | "TSKit",
42 | ]),
43 | // Binary tree-sitter CLI library
44 | .binaryTarget(name: "TreeSitterCLI",
45 | path: "XCFrameworks/TreeSitterCLI.xcframework"
46 | ),
47 | // Tests for general TSKit functionality
48 | .testTarget(name: "TSKitTests",
49 | dependencies: [
50 | "TSKit",
51 | ],
52 | path: "Tests/TSKit"
53 | ),
54 | // Example languages for testing purposes...
55 | .testTarget(name: "ExprLangTests",
56 | dependencies: [
57 | "TSKit",
58 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
59 | ],
60 | path: "Tests/ExprLang"
61 | ),
62 | .testTarget(name: "TypedLangTests",
63 | dependencies: [
64 | "TSKit",
65 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
66 | ],
67 | path: "Tests/TypedLang"
68 | ),
69 | ]
70 | )
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TreeSitterKit
2 |
3 | This project aims to simplify the use of [tree-sitter](https://tree-sitter.github.io/tree-sitter/) for parsing programming languages and data formats in [Swift](https://swift.org).
4 | Specifically, it enables writing grammar rules as Swift code and automates the translation of parse trees into the Swift data structures which implement an abstract syntax.
5 |
6 |
7 | ## Motivation
8 |
9 | Consider an abstract syntax for a simple language of arithmetic expressions.
10 |
11 | ```
12 | indirect enum Expr {
13 | case num(Int)
14 | case add(Expr, Expr)
15 | case mult(Expr, Expr)
16 | }
17 | ```
18 |
19 | We'll first show the steps necessary to parse this language using tree sitter directly, and then see how the process is simplified through use of this package.
20 |
21 |
22 | ### Tree-sitter Basics
23 |
24 | A tree-sitter grammar is typically written as a javascript object.
25 |
26 | ```
27 | module.exports = grammar({
28 | name: 'ExprLang',
29 | rules: {
30 | Expr: $ => choice(
31 | $.Num,
32 | $.Add,
33 | $.Mult,
34 | $.Paren
35 | ),
36 | Num: $ => [0-9]+,
37 | Add: $ => prec(left(1, seq($.Expr, '+', $.Expr))),
38 | Mult: $ => prec(left(2, seq($.Expr, '*', $.Expr))),
39 | Paren: $ => seq('(', $.Expr, ')'),
40 | }
41 | })
42 | ```
43 |
44 | The tree-sitter program is used to generate a parser configuration as a pair of C source files, *parser.c* and *parser.h*.
45 |
46 | ```
47 | % tree-sitter generate grammar.js
48 | ```
49 |
50 | The generated files, along with a header file declaring the generated *TSLanguage* structure, must be included in your project.
51 |
52 | ```
53 | #include
54 | TSLanguage *tree_sitter_ExprLang();
55 | ```
56 |
57 | An instance of the *TSParser* type, configured with the generated language, enables translating strings into *TSTree* instances which act as containers for a hierarchy of *TSNode* structures.
58 | Each node maintains the identity of its grammar symbol and the byte range of the text which it represents.
59 |
60 | Finally we need a method to translate nodes to instances of our abstract syntax; below we use a context parameter to distinguish node symbols and to extract ranges of source text.
61 |
62 | ```
63 | extension Expr {
64 | init(node: TSNode, context ctx: Context) {
65 | assert(ctx.symbol(for: node) == "Expr" && node.count == 1)
66 | let child = node[0]
67 | switch ctx.symbol(for: child) {
68 | case "Num" :
69 | self = .num(Int(ctx.text(for: child))!)
70 | case "Add" :
71 | self = .add(Self(node: child[0], context: ctx), Self(node: child[2], context: ctx))
72 | case "Mult" :
73 | self = .mult(Self(node: child[0], context: ctx), Self(node: child[2], context: ctx))
74 | case "Paren" :
75 | self = Self(node: child[1], context: ctx)
76 | default :
77 | fatalError("unexpected symbol")
78 | }
79 | }
80 | }
81 | ```
82 |
83 | Although the translation code is straight-forward, it is tedious for grammars which have many symbol types or have productions with optional elements.
84 | More importantly, its type safety cannot be guaranteed by the compiler because there is no explict relationship between the grammar and the abstract syntax.
85 |
86 | ### Streamlined Interface
87 |
88 | Using this package, the construction of a parser for the *Expr* type is reduced to the following code:
89 |
90 | ```
91 | import TSKit
92 |
93 | @Grammar
94 | struct ExprLang : Grammar {
95 | typealias Root = Expr
96 | static var productionRules : [ProductionRule] {
97 | return [
98 | ProductionRule(Expr.self, choicesByName: [
99 | "num": .init(.sym(Int.self)) { value in
100 | .num(value)
101 | },
102 | "add": .init(.prec(.left(1), .seq([.sym(Expr.self), "+", .sym(Expr.self)]))) { lhs, rhs in
103 | .add(lhs, rhs)
104 | },
105 | "mult": .init(.prec(.left(2), .seq([.sym(Expr.self), "*", .sym(Expr.self)]))) { lhs, rhs in
106 | .mult(lhs, rhs)
107 | },
108 | "paren": .init(.seq(["(", .sym(Expr.self), ")"])) { expr in
109 | expr
110 | }
111 | ]),
112 | ProductionRule(Int.self, .pat("[0-9]+")) { text in
113 | Int(text)!
114 | },
115 | ]
116 | }
117 | }
118 | ```
119 |
120 | Here the *Grammar* macro is applied to a structure conforming to the *Grammar* protocol.
121 | The protocol requires specifying the symbol type which appears at the root of each parse tree, together with a list of production rules associating each symbol type with a syntax expression and a constructor for that type.
122 | Swift's parameter packs feature enables each production rule to refer to a distinct sequence of symbol types, and the macro ensures the number and type of syntax expression captures matches the number and type of constructor parameters.
123 |
124 | The macro generates the *TSLanguage* instance which serves as parser configuration and a method for translating parse tree nodes into instances of the root type.
125 |
126 | ```
127 | static var language : UnsafePointer
128 | static func translate(parseTree node: TSNode, in context: ParsingContext) -> Root
129 | ```
130 |
131 | The *Grammar* protocol provides a convenience method for translating text to its *Root* type -- throwing when the parse tree contains errors.
132 |
133 | ```
134 | static func parse(inputSource src: InputSource) throws -> Root
135 | ```
136 |
137 | Since Swift macros can neither invoke subprocesses nor interact with the file system, the macro relies on a [fork](https://github.com/daspoon/tree-sitter) of tree-sitter extended with a callable interface to generate the Swift equivalent of *parser.c*.
138 |
139 |
140 | ## Installation
141 |
142 | Building this project requires an Apple platform (due to the use of binary targets) with both tree-sitter and Rust (in order to build the tree-sitter fork) installed.
143 | After cloning the project you must run the shell script `build_xcframework.rb` from the checkout directory in order to create the binary target TreeSitterCLI.xcframework.
144 | That script must be run again if the tree-sitter working copy is updated via `swift package update`.
145 |
146 |
147 | ## Other Examples
148 |
149 | Some example parsers are provided as test cases:
150 |
151 | [ExprLang](https://github.com/daspoon/tree-sitter-kit/blob/main/Tests/ExprLang/ExprLang.swift) shows arithmetic expressions involving function calls and various operators.
152 |
153 | [LambdaTests](https://github.com/daspoon/tree-sitter-kit/blob/main/Tests/TSKit/LambdaTests.swift) shows an untyped lambda calculus.
154 |
155 | [JSONTests](https://github.com/daspoon/tree-sitter-kit/blob/main/Tests/TSKit/JsonTests.swift) shows a JSON.
156 |
157 | [TypedLang](https://github.com/daspoon/tree-sitter-kit/blob/main/Tests/TypedLang/TypedLang.swift) shows a minimal typed functional language with blocks, closures and declarations.
158 |
159 |
160 | ## Related Work
161 |
162 | A Swift binding for tree-sitter already exists at https://github.com/ChimeHQ/SwiftTreeSitter.
163 | That work exposes nearly the full tree-sitter runtime API to Swift, but relies on tree-sitter's standard tech for mapping javascript grammar specifications to separately compiled C code.
164 | This work exposes a minimal subset of tree-sitter functionality, but enables defining parsers entirely in Swift -- eliminating the need for javascript and mixed-language targets, and streamlining the build process.
165 |
166 |
167 | ## Future Plans
168 |
169 | This is a work in progress, so there are a number of known [issues](https://github.com/daspoon/tree-sitter-kit/issues) and likely many more to be discovered through experimentation.
170 | Among the most obvious:
171 |
172 | - Support for error reporting is very limited. Although tree-sitter performs implicit error recovery, it does not provide support for [generating useful error messages](https://github.com/tree-sitter/tree-sitter/issues/255). I have added the application target TSPlayground in order to explore the space, but haven't made much progress.
173 |
174 | - TreeSitterCLI.xcframework must be built using a separate script. I don't see how to improve the situation without additional features in Swift Packge Manager.
175 |
176 | - All production rules must be defined within the top-level grammar struct, which makes non-trivial grammar definitions lengthy. Ideally each symbol type would specify its own production rules and the set of all rules would be calculated from the grammar's root type. Unfortunately Swift member macros have no apparent means to access components outside the declaration to which they're attached.
177 |
--------------------------------------------------------------------------------
/Sources/TSCommon/Exception.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// *Exception* is used to signal error conditions in macro expansion.
7 |
8 | public struct Exception : Error {
9 | public let code : Int
10 | public let failureReason : String
11 |
12 | public init(code k: Int, failureReason r: String) {
13 | code = k
14 | failureReason = r
15 | }
16 |
17 | public init(_ failureReason: String) {
18 | self.init(code: -1, failureReason: failureReason)
19 | }
20 | }
21 |
22 | extension Exception : CustomStringConvertible {
23 | public var description : String {
24 | failureReason
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/TSCommon/swiftgen.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitterCLI
6 | import Foundation
7 |
8 |
9 | // Maintain the pairing of status code and text returned by the call to `swiftgen`.
10 | fileprivate class Result {
11 | let status : Int
12 | let text : NSString
13 | init(status: UInt32, textBytes: UnsafePointer?, textLen: UInt32) {
14 | if let textBytes {
15 | if let text = NSString(bytes: textBytes, length: Int(textLen), encoding: NSUTF8StringEncoding) {
16 | self.status = Int(status)
17 | self.text = text
18 | } else {
19 | self.status = -1
20 | self.text = "Failed to interpret generated bytes as UTF8"
21 | }
22 | } else {
23 | self.status = -1
24 | self.text = "Pointer to generated bytes is null"
25 | }
26 | }
27 | }
28 |
29 |
30 | /// Return the Swift parser source for a grammar expressed as JSON and the access modifier applied to enclosing struct.
31 | package func generateParserSource(for jsonText: String, accessModifier modText: String = "", abi_version v: UInt32 = 0) throws -> String {
32 | // We must call tree-sitter in a context where the contiguous utf8 representations of the grammar and visibility strings are available...
33 | let optionalOptionalSwiftText = try jsonText.utf8.withContiguousStorageIfAvailable { jsonBuf in
34 | try modText.utf8.withContiguousStorageIfAvailable { modBuf in
35 | try generateParserSource(for: jsonBuf, accessModifier: modBuf, abi_version: v)
36 | }
37 | }
38 |
39 | // The generated source will be nil if either of the grammar or visibility text have no contiguous UTF8 representation.
40 | guard let optionalSwiftText = optionalOptionalSwiftText, let swiftText = optionalSwiftText
41 | else { throw Exception("given text lacks a contiguous UTF8 representation") }
42 |
43 | return swiftText
44 | }
45 |
46 | /// Return the Swift parser source for a grammar expressed as a UTF8-encoding JSON text.
47 | package func generateParserSource(for jsonBuf: UnsafeBufferPointer, accessModifier modBuf: UnsafeBufferPointer, abi_version v: UInt32 = 0) throws -> String {
48 | guard jsonBuf.count <= Int(UInt32.max)
49 | else { throw Exception("grammar text is too long") }
50 | guard modBuf.count <= Int(UInt32.max)
51 | else { throw Exception("visibility text is too long") }
52 |
53 | // Invoke tree-sitter with a completion callback that returns a retained instance of Result as an opaque pointer.
54 | let optionalUnsafeRawPointer = swiftgen(jsonBuf.baseAddress, UInt32(jsonBuf.count), modBuf.baseAddress, UInt32(modBuf.count), v) {
55 | let result = Result(status: $0, textBytes: $1, textLen: $2)
56 | return UnsafeRawPointer(Unmanaged.passRetained(result).toOpaque())
57 | }
58 |
59 | // The call to 'swiftgen' returns the result of the completion callback, so that value must be non-nil.
60 | guard let unsafeRawPointer = optionalUnsafeRawPointer
61 | else { throw Exception("unexpected null pointer") }
62 |
63 | // Extract and take ownership of the Result object: throw if the call was unsuccessful, or return the generated text otherwise.
64 | let result = Unmanaged.fromOpaque(unsafeRawPointer).takeRetainedValue()
65 | guard result.status == 0
66 | else { throw Exception(code: result.status, failureReason: result.text as String) }
67 | return result.text as String
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/TSKit/Expression.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | /// An analogue of tree-sitter syntax expressions.
6 | public indirect enum Expression {
7 | case lit(String)
8 | case pat(String)
9 | case sym(Any.Type)
10 | case alt([String])
11 | case opt(Expression)
12 | case rep(Expression, Punctuation?=nil)
13 | case seq([Expression])
14 | case prec(Precedence, Expression)
15 | }
16 |
17 |
18 | extension Expression : ExpressibleByStringLiteral {
19 | public init(stringLiteral s: String) {
20 | self = .lit(s)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TSKit/Extensions/Array-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | extension Array {
7 | public static func extractElements(from node: TSNode, in context: ParsingContext, using f: (TSNode, ParsingContext) -> Element) -> Self {
8 | return (0 ..< node.count).map { i in
9 | f(node[i], context)
10 | }
11 | }
12 |
13 | public static func extractElements(separatedBy sep: String, from node: TSNode, in context: ParsingContext, using f: (TSNode, ParsingContext) -> Element) -> Self {
14 | let n = node.count; assert(n > 0 && n % 2 == 1)
15 | return stride(from: 0, to: n, by: 2).map { i in
16 | assert(i == 0 || context.text(for: node[i-1]) == sep)
17 | return f(node[i], context)
18 | }
19 | }
20 |
21 | public static func extractElements(delimitedBy del: String, from node: TSNode, in context: ParsingContext, using f: (TSNode, ParsingContext) -> Element) -> Self {
22 | let n = node.count; assert(n > 0 && n % 2 == 0)
23 | return stride(from: 0, to: n, by: 2).map { i in
24 | assert(context.text(for: node[i+1]) == del)
25 | return f(node[i], context)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/TSKit/Extensions/BinaryInteger-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | extension BinaryInteger {
7 | public mutating func postincrement() -> Self {
8 | defer { self += 1 }
9 | return self
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/TSKit/Extensions/StaticString.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | extension StaticString {
6 | /// Return the address of the first ascii code unit. Accessing this property when isASCII is false triggers a runtime error.
7 | public var cStringStart : UnsafePointer {
8 | return UnsafeRawPointer(utf8Start).bindMemory(to: CChar.self, capacity: utf8CodeUnitCount + 1)
9 | }
10 |
11 | /// Compare a static string with an ordinary string.
12 | public static func == (lhs: Self, rhs: String) -> Bool {
13 | let lhsCount = lhs.utf8CodeUnitCount
14 | let rhsChars = rhs.utf8
15 | guard rhsChars.count == lhsCount
16 | else { return false }
17 | return lhs.withUTF8Buffer { lhsChars in
18 | rhsChars.enumerated().allSatisfy({i, c in lhsChars[i] == c})
19 | }
20 | }
21 |
22 | enum Order { case ascending, equal, descending }
23 | static func compare(_ lhs: Self, _ rhs: Self) -> Order {
24 | let (lhsCount, rhsCount) = (lhs.utf8CodeUnitCount, rhs.utf8CodeUnitCount)
25 | return lhs.withUTF8Buffer { lhsChars in
26 | rhs.withUTF8Buffer { rhsChars in
27 | for i in 0 ..< min(lhsCount, rhsCount) {
28 | let diff = Int(rhsChars[i]) - Int(lhsChars[i])
29 | if diff != 0 {
30 | return diff > 0 ? .ascending : .descending
31 | }
32 | }
33 | let diff = rhsCount - lhsCount
34 | return diff > 0 ? .ascending : diff < 0 ? .descending : .equal
35 | }
36 | }
37 | }
38 | }
39 |
40 | extension StaticString : Equatable {
41 | public static func == (lhs: Self, rhs: Self) -> Bool {
42 | compare(lhs, rhs) == .equal
43 | }
44 | }
45 |
46 | extension StaticString : Comparable {
47 | public static func < (lhs: Self, rhs: Self) -> Bool {
48 | compare(lhs, rhs) == .ascending
49 | }
50 |
51 | public static func <= (lhs: Self, rhs: Self) -> Bool {
52 | compare(lhs, rhs) != .descending
53 | }
54 |
55 | public static func >= (lhs: Self, rhs: Self) -> Bool {
56 | compare(lhs, rhs) != .ascending
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/TSKit/Extensions/String-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 |
7 |
8 | extension String {
9 | /// Return the concatenation of a number of newline characters, defaulting to 1.
10 | static func newline(_ n: Int = 1) -> String
11 | { .init(repeating: "\n", count: n) }
12 |
13 | /// Return the concatenation of a number of space characters, defaulting to 1.
14 | static func space(_ n: Int) -> String
15 | { .init(repeating: " ", count: n) }
16 |
17 | /// Return a copy of the receiver with each non-initial line prefixed by the given number of space characters.
18 | func indented(_ n: Int) -> String
19 | { components(separatedBy: String.newline()).joined(separator: .newline() + .space(n)) }
20 |
21 | /// Return a buffer containing the receiver's utf8 representation along with its length. The buffer contains a null terminator which does not contribute to the returned length. The caller is responsible for deallocating the buffer.
22 | var cstringBuffer : (UnsafePointer, Int) {
23 | let v = utf8
24 | let n = v.count
25 | let buf = UnsafeMutablePointer.allocate(capacity: n + 1)
26 | for (i, c) in v.enumerated() {
27 | buf[i] = c
28 | }
29 | buf[n] = 0
30 | return (UnsafePointer(UnsafeRawPointer(buf).bindMemory(to: CChar.self, capacity: n)), n)
31 | }
32 |
33 | /// Translate the given byte range into a character range for the given encoding. Return nil if either the given range does not correspond to a valid encoded sequence or the given encoding is not supported.
34 | public func characterRange(forByteRange byteRange: Range, encoding: Encoding) -> Range? {
35 | switch encoding {
36 | case .utf8 :
37 | utf8.characterRange(forByteRange: byteRange)
38 | case .utf16 :
39 | nil // TODO: ...
40 | default :
41 | nil
42 | }
43 | }
44 |
45 | public func substring(forByteRange r: Range, encoding e: Encoding) -> Substring? {
46 | guard let charRange = characterRange(forByteRange: r, encoding: e)
47 | else { return nil }
48 | let lb = index(startIndex, offsetBy: charRange.lowerBound)
49 | let ub = index(startIndex, offsetBy: charRange.upperBound)
50 | return self[lb ..< ub]
51 | }
52 |
53 | public func string(forByteRange r: Range, encoding e: Encoding) -> String? {
54 | substring(forByteRange: r, encoding: e).map {String($0)}
55 | }
56 |
57 | /// Return a copy of the receiver with the given prefix and/or suffix removed.
58 | func removing(prefix: String? = nil, suffix: String? = nil) -> String {
59 | var trimmed = self
60 | if let prefix, let range = trimmed.range(of: prefix, options:.anchored) {
61 | trimmed = String(trimmed[range.upperBound ..< trimmed.endIndex])
62 | }
63 | if let suffix, let range = trimmed.range(of: suffix, options: [.anchored, .backwards]) {
64 | trimmed = String(trimmed[trimmed.startIndex ..< range.lowerBound])
65 | }
66 | return trimmed
67 | }
68 | }
69 |
70 |
71 | extension String.UTF8View {
72 | /// Count the number of characters between `fromIndex` and `toIndex`, which default to `startIndex` and `endIndex` respectively.
73 | /// Return `nil` if the specified byte range is either lies outside the receiver's bounds or does not represent a valid utf8 character sequence.
74 | public func characterCount(fromIndex i: Index? = nil, toIndex n: Index? = nil) -> Int? {
75 | var (i, n) = (i ?? startIndex, n ?? endIndex)
76 | guard startIndex <= i && n <= endIndex else { return nil }
77 | var k = 0
78 | while i < n {
79 | let c = self[i]
80 | func validate(_ i: inout Index, trailingByteCount: Int, accumulatedCharCount: inout Int) -> Bool {
81 | guard index(i, offsetBy: trailingByteCount) < endIndex else { return false }
82 | i = index(i, offsetBy: 1)
83 | for _ in 0 ..< trailingByteCount {
84 | guard self[i] & 0xc0 == 0x80 else { return false }
85 | i = index(i, offsetBy: 1)
86 | }
87 | accumulatedCharCount += 1
88 | return true
89 | }
90 | if c & 0x80 == 0x00 {
91 | guard validate(&i, trailingByteCount: 0, accumulatedCharCount: &k) else { return nil }
92 | } else if c & 0xe0 == 0xc0 {
93 | guard validate(&i, trailingByteCount: 1, accumulatedCharCount: &k) else { return nil }
94 | } else if c & 0xf0 == 0xe0 {
95 | guard validate(&i, trailingByteCount: 2, accumulatedCharCount: &k) else { return nil }
96 | } else if c & 0xf8 == 0xf0 {
97 | guard validate(&i, trailingByteCount: 3, accumulatedCharCount: &k) else { return nil }
98 | } else {
99 | return nil
100 | }
101 | }
102 | return k
103 | }
104 |
105 | /// Return the character range for the given byte range, or nil if the given range does not correspond to a sequence of valid characters.
106 | public func characterRange(forIndexRange indexRange: Range) -> Range? {
107 | guard indexRange.upperBound <= endIndex
108 | else { return nil }
109 | guard let charStart = characterCount(toIndex: indexRange.lowerBound)
110 | else { return nil }
111 | guard let charCount = characterCount(fromIndex: indexRange.lowerBound, toIndex: indexRange.upperBound)
112 | else { return nil }
113 | return charStart ..< charStart + charCount
114 | }
115 |
116 | /// Return the character range for the given byte range, or nil if the given range does not correspond to a sequence of valid characters.
117 | public func characterRange(forByteRange byteRange: Range) -> Range? {
118 | let indexRange = index(startIndex, offsetBy: byteRange.lowerBound) ..< index(startIndex, offsetBy: byteRange.upperBound)
119 | return characterRange(forIndexRange: indexRange)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/TSKit/Extensions/UnsafeBufferPointer.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 |
7 |
8 | extension UnsafeBufferPointer {
9 | /// Return a buffer containing the given elements.
10 | public static func initialized(with elements: [Element]) -> Self {
11 | let umrbp = UnsafeMutableRawBufferPointer.allocate(byteCount: elements.count * MemoryLayout.stride, alignment: MemoryLayout.alignment)
12 | let umbp = umrbp.bindMemory(to: Element.self)
13 | for i in 0 ..< elements.count {
14 | umbp[i] = elements[i]
15 | }
16 | return Self(umbp)
17 | }
18 |
19 | /// Return a buffer representing an array of *count* values, containing the given index/value pairs and *zero* elsewhere.
20 | public static func initialized(count: Int, zero: Element, indexValuePairs: [(index: Int, value: Element)]) -> Self {
21 | let umrbp = UnsafeMutableRawBufferPointer.allocate(byteCount: count * MemoryLayout.stride, alignment: MemoryLayout.alignment)
22 | let umbp = umrbp.bindMemory(to: Element.self)
23 | var indices = IndexSet(integersIn: 0 ..< count)
24 | for (index, value) in indexValuePairs {
25 | assert((0 ..< count).contains(index))
26 | umbp[index] = value
27 | indices.remove(index)
28 | }
29 | for index in indices {
30 | umbp[index] = zero
31 | }
32 | return Self(umbp)
33 | }
34 |
35 | // Return a buffer representing a 2d matrix of *rowCount* times *columnCount* values, containing the given row/column/value triples and *zero* elsewhere.
36 | public static func initialized(rowCount: Int, columnCount: Int, zero: Element, rowColumnValueTriples: [(row: Int, column: Int, value: Element)]) -> Self {
37 | let umrbp = UnsafeMutableRawBufferPointer.allocate(byteCount: rowCount * columnCount * MemoryLayout.stride, alignment: MemoryLayout.alignment)
38 | let umbp = umrbp.bindMemory(to: Element.self)
39 | var indices = IndexSet(integersIn: 0 ..< rowCount * columnCount)
40 | for (row, column, value) in rowColumnValueTriples {
41 | assert((0 ..< rowCount).contains(row) && (0 ..< columnCount).contains(column))
42 | let index = row * columnCount + column
43 | umbp[index] = value
44 | indices.remove(index)
45 | }
46 | for index in indices {
47 | umbp[index] = zero
48 | }
49 | return Self(umbp)
50 | }
51 |
52 | // Return a buffer representing a 2d matrix of *rowCount* times *columnCount* values, with each specified row containing the given column values and *zero* elsewhere.
53 | public static func initialized(rowCount: Int, columnCount: Int, zero: Element, columnValuesByRow: [(row: R, [(column: C, value: Element)])]) -> Self {
54 | let umrbp = UnsafeMutableRawBufferPointer.allocate(byteCount: rowCount * columnCount * MemoryLayout.stride, alignment: MemoryLayout.alignment)
55 | let umbp = umrbp.bindMemory(to: Element.self)
56 | var indices = IndexSet(integersIn: 0 ..< rowCount * columnCount)
57 | for (row, columnValues) in columnValuesByRow {
58 | for (column, value) in columnValues {
59 | assert((0 ..< rowCount).contains(Int(row)) && (0 ..< columnCount).contains(Int(column)))
60 | let index = Int(row) * columnCount + Int(column)
61 | umbp[index] = value
62 | indices.remove(index)
63 | }
64 | }
65 | for index in indices {
66 | umbp[index] = zero
67 | }
68 | return Self(umbp)
69 | }
70 | }
71 |
72 |
73 | extension UnsafeBufferPointer where Element : ExpressibleByIntegerLiteral {
74 | public static func initialized(count k: Int, indexValuePairs v: [(index: Int, value: Element)]) -> Self {
75 | .initialized(count: k, zero: 0, indexValuePairs: v)
76 | }
77 |
78 | public static func initialized(rowCount n: Int, columnCount m: Int, rowColumnValueTriples v: [(row: Int, column: Int, value: Element)]) -> Self {
79 | .initialized(rowCount: n, columnCount: m, zero: 0, rowColumnValueTriples: v)
80 | }
81 |
82 | public static func initialized(rowCount n: Int, columnCount m: Int, columnValuesByRow v: [(row: R, [(column: C, value: Element)])]) -> Self {
83 | .initialized(rowCount: n, columnCount: m, zero: 0, columnValuesByRow: v)
84 | }
85 | }
86 |
87 |
88 | extension UnsafeBufferPointer where Element == UnsafePointer? {
89 | typealias CString = UnsafePointer?
90 |
91 | /// Allocate and initialize a buffer containing the C string pointers for the given array of static strings.
92 | /// A runtime error will occur unless all given strings satisfy `hasPointerRepresentation`.
93 | public static func arrayOfCSrings(_ strings: [StaticString?]) -> Self {
94 | let ptrbuf = UnsafeMutableRawBufferPointer.allocate(byteCount: strings.count * MemoryLayout.stride, alignment: MemoryLayout.alignment)
95 | .bindMemory(to: CString.self)
96 | for (i, string) in strings.enumerated() {
97 | ptrbuf[i] = string?.cStringStart
98 | }
99 | return Self(ptrbuf)
100 | }
101 |
102 | public static func arrayOfCSrings(_ strings: [StaticString]) -> Self {
103 | arrayOfCSrings(strings.map {$0 as StaticString?})
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/TSKit/Extensions/UnsafePointer.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | extension UnsafePointer {
7 | /// Return an allocated instance intialized with the given procedure. If 'zero' is true (the default) then the allocated memory is first initialized to zero.
8 | public static func initialized(implicitlyZeroing zero: Bool = true, using initializer: (inout Pointee) -> Void) -> Self {
9 | let rp = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout.size, alignment: MemoryLayout.alignment)
10 | if zero {
11 | _ = rp.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size)
12 | }
13 | let mp = rp.bindMemory(to: Pointee.self, capacity: 1)
14 | initializer(&mp.pointee)
15 | return UnsafeRawPointer(rp).bindMemory(to: Pointee.self, capacity: 1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/TSKit/Grammar.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 | import TreeSitterCLI
7 | import TSCommon
8 | import Foundation
9 |
10 |
11 | /// The *Grammar* protocol, in conjunction with the same-named macro, enables synthesis of a
12 | /// tree-sitter parser from a list of production rules.
13 |
14 | public protocol Grammar {
15 | associatedtype Root
16 |
17 | /// The grammar/language name. The default implementation returns the receiver's type name.
18 | static var name : String { get }
19 |
20 | /// The set of production rules. Implementation required.
21 | static var productionRules : [ProductionRule] { get }
22 |
23 | /// Specifies the pattern for the word rule. The default implementation returns nil, indicating there is no special treatment of words.
24 | static var word : String? { get }
25 |
26 | /// Specifies the tokens which are ignored. The default implementation returns nil, indicating the tree-sitter default (whitespace is ignored).
27 | static var extras : [Token]? { get }
28 |
29 | /// The shared instance of the language structure. Implementation provided by @Grammar.
30 | static var language : UnsafePointer { get }
31 |
32 | /// The array of symbol names, excluding the built-in errorSymbol and errorRepeatSymbol. Implementation provided by @Grammar.
33 | static var symbolNames : [StaticString] { get }
34 |
35 | /// Produce an instance of the root type from the the root node of a parse tree. Implementation provided by @Grammar.
36 | static func translate(parseTree node: TSNode, in context: ParsingContext) -> Root
37 |
38 | /// Return *true* if the rule for the given type is hidden. Implementation provided by @Grammar.
39 | static func isRuleHidden(for type: Any.Type) -> Bool
40 |
41 | /// Translate alternate rule symbols to their corresponding production rule symbols, leaving other symbols intact. Implementation provided by @Grammar.
42 | static func representative(for symbol: TSSymbol) -> TSSymbol
43 |
44 | /// Return the symbol for the given type, if any.
45 | static func symbol(for type: Any.Type) -> TSSymbol?
46 | }
47 |
48 |
49 | /// The Grammar macro is intended to be applied to structures conforming to the Grammar protocol.
50 | /// It implements the named protocol requirements, along with an extraction method of the following
51 | /// form for each symbol type `T` reachable from the production rule of the `Root` type:
52 | /// `private static func extractT(from: TSNode, in: ParsingContext) -> T
53 |
54 | @attached(member, names:
55 | named(language),
56 | named(isRuleHidden(for:)),
57 | named(translate(parseTree:context:)),
58 | named(symbolName(for:)),
59 | named(representative(for:)),
60 | named(symbol(for:)),
61 | arbitrary
62 | )
63 | public macro Grammar() = #externalMacro(module: "TSMacros", type: "GrammarMacro")
64 |
65 |
66 | /// Default implementations.
67 |
68 | extension Grammar {
69 | public static var name : String
70 | { "\(Self.self)" }
71 |
72 | public static var word : String?
73 | { nil }
74 |
75 | public static var extras : [Token]?
76 | { nil }
77 | }
78 |
79 |
80 | /// Utility methods.
81 |
82 | extension Grammar {
83 | /// Extract the text for the given node. This enables String to be treated as a parsable types within with respect to production rule captures.
84 | public static func extractString(from node: TSNode, in context: ParsingContext) -> String {
85 | context.text(for: node)
86 | }
87 |
88 | /// Return the symbol name for the given node.
89 | public static func symbolName(for node: TSNode) -> StaticString {
90 | symbolName(for: ts_node_grammar_symbol(node))
91 | }
92 |
93 | /// Return the string representation of the given symbol identifier.
94 | public static func symbolName(for symbol: TSSymbol) -> StaticString {
95 | switch symbol {
96 | case TSLanguage.errorSymbol : "error"
97 | case TSLanguage.errorRepeatSymbol : "errorRepeat"
98 | default :
99 | symbolNames[Int(symbol)]
100 | }
101 | }
102 |
103 | /// Return the 'type' of the given symbol.
104 | public static func symbolType(for symbol: TSSymbol) -> TSSymbolType {
105 | ts_language_symbol_type(language, symbol)
106 | }
107 |
108 | /// Create an instance of the root type from the given input source.
109 | public static func parse(inputSource src: InputSource) throws -> Root {
110 | // Create a parser for the given language
111 | let parser = TSParser(language)
112 | // Get the parse tree for the input source; this can return nil if parsing is cancelled.
113 | guard let tree = parser.parse(src)
114 | else { throw Exception("parsing was cancelled") }
115 | // Throw if the parse tree contains errors.
116 | guard tree.rootNode.hasError == false
117 | else { throw Exception("error in parse tree: \(tree.rootNode.description)") }
118 | // Ensure the root node corresponds to the start rule and has a single child.
119 | let startNode = tree.rootNode
120 | guard symbolName(for: startNode) == "start", startNode.count == 1
121 | else { throw Exception("start node has unexpected type (\(symbolName(for: startNode))) and/or count \(startNode.count)") }
122 | // Ensure the sole child of the start node either corresponds to a production of this type or is hidden (e.g. corresponds to an enum case).
123 | let rootNode = startNode[0]
124 | guard isRuleHidden(for: Root.self) || symbolName(for: rootNode) == "\(Root.self)"
125 | else { throw Exception("root node has unexpected type (\(symbolName(for: rootNode)))") }
126 | // Delegate to the ingestion method, providing the necessary context...
127 | return translate(parseTree: rootNode, in: ParsingContext(inputSource: src))
128 | }
129 |
130 | /// Create an instance of the root type from the given text.
131 | public static func parse(text: String, encoding: String.Encoding = .utf8) throws -> Root {
132 | guard let source = StringInputSource(string: text, encoding: encoding)
133 | else { throw Exception("unsupported string encoding: \(encoding)") }
134 | return try parse(inputSource: source)
135 | }
136 |
137 | /// Return the list of syntax errors.
138 | public static func syntaxErrors(in tree: TSTree, for text: String, encoding e: String.Encoding = .utf8) -> [SyntaxError] {
139 | var errors : [SyntaxError] = []
140 |
141 | func report(node: TSNode, kind k: SyntaxError.Kind) {
142 | guard let r = text.characterRange(forByteRange: node.sourceByteRange, encoding: e)
143 | else { fatalError("failed to translate byte range \(node.sourceByteRange) for encoding \(e)") }
144 | errors.append(SyntaxError(range: r, kind: k))
145 | }
146 |
147 | // Report errors in the given subtree; return true iff no error exists.
148 | func walk(_ node: TSNode) -> Bool {
149 | // Do nothing and return true if there is no embedded error.
150 | guard node.hasError || node.isError
151 | else { return true }
152 |
153 | // Nodes with no text range are deemed missing; process these before recursion to ensure reporting the highest-level symbol.
154 | if node.isEmpty {
155 | report(node: node, kind: .missing(representative(for: node.symbol)))
156 | return false
157 | }
158 |
159 | // Recurse over children, returning false if an error is found.
160 | guard (0 ..< node.count).reduce(true, {ok, i in walk(node[i]) && ok})
161 | else { return false }
162 |
163 | // Terminal error nodes represent invaild character sequences.
164 | if node.isError && node.count == 0 {
165 | report(node: node, kind: .unacceptable)
166 | return false
167 | }
168 |
169 | // Nodes whose state is .max appear to represent reductions which are potentially incomplete...
170 | if node.isError || node.state == .max {
171 | var pendingSymbols : [TSSymbol] = []
172 | for i in 0 ..< node.count {
173 | let child = node[i]
174 | if child.isError {
175 | for j in 0 ..< child.count {
176 | let grandchild = child[j]
177 | if grandchild.isError == false {
178 | pendingSymbols.append(representative(for: grandchild.symbol))
179 | }
180 | }
181 | }
182 | else {
183 | pendingSymbols.append(representative(for: child.symbol))
184 | }
185 | }
186 | if pendingSymbols != [] {
187 | report(node: node, kind: .incomplete(pendingSymbols))
188 | }
189 | return false
190 | }
191 |
192 | return true
193 | }
194 |
195 | let root = tree.rootNode
196 | if root.isError && root.count == 0 {
197 | report(node: root, kind: .empty)
198 | }
199 | else {
200 | _ = walk(root)
201 | }
202 |
203 | return errors
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Sources/TSKit/GrammarPlugin.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// A convenience type for creating loadable grammar bundles.
7 | open class GrammarPlugin {
8 | public init() {
9 | }
10 |
11 | /// The associated grammar type. Override required.
12 | open var grammarType : any Grammar.Type {
13 | fatalError("subclass responsibility")
14 | }
15 |
16 | /// A pairing of names and example strings to be presented in a drop-down. The default implementation returns `[]`.
17 | open var exampleNamesAndStrings : [(name: String, string: String)] {
18 | []
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/TSKit/InputSource.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 |
7 |
8 | /// This protocol serves as an extension on *TSInput* which enables retrieval of source text corresponding to *TSNode* instances.
9 |
10 | public protocol InputSource {
11 | /// Return the portion of the source text for the given byte range, or the empty string if that range is invalid.
12 | func text(in range: Range) -> String
13 |
14 | /// Return the structure required by tree-sitter's parse method.
15 | var tsinput : TSInput { get }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/TSKit/ParsingContext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// Aggregate the data required to translate parse trees into instances of *Parsable* types.
7 |
8 | public struct ParsingContext {
9 | public let inputSource : InputSource
10 |
11 | public init(inputSource s: InputSource) {
12 | inputSource = s
13 | }
14 |
15 | /// Return the portion of source text for the given node.
16 | public func text(for node: TSNode) -> String {
17 | inputSource.text(in: node.sourceByteRange)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/TSKit/Precedence.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | /// An analogue of tree-sitter precedence specifications.
6 | public enum Precedence {
7 | case none(Int)
8 | case left(Int)
9 | case right(Int)
10 | }
11 |
12 |
13 | extension Precedence : ExpressibleByIntegerLiteral {
14 | public init(integerLiteral i: Int)
15 | { self = .none(i) }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/TSKit/ProductionRule.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// The *ProductionRule* type serves as a syntax for specifying production rules for language elements,
7 | /// pairing syntax expressions with instance constructors.
8 | public struct ProductionRule {
9 | /// Specify a production rule for language element `T` as a single syntax expression along
10 | /// a constructor for `T` (i.e. a function taking the captures of the syntax expression and
11 | /// returning an instance of `T`).
12 | public init(_ type: T.Type, _ expression: Expression, constructor f: (repeat each A) throws -> T) { }
13 |
14 | /// Specify a production rule for language element `T` as a choice of syntax expressions
15 | /// along with the corresponding constructors.
16 | public init(_ type: T.Type, choicesByName: [String: Choice]) { }
17 |
18 | /// A pairing of syntax expression and constructor.
19 | public struct Choice {
20 | public init(_ expression: Expression, _ constructor: (repeat each A) throws -> T) { }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TSKit/Punctuation.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | /// A type used to in syntax expressions as a convenience for specifying separated
6 | /// and delimited sequences.
7 | public enum Punctuation {
8 | case sep(String)
9 | case del(String)
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/TSKit/StringInputSource.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 | import TreeSitter
7 |
8 |
9 | /// An InputSource backed by a String.
10 |
11 | public class StringInputSource : InputSource {
12 |
13 | let cstring : (buffer: UnsafePointer, length: Int)
14 | let encoding : String.Encoding
15 |
16 | public init?(string: String, encoding e: String.Encoding = .utf8) {
17 | guard TSInputEncoding(e) != nil else { return nil }
18 | cstring = string.cstringBuffer
19 | encoding = e
20 | }
21 |
22 | deinit {
23 | cstring.buffer.deallocate()
24 | }
25 |
26 | public func text(in range: Range) -> String {
27 | guard range.upperBound <= cstring.length else { return "" }
28 | let ptr = cstring.buffer + Int(range.lowerBound)
29 | let len = Int(range.upperBound - range.lowerBound)
30 | let enc = encoding.rawValue
31 | return NSString(bytes: ptr, length: len, encoding: enc)! as String
32 | }
33 |
34 | public var tsinput : TSInput {
35 | return TSInput(
36 | payload: Unmanaged.passUnretained(self).toOpaque(),
37 | read: { payload, offset, _, count_p -> UnsafePointer? in
38 | let this = Unmanaged.fromOpaque(payload!).takeUnretainedValue()
39 | let offset = min(Int(offset), this.cstring.length)
40 | count_p?.pointee = UInt32(this.cstring.length - offset)
41 | return this.cstring.buffer + offset
42 | },
43 | encoding: .init(encoding)!
44 | )
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/Sources/TSKit/SyntaxError.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 |
7 |
8 | public struct SyntaxError : Error, Equatable {
9 | public enum Kind : Equatable {
10 | case empty
11 | case expecting(Set)
12 | case incomplete([TSSymbol])
13 | case missing(TSSymbol)
14 | case unacceptable
15 | }
16 |
17 | public let range : Range
18 | public let kind : Kind
19 |
20 | public init(range r: Range, kind k: Kind) {
21 | range = r
22 | kind = k
23 | }
24 |
25 | // Describe a syntax error with respect to a given grammar.
26 | public func describe(using _: G) -> String {
27 | switch kind {
28 | case .empty :
29 | return "input text is empty"
30 | case .expecting(let symbols) :
31 | return "expecting \(symbols.sorted().map({G.symbolName(for: $0).description}).joined(separator: " "))"
32 | case .incomplete(let symbols) :
33 | return "incomplete production: \(symbols.map({G.symbolName(for: $0).description}).joined(separator: " "))"
34 | case .missing(let symbol) :
35 | return "missing \(G.symbolName(for: symbol).description)"
36 | case .unacceptable :
37 | return "unacceptable characters"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSFieldMapEntry.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitterCLI
6 |
7 |
8 | extension TSFieldMapEntry {
9 | public init(_ field_id: TSFieldId, _ child_index: UInt8, inherited: Bool = false) {
10 | self.init(field_id: field_id, child_index: child_index, inherited: inherited)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSInput.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 |
7 |
8 | public typealias TSInput = TreeSitter.TSInput
9 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSInputEncoding.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 | import TreeSitter
7 |
8 |
9 | public typealias TSInputEncoding = TreeSitter.TSInputEncoding
10 |
11 |
12 | extension TSInputEncoding {
13 | /// Create an instance from a Swift string encoding, returning *nil* if that encoding is not supported.
14 | public init?(_ encoding: String.Encoding) {
15 | switch encoding {
16 | case .utf8 : self = TSInputEncodingUTF8
17 | case .utf16 : self = TSInputEncodingUTF16
18 | default:
19 | return nil
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSLanguage.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 | @_exported import TreeSitterCLI
7 |
8 |
9 | extension TSLanguage {
10 | /// The built-in symbol for `ERROR`.
11 | public static var errorSymbol : TSSymbol
12 | { .max } // ts_builtin_sym_error
13 |
14 | /// The built-in symbol for `_ERROR`.
15 | public static var errorRepeatSymbol : TSSymbol
16 | { .max - 1 } // ts_builtin_sym_error_repeat
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSLexMode.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitterCLI
6 |
7 |
8 | extension TSLexMode {
9 | public init(lex_state s: UInt16) {
10 | self.init(lex_state: s, external_lex_state: 0)
11 | }
12 |
13 | public static var zero : Self {
14 | .init()
15 | }
16 |
17 | public static var endOfNonTerminalExtra : Self {
18 | .init(lex_state: .max, external_lex_state: .max)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSLookaheadIterator.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 | import TreeSitterCLI
7 |
8 |
9 | /// A wrapper for tree-sitter's *TSLookaheadIterator* type.
10 | public class TSLookaheadIterator {
11 | let ptr : OpaquePointer
12 |
13 | /// Initialize an instance for the given language.
14 | public init(language: UnsafePointer) {
15 | // Note: state 0 indicates end-of-input and is always valid.
16 | ptr = ts_lookahead_iterator_new(language, 0)
17 | }
18 |
19 | /// Initialize an instance for the given language and state. This will fail for invalid states, such as 65535 which is assigned to various nodes in the vicinity of ERROR nodes.
20 | public init?(language: UnsafePointer, state: TSStateId) {
21 | guard let ptr = ts_lookahead_iterator_new(language, state)
22 | else { return nil }
23 | self.ptr = ptr
24 | }
25 |
26 | deinit {
27 | ts_lookahead_iterator_delete(ptr)
28 | }
29 |
30 | public var currentSymbol : TSSymbol {
31 | ts_lookahead_iterator_current_symbol(ptr)
32 | }
33 |
34 | public func next() -> Bool {
35 | ts_lookahead_iterator_next(ptr)
36 | }
37 |
38 | /// Attempt to reset iteration to the given state. Return `true` iff successful.
39 | public func reset(state: TSStateId) -> Bool {
40 | ts_lookahead_iterator_reset_state(ptr, state)
41 | }
42 |
43 | /// Return the list of valid symbols for the parse state given on initialization.
44 | public var validSymbols : AnySequence {
45 | return AnySequence(AnyIterator {
46 | self.next() ? self.currentSymbol : nil
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSNode.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 |
7 |
8 | public typealias TSNode = TreeSitter.TSNode
9 | public typealias TSStateId = TreeSitter.TSStateId
10 |
11 |
12 | extension TSNode
13 | {
14 | /// Return *true* iff the receiver is the 'null' node.
15 | public var isNull : Bool {
16 | ts_node_is_null(self)
17 | }
18 |
19 | /// Return *true* iff the receiver represents a named production (as opposed to a string literal).
20 | public var isNamed : Bool {
21 | ts_node_is_named(self)
22 | }
23 |
24 | /// Return the range of the parsed text represented by the receiver.
25 | public var sourceByteRange : Range {
26 | Int(ts_node_start_byte(self)) ..< Int(ts_node_end_byte(self))
27 | }
28 |
29 | /// Return the parent node, which may be 'null'.
30 | public var parent : TSNode {
31 | ts_node_parent(self)
32 | }
33 |
34 | /// Return the number of child nodes.
35 | public var count : Int {
36 | Int(ts_node_child_count(self))
37 | }
38 |
39 | /// Return the child node at the specified index, which is assumed to be between *0* and *count*.
40 | public subscript (_ index: Int) -> TSNode {
41 | ts_node_child(self, UInt32(index))
42 | }
43 |
44 | /// Return the child node with the given field name.
45 | public subscript (_ name: StaticString) -> TSNode {
46 | name.withUTF8Buffer { buf in
47 | ts_node_child_by_field_name(self, buf.baseAddress, UInt32(buf.count))
48 | }
49 | }
50 |
51 | /// Return the receiver's symbol, ignoring aliases.
52 | public var symbol : TSSymbol {
53 | ts_node_grammar_symbol(self)
54 | }
55 |
56 | /// Return *true* iff the receiver's byte range is empty.
57 | var isEmpty : Bool {
58 | ts_node_start_byte(self) == ts_node_end_byte(self)
59 | }
60 |
61 | /// Return *true* if the receiver represents a syntax error.
62 | public var isError : Bool {
63 | ts_node_is_error(self)
64 | }
65 |
66 | /// Return *true* if the receiver or any of its descendants represents a syntax error.
67 | public var hasError : Bool {
68 | ts_node_has_error(self)
69 | }
70 |
71 | /// Return the node's parse state.
72 | public var state : TSStateId {
73 | ts_node_parse_state(self)
74 | }
75 |
76 | public var nextState : TSStateId {
77 | ts_node_next_parse_state(self)
78 | }
79 | }
80 |
81 |
82 | extension TSNode : CustomStringConvertible
83 | {
84 | public var description : String {
85 | guard let buffer = ts_node_string(self)
86 | else { return "?" }
87 | defer { free(buffer) }
88 | return String(cString: buffer)
89 | }
90 | }
91 |
92 |
93 | extension TSNode : Equatable
94 | {
95 | public static func == (lhs: Self, rhs: Self) -> Bool {
96 | ts_node_eq(lhs, rhs)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSParseActionEntry.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitterCLI
6 |
7 |
8 | extension TSParseActionEntry {
9 | public static func shift(_ state: TSStateId = 0, extra: Bool = false, repeat: Bool = false) -> Self {
10 | .init(action: .init(shift: .init(type: UInt8(TSParseActionTypeShift.rawValue), state: state, extra: extra, repetition: `repeat`)))
11 | }
12 |
13 | public static func reduce(_ symbol: TSSymbol, _ child_count: UInt8, dynamic_precedence: Int16 = 0, production_id: UInt16 = 0) -> Self {
14 | .init(action: .init(reduce: .init(type: UInt8(TSParseActionTypeReduce.rawValue), child_count: child_count, symbol: symbol, dynamic_precedence: dynamic_precedence, production_id: production_id)))
15 | }
16 |
17 | public static var recover : Self {
18 | .init(action: .init(type: UInt8(TSParseActionTypeRecover.rawValue)))
19 | }
20 |
21 | public static var accept : Self {
22 | .init(action: .init(type: UInt8(TSParseActionTypeAccept.rawValue)))
23 | }
24 |
25 | public static func entry(count: UInt8, reusable: Bool) -> Self {
26 | .init(entry: .init(count: count, reusable: reusable))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSParser.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 | import TreeSitterCLI
7 |
8 |
9 | /// A wrapper for the tree-sitter's *TSParser* struct.
10 | public class TSParser
11 | {
12 | public let language : UnsafePointer
13 | let ptr : OpaquePointer
14 |
15 | public init(_ language: UnsafePointer) {
16 | self.language = language
17 | ptr = ts_parser_new()
18 | ts_parser_set_language(ptr, language)
19 | }
20 |
21 | deinit {
22 | ts_parser_delete(ptr)
23 | }
24 |
25 | /// Parse text from the given input source to produce a tree which is optionally an alteration of an existing tree. May return *nil*if parsing was cancelled.
26 | public func parse(_ source: InputSource, existingTree: TSTree? = nil) -> TSTree? {
27 | let opaqueTree = ts_parser_parse(ptr, existingTree?.opaqueTree, source.tsinput)
28 | return TSTree(opaqueTree: opaqueTree)
29 | }
30 |
31 | /// Parse the given string to produce a tree which is optionally an alteration of an existing tree. May return *nil*if parsing was cancelled.
32 | public func parse(_ text: String, existingTree: TSTree? = nil) -> TSTree? {
33 | guard let source = StringInputSource(string: text)
34 | else { return nil }
35 | return parse(source)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSQuery.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 | import TreeSitterCLI
7 | import TSCommon
8 |
9 |
10 | /// A wrapper for tree-sitter's *TSQuery* type.
11 | public class TSQuery {
12 | public let language : UnsafePointer
13 | public let text : String
14 | let ptr : OpaquePointer
15 |
16 | /// Initialize a query for the given language and text, throwing on failure.
17 | public init(language l: UnsafePointer, text t: String) throws {
18 | guard let byteCount = t.utf8.characterCount()
19 | else { throw Exception("failed to calculate text length") }
20 | var errorOffset : UInt32 = 0
21 | var errorType = TSQueryErrorNone
22 | guard let p = t.withCString({ cstr in
23 | ts_query_new(l, cstr, UInt32(byteCount), &errorOffset, &errorType)
24 | }) else {
25 | throw Exception("failed to parse query: error_offset=\(errorOffset); error_type=\(errorType)")
26 | }
27 | language = l
28 | text = t
29 | ptr = p
30 | }
31 |
32 | deinit {
33 | ts_query_delete(ptr)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSQueryCursor.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 |
7 |
8 | public typealias TSQueryCapture = TreeSitter.TSQueryCapture
9 | public typealias TSQueryMatch = TreeSitter.TSQueryMatch
10 |
11 |
12 | /// A wrapper for tree-sitter's *TSQueryCursor* type.
13 | public class TSQueryCursor {
14 | let ptr : OpaquePointer
15 | var state : (query: TSQuery, node: TSNode)?
16 |
17 | public init() {
18 | ptr = ts_query_cursor_new()
19 | }
20 |
21 | public convenience init(query: TSQuery, on node: TSNode) {
22 | self.init()
23 | exec(query: query, on: node)
24 | }
25 |
26 | deinit {
27 | ts_query_cursor_delete(ptr)
28 | }
29 |
30 | public var matchLimit : UInt32 {
31 | get { ts_query_cursor_match_limit(ptr) }
32 | set { ts_query_cursor_set_match_limit(ptr, newValue) }
33 | }
34 |
35 | public var didExceedMatchLimit : Bool {
36 | ts_query_cursor_did_exceed_match_limit(ptr)
37 | }
38 |
39 | public func exec(query: TSQuery, on node: TSNode) {
40 | ts_query_cursor_exec(ptr, query.ptr, node)
41 | }
42 |
43 | /// Return a lazy sequence of the receiver's matches through incremental calls to `ts_query_cursor_next_match`.
44 | public var matches : AnySequence {
45 | let ptr = ptr
46 | var match = TSQueryMatch()
47 | return AnySequence(AnyIterator {
48 | guard ts_query_cursor_next_match(ptr, &match) else { return nil }
49 | return match
50 | })
51 | }
52 |
53 | /// Return a lazy sequence of the receiver's captures through incremental calls to `ts_query_cursor_next_capture`.
54 | public var captures : AnySequence {
55 | let ptr = ptr
56 | var match = TSQueryMatch()
57 | var index = UInt32(0)
58 | return AnySequence(AnyIterator {
59 | guard ts_query_cursor_next_capture(ptr, &match, &index) else { return nil }
60 | assert((0 ..< UInt32(match.capture_count)).contains(index))
61 | return match.captures![Int(index)]
62 | })
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSSymbolMetadata.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitterCLI
6 |
7 |
8 | extension TSSymbolMetadata {
9 | public init(visible v: Bool, named n: Bool) {
10 | self.init(visible: v, named: n, supertype: false)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSSymbolType.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TreeSitter
6 |
7 |
8 | extension TSSymbolType {
9 | public static var regular : Self
10 | { .init(rawValue: 0) }
11 | public static var anonymous : Self
12 | { .init(rawValue: 1) }
13 | public static var auxiliary : Self
14 | { .init(rawValue: 2) }
15 | }
16 |
17 | extension TSSymbolType : CustomStringConvertible {
18 | public var description : String {
19 | switch self {
20 | case .regular : return "regular"
21 | case .anonymous : return "anonymous"
22 | case .auxiliary : return "auxiliary"
23 | default :
24 | return "unknown(\(rawValue))"
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSTree.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 | import TreeSitter
7 |
8 |
9 | /// A wrapper for the tree-sitter's opaque *TSTree*.
10 | public class TSTree
11 | {
12 | let opaqueTree : OpaquePointer
13 |
14 | /// Intended for use by *TSParser* to initialize a new instance given the opaque parse tree.
15 | internal init?(opaqueTree ptr: OpaquePointer?) {
16 | guard let ptr else { return nil }
17 | opaqueTree = ptr
18 | }
19 |
20 | deinit {
21 | ts_tree_delete(opaqueTree)
22 | }
23 |
24 | /// The start node.
25 | public var rootNode : TSNode {
26 | ts_tree_root_node(opaqueTree)
27 | }
28 |
29 | /// Return a list of the byte ranges which have changed from the given previous tree to the receiver.
30 | public func getChangedRanges(from previous: TSTree?) -> [Range] {
31 | var len : UInt32 = 0
32 | guard let buf = ts_tree_get_changed_ranges(opaqueTree, previous?.opaqueTree, &len)
33 | else { print("ts_tree_get_changed_ranges returned nil"); return [] }
34 | defer { free(buf) }
35 | return Array(unsafeUninitializedCapacity: Int(len)) { arr, count in
36 | print("input count is \(count)")
37 | for i in 0 ..< Int(len) {
38 | let tsrange = buf[i]
39 | arr[i] = Range(uncheckedBounds: (Int(tsrange.start_byte), Int(tsrange.end_byte)))
40 | }
41 | count = Int(len)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/TSKit/TSTreeCursor.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Notes:
4 | - ts_tree_cursor_current_field_name is not exposed (say as currentFieldName); instead use currentFieldId and TSLanguage.fieldName(for:).
5 | - an earlier implementation existed as TSCursor
6 |
7 | */
8 |
9 | import TreeSitter
10 |
11 |
12 | /// A wrapper for tree-sitter's *TSTreeCursor* type.
13 | public class TSTreeCursor {
14 | private var _cursor : TreeSitter.TSTreeCursor
15 |
16 | /// Initialize a new instance pointing to the given node.
17 | public init(node: TSNode) {
18 | _cursor = ts_tree_cursor_new(node)
19 | }
20 |
21 | public init(_ other: TSTreeCursor) {
22 | _cursor = ts_tree_cursor_copy(&other._cursor)
23 | }
24 |
25 | deinit {
26 | ts_tree_cursor_delete(&_cursor)
27 | }
28 |
29 | /// The current node, or *nil* if there is none.
30 | public var currentNode : TSNode {
31 | ts_tree_cursor_current_node(&_cursor)
32 | }
33 |
34 | public var currentFieldId : TSFieldId {
35 | ts_tree_cursor_current_field_id(&_cursor)
36 | }
37 |
38 | public func reset(_ node: TSNode) {
39 | ts_tree_cursor_reset(&_cursor, node)
40 | }
41 |
42 | public func reset(to other: TSTreeCursor) {
43 | ts_tree_cursor_reset_to(&_cursor, &other._cursor)
44 | }
45 |
46 | public func gotoParent() -> Bool {
47 | ts_tree_cursor_goto_parent(&_cursor)
48 | }
49 |
50 | public func gotoNextSibling() -> Bool {
51 | ts_tree_cursor_goto_next_sibling(&_cursor)
52 | }
53 |
54 | public func gotoPreviousSibling() -> Bool {
55 | ts_tree_cursor_goto_previous_sibling(&_cursor)
56 | }
57 |
58 | public func gotoFirstChild() -> Bool {
59 | ts_tree_cursor_goto_first_child(&_cursor)
60 | }
61 |
62 | public func gotoLastChild() -> Bool {
63 | ts_tree_cursor_goto_last_child(&_cursor)
64 | }
65 |
66 | public func gotoFirstChild(for byte: UInt32) -> Int64 {
67 | ts_tree_cursor_goto_first_child_for_byte(&_cursor, byte)
68 | }
69 |
70 | public func gotoDescendant(at index: UInt32) {
71 | ts_tree_cursor_goto_descendant(&_cursor, index)
72 | }
73 |
74 | public var currentDescendantIndex : UInt32 {
75 | ts_tree_cursor_current_descendant_index(&_cursor)
76 | }
77 |
78 | public var currentDepth : UInt32 {
79 | ts_tree_cursor_current_depth(&_cursor)
80 | }
81 |
82 | /// Move the cursor to the first leaf descendant of the current node. Return `true` if the cursor moved, or `false` if there was no descendant node.
83 | public func gotoFirstLeafDescendant() -> Bool {
84 | var moved = false
85 | while gotoFirstChild() {
86 | moved = true
87 | }
88 | return moved
89 | }
90 |
91 | public func gotoLastLeafDescendant() -> Bool {
92 | var moved = false
93 | while gotoLastChild() {
94 | moved = true
95 | }
96 | return moved
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/TSKit/Token.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// A restricted class of syntax expression which can be collapsed to a single token.
7 |
8 | public indirect enum Token {
9 | case lit(String)
10 | case pat(Regex)
11 | case seq([Token])
12 | case choice([Token])
13 | }
14 |
15 |
16 | extension Token : ExpressibleByStringLiteral {
17 | public init(stringLiteral s: String) {
18 | self = .lit(s)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/TSMacros/ExpansionError.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 |
7 |
8 | struct ExpansionError : Error {
9 | typealias Issue = (node: any SyntaxProtocol, message: String)
10 | let issues : [Issue]
11 |
12 | init(issues: [Issue]) {
13 | self.issues = issues
14 | }
15 |
16 | init(node: some SyntaxProtocol, message: String) {
17 | issues = [(node, message)]
18 | }
19 |
20 | static func combining(_ fst: Self, _ snd: Self) -> Self {
21 | .init(issues: fst.issues + snd.issues)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/TSMacros/ExpressionRep.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 |
7 |
8 | /// Implements a syntax expression as a pairing of a tree structure and a signature indicating the sequence of captured symbols types.
9 | struct Expression {
10 | // Corresponds to the client-side Expression type.
11 | indirect enum Node {
12 | case lit(String)
13 | case pat(String)
14 | case sym(String)
15 | case alt([String])
16 | case opt(Self)
17 | case rep(Self, Punctuation?=nil)
18 | case seq([Self])
19 | case prec(Precedence, Self)
20 | }
21 |
22 | let rootNode : Node
23 | let signature : Signature
24 |
25 | /// Convert the receiver into a raw expression. Type names in sym cases are translated
26 | /// to symbol names by the given function, and all captures are implicitly wrapped in
27 | /// fields with increasing numeric names.
28 | func getRawExpression(_ lookup: (String) throws -> String) throws -> RawExpression {
29 | var index : Int = 0
30 | func walk(_ expr: Node) throws -> RawExpression {
31 | switch expr {
32 | case .lit(let string) :
33 | .string(string)
34 | case .pat(let pattern) :
35 | .field("\(index.postincrement())", .pattern(pattern))
36 | case .sym(let typeName) :
37 | .field("\(index.postincrement())", .symbol(try lookup(typeName)))
38 | case .alt(let strings) :
39 | .field("\(index.postincrement())", .choice(strings.map {.string($0)}))
40 | case .opt(let expr) :
41 | .choice([try walk(expr), .blank])
42 | case .rep(let expr, .none) :
43 | {.seq([$0, .repeat($0)])}(try walk(expr))
44 | case .rep(let expr, .sep(let sep)) :
45 | {.seq([$0, .repeat(.seq([.string(sep), $0]))])}(try walk(expr))
46 | case .rep(let expr, .del(let del)) :
47 | {.seq([$0, .repeat($0)])}(.seq([try walk(expr), .string(del)]))
48 | case .seq(let exprs) :
49 | .seq(try exprs.map({try walk($0)}))
50 | case .prec(let prec, let expr) :
51 | .prec(prec, try walk(expr))
52 | }
53 | }
54 | return try walk(rootNode)
55 | }
56 | }
57 |
58 |
59 | extension Expression {
60 | struct Options : OptionSet {
61 | let rawValue : Int
62 | // Note: explicitly hidden symbols cannot appear within seq or rep expressions because because their tree structure is not maintained by a child node.
63 | static let disallowHiddenSymbols = Self(rawValue: 1 << 0)
64 | }
65 |
66 | /// Attempt to create an instance from a Swift syntax expression.
67 | init(exprSyntax: ExprSyntax, options: Options = []) throws {
68 | switch exprSyntax.kind {
69 | case .functionCallExpr :
70 | let (name, args) = try exprSyntax.caseComponents
71 | switch (name, args.count) {
72 | case ("lit", 1) :
73 | guard let string = args[0].expression.as(StringLiteralExprSyntax.self)?.stringLiteral
74 | else { throw ExpansionError(node: args[0].expression, message: "expecting string literal") }
75 | self = .init(rootNode: .lit(string), signature: .empty)
76 | case ("pat", 1) :
77 | guard let string = args[0].expression.as(StringLiteralExprSyntax.self)?.stringLiteral
78 | else { throw ExpansionError(node: args[0].expression, message: "expecting string literal") }
79 | self = .init(rootNode: .pat(string), signature: .string)
80 | case ("sym", 1) :
81 | guard let name = try args[0].expression.typeName
82 | else { throw ExpansionError(node: args[0].expression, message: "expecting type reference") }
83 | guard !options.contains(.disallowHiddenSymbols) || name.hasPrefix("_") == false
84 | else { throw ExpansionError(node: args[0].expression, message: "hidden symbols cannot appear in this context") }
85 | self = .init(rootNode: .sym(name), signature: .symbol(name))
86 | case ("alt", 1) :
87 | guard let arrayExpr = args[0].expression.as(ArrayExprSyntax.self)
88 | else { throw ExpansionError(node: args[0].expression, message: "expecting array literal") }
89 | let literals = try arrayExpr.elements.map {
90 | guard let string = $0.expression.as(StringLiteralExprSyntax.self)?.stringLiteral
91 | else { throw ExpansionError(node: $0.expression, message: "expecting string literal") }
92 | return string
93 | }
94 | self = .init(rootNode: .alt(literals), signature: .string)
95 | case ("opt", 1) :
96 | let subexpr = try Self(exprSyntax: args[0].expression, options: options)
97 | let sig = switch subexpr.signature {
98 | case .symbol(let name) : Signature.tuple([(name, true)])
99 | case .tuple(let captures) : Signature.tuple(captures.map {($0.symbol, true)})
100 | default :
101 | throw ExpansionError(node: args[0].expression, message: "unsupported expression")
102 | }
103 | self = .init(rootNode: .opt(subexpr.rootNode), signature: sig)
104 | case ("rep", let n) where 0 < n && n < 3 :
105 | let subexpr = try Self(exprSyntax: args[0].expression, options: options.union(.disallowHiddenSymbols))
106 | guard case .symbol(let symbolName) = subexpr.signature
107 | else { throw ExpansionError(node: args[0].expression, message: "unsupported expression") }
108 | let punc = n == 2 ? try Punctuation(exprSyntax: args[1].expression) : nil
109 | self = .init(rootNode: .rep(subexpr.rootNode, punc), signature: .array(symbolName, punc))
110 | case ("seq", 1) :
111 | guard let arrayExpr = args[0].expression.as(ArrayExprSyntax.self)
112 | else { throw ExpansionError(node: args[0].expression, message: "expecting array literal") }
113 | let (subtrees, captures) : ([Node], [Signature.Capture]) = try arrayExpr.elements.enumerated().reduce(into: ([],[])) { accum, elt in
114 | let eltexp = try Self(exprSyntax: elt.1.expression, options: options.union(.disallowHiddenSymbols))
115 | switch eltexp.signature {
116 | case .symbol(let name) : accum.1 += [(name, false)]
117 | case .tuple(let elts) : accum.1 += elts
118 | default :
119 | throw ExpansionError(node: elt.1, message: "unsupported expression")
120 | }
121 | accum.0 += [eltexp.rootNode]
122 | }
123 | self = .init(rootNode: .seq(subtrees), signature: .tuple(captures))
124 | case ("prec", 2) :
125 | let prec = try Precedence(exprSyntax: args[0].expression)
126 | let subexpr = try Self(exprSyntax: args[1].expression, options: options)
127 | self = .init(rootNode: .prec(prec, subexpr.rootNode), signature: subexpr.signature)
128 | case let other :
129 | throw ExpansionError(node: args[0].expression, message: "unsupported case: \(other)")
130 | }
131 | case .stringLiteralExpr :
132 | guard let string = exprSyntax.cast(StringLiteralExprSyntax.self).stringLiteral
133 | else { throw ExpansionError(node: exprSyntax, message: "expecting string literal") }
134 | self = .init(rootNode: .lit(string), signature: .empty)
135 | case let unsupported :
136 | throw ExpansionError(node: exprSyntax, message: "unsupported expression syntax: \(unsupported)")
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/TSMacros/Extensions/Foundation-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 |
7 |
8 | extension Character {
9 | /// Return the unicode scalar representation, or nil if the character is composed of multiple unicode scalars.
10 | public var unicodeScalar : Unicode.Scalar? {
11 | unicodeScalars.count == 1 ? unicodeScalars[unicodeScalars.startIndex] : nil
12 | }
13 | }
14 |
15 |
16 | extension CharacterSet {
17 | /// Return true iff the receiver contains the given character, which must be represented by a single unicode scalar.
18 | public func contains(_ character: Character) -> Bool {
19 | guard let unicodeScalar = character.unicodeScalar else { return false }
20 | return self.contains(unicodeScalar)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/TSMacros/Extensions/Swift-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Methods added to various types from the Swift standard library.
4 |
5 | */
6 |
7 | import SwiftSyntax
8 |
9 |
10 | extension BinaryInteger {
11 | public mutating func postincrement() -> Self {
12 | defer { self += 1 }
13 | return self
14 | }
15 | }
16 |
17 | // MARK: -
18 |
19 | extension Collection {
20 | public var only : Element? {
21 | count == 1 ? first : nil
22 | }
23 | }
24 |
25 | // MARK: -
26 |
27 | extension Int {
28 | init(exprSyntax: ExprSyntax) throws {
29 | guard let literal = exprSyntax.as(IntegerLiteralExprSyntax.self)?.literal
30 | else { throw ExpansionError(node: exprSyntax, message: "expecting integer literal") }
31 | guard let intValue = Self(literal.text)
32 | else { throw ExpansionError(node: exprSyntax, message: "failed to interpret integer literal: \(literal)") }
33 | self = intValue
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/TSMacros/Extensions/SwiftSyntax-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Methods added to types defined in SwiftSyntax.
4 |
5 | */
6 |
7 | import SwiftSyntax
8 |
9 |
10 | extension ClosureSignatureSyntax {
11 | public var parameterCount : Int {
12 | switch parameterClause {
13 | case .none :
14 | return 0
15 | case .simpleInput(let shorthandParameterList) :
16 | return shorthandParameterList.count
17 | case .parameterClause(let parameterClause) :
18 | return parameterClause.parameters.count
19 | }
20 | }
21 | }
22 |
23 |
24 | // MARK: -
25 |
26 | extension DeclGroupSyntax {
27 | public var visibility : String {
28 | let options : Set = ["public", "internal", "private", "fileprivate"]
29 | return modifiers.map({$0.name.text})
30 | .filter({options.contains($0)})
31 | .joined(separator: " ")
32 | }
33 |
34 | public func aliasType(for name: String) -> TypeSyntax? {
35 | memberBlock.members
36 | .compactMap({$0.decl.as(TypeAliasDeclSyntax.self)})
37 | .filter({$0.name.text == name})
38 | .only?.initializer.value
39 | }
40 |
41 | public func variableBindingWith(name: String, type: TypeSyntax, isStatic: Bool = false) -> PatternBindingSyntax? {
42 | memberBlock.members
43 | .compactMap({$0.decl.as(VariableDeclSyntax.self)})
44 | .filter({$0.isStatic == isStatic})
45 | .compactMap({$0.bindings.only})
46 | .filter({
47 | guard let identifier = $0.pattern.as(IdentifierPatternSyntax.self)?.identifier else { return false }
48 | return identifier.text == name
49 | })
50 | .filter({
51 | guard let annotation = $0.typeAnnotation else { return false }
52 | return annotation.type == type
53 | })
54 | .first
55 | }
56 | }
57 |
58 | extension ExprSyntax {
59 | var caseComponents : (name: String, args: LabeledExprListSyntax) {
60 | get throws {
61 | guard let call = self.as(FunctionCallExprSyntax.self)
62 | else { throw ExpansionError(node: self, message: "expecting function call") }
63 | guard let memberAccess = call.calledExpression.as(MemberAccessExprSyntax.self)
64 | else { throw ExpansionError(node: call, message: "expecting member access") }
65 | guard memberAccess.base == nil
66 | else { throw ExpansionError(node: memberAccess, message: "expecting baseless member access") }
67 | return (memberAccess.declName.baseName.text, call.arguments)
68 | }
69 | }
70 |
71 | // Get the produced type from the first argument, which must be of the form `T.self`
72 | var typeName : String? {
73 | get throws {
74 | guard let memberAccess = self.as(MemberAccessExprSyntax.self)
75 | else { throw ExpansionError(node: self, message: "expecting member access") }
76 | guard let baseRef = memberAccess.base
77 | else { throw ExpansionError(node: memberAccess, message: "expecting type base for member access") }
78 | guard memberAccess.declName.baseName.text == "self"
79 | else { throw ExpansionError(node: memberAccess.declName.baseName, message: "accessed member must be 'self'") }
80 | guard let declRef = baseRef.as(DeclReferenceExprSyntax.self)
81 | else { throw ExpansionError(node: baseRef, message: "type argument must be a declaration reference") }
82 | return declRef.baseName.text
83 | }
84 | }
85 | }
86 |
87 | // MARK: -
88 |
89 | extension FunctionDeclSyntax {
90 | /// Return *true* if the receiver's modifiers contain 'static'.
91 | public var isStatic : Bool {
92 | modifiers.contains {$0.name.text == "static"}
93 | }
94 | }
95 |
96 | // MARK: -
97 |
98 | extension PatternBindingSyntax {
99 | /// Return the expression returned by the defined getter. Return nil if the receiver does not have a single getter or if its code block is empty.
100 | public var resultExpr : ExprSyntax? {
101 | guard case .getter(let codeBlockItemList) = accessorBlock?.accessors
102 | else { return nil }
103 | guard let returnItem = codeBlockItemList.last?.item
104 | else { return nil }
105 | return returnItem.kind == .returnStmt
106 | ? returnItem.cast(ReturnStmtSyntax.self).expression
107 | : returnItem.as(ExprSyntax.self)
108 | }
109 | }
110 |
111 | // MARK: -
112 |
113 | extension StringLiteralExprSyntax {
114 | var stringLiteral : String? {
115 | guard let stringSegment = segments.first?.as(StringSegmentSyntax.self), segments.count == 1
116 | else { return nil }
117 | return stringSegment.trimmedDescription
118 | }
119 | }
120 |
121 | // MARK: -
122 |
123 | extension SyntaxCollection {
124 | subscript (_ offset: Int) -> Element {
125 | self[index(startIndex, offsetBy: offset)]
126 | }
127 | }
128 |
129 | // MARK: -
130 |
131 | extension TypeSyntax {
132 | var text : String {
133 | trimmed.description
134 | }
135 |
136 | /// Structual equality for type syntax expressions. Partial.
137 | static func == (lhs: Self, rhs: Self) -> Bool {
138 | guard lhs.kind == rhs.kind else { return false }
139 | switch lhs.kind {
140 | case .arrayType :
141 | return lhs.cast(ArrayTypeSyntax.self).element == rhs.cast(ArrayTypeSyntax.self).element
142 | case .dictionaryType :
143 | return lhs.cast(DictionaryTypeSyntax.self).key == rhs.cast(DictionaryTypeSyntax.self).key
144 | && lhs.cast(DictionaryTypeSyntax.self).value == rhs.cast(DictionaryTypeSyntax.self).value
145 | case .identifierType :
146 | return lhs.cast(IdentifierTypeSyntax.self).name.text == rhs.cast(IdentifierTypeSyntax.self).name.text
147 | case .optionalType :
148 | return lhs.cast(OptionalTypeSyntax.self).wrappedType == rhs.cast(OptionalTypeSyntax.self).wrappedType
149 | case .attributedType, .classRestrictionType, .compositionType, .functionType, .implicitlyUnwrappedOptionalType, .memberType, .metatypeType, .missingType, .namedOpaqueReturnType, .packElementType, .packExpansionType, .someOrAnyType, .suppressedType, .tupleType:
150 | fallthrough
151 | default :
152 | return false
153 | }
154 | }
155 |
156 | static func == (lhs: Self, rhs: String) -> Bool {
157 | lhs.trimmed.description == rhs
158 | }
159 | }
160 |
161 | // MARK: -
162 |
163 | extension VariableDeclSyntax {
164 | /// Return *true* if the receiver's modifiers contain 'static'.
165 | public var isStatic : Bool {
166 | modifiers.contains {$0.name.text == "static"}
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/Sources/TSMacros/GrammarMacro.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 | import SwiftSyntaxMacros
7 | import TSCommon
8 |
9 |
10 | public struct GrammarMacro : MemberMacro {
11 | public static func expansion(of node: AttributeSyntax, providingMembersOf decl: some DeclGroupSyntax, in ctx: some MacroExpansionContext) throws -> [DeclSyntax] {
12 | guard let decl = decl.as(StructDeclSyntax.self)
13 | else { throw ExpansionError(node: decl, message: "applicable only to struct declarations") }
14 |
15 | do {
16 | // Create a representative of the declared struct.
17 | let grammar = try GrammarRep.from(structDecl: decl)
18 |
19 | // Return the declarations for the generated methods...
20 | return grammar.definedRules.map {
21 | DeclSyntax(stringLiteral: $0.extractionDeclText)
22 | } + [
23 | DeclSyntax(stringLiteral: """
24 | \(grammar.visibility) static func translate(parseTree node: TSNode, in context: ParsingContext) -> Root {
25 | extract\(grammar.rootType)(from: node, in: context)
26 | }
27 | """),
28 | DeclSyntax(stringLiteral: """
29 | \(grammar.visibility) static func isRuleHidden(for type: Any.Type) -> Bool {
30 | let hiddenTypes : Set = [
31 | \(grammar.definedRules.compactMap({$0.isSymbolHidden ? ".init(\($0.typeName).self)" : nil})
32 | .joined(separator: ", "))
33 | ]
34 | return hiddenTypes.contains(ObjectIdentifier(type))
35 | }
36 | """),
37 | DeclSyntax(stringLiteral: """
38 | \(grammar.visibility) static func representative(for symbol: TSSymbol) -> TSSymbol {
39 | let mapped : [TSSymbol: TSSymbol] = [
40 | \(grammar.mappedSymbolPairs.map { "sym_\($0): sym_\($1)" }
41 | .joined(separator: ",\n")
42 | )
43 | ]
44 | return mapped[symbol] ?? symbol
45 | }
46 | """),
47 | DeclSyntax(stringLiteral: """
48 | \(grammar.visibility) static func symbol(for type: Any.Type) -> TSSymbol? {
49 | let mapped : [ObjectIdentifier: TSSymbol] = [
50 | \(grammar.definedRules.map { ".init(\($0.typeName).self): sym_\($0.symbolName)," }
51 | .joined(separator: "\n")
52 | )
53 | ]
54 | return mapped[.init(type)]
55 | }
56 | """),
57 | DeclSyntax(stringLiteral:
58 | grammar.languageSource
59 | ),
60 | ]
61 | }
62 | catch let error as ExpansionError {
63 | for issue in error.issues {
64 | ctx.addDiagnostics(from: Exception(issue.message), node: issue.node)
65 | }
66 | return Self.missingImplementations(with: decl.visibility)
67 | }
68 | catch {
69 | ctx.addDiagnostics(from: error, node: decl)
70 | return Self.missingImplementations(with: decl.visibility)
71 | }
72 | }
73 |
74 |
75 | // Return stubs for generated requirements to avoid non-conformance error on failed expansion...
76 | private static func missingImplementations(with visibility: String) -> [DeclSyntax] {
77 | let texts : [String] = [
78 | "\(visibility) static var language : UnsafePointer { fatalError() }",
79 | "\(visibility) static var symbolNames : [StaticString] { [] }",
80 | "\(visibility) static func translate(parseTree node: TSNode, in context: ParsingContext) -> Root { fatalError() }",
81 | "\(visibility) static func isRuleHidden(for type: Any.Type) -> Bool { false }",
82 | ]
83 | return texts.map { DeclSyntax(stringLiteral: $0) }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/TSMacros/GrammarRep.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 | import TSCommon
7 |
8 |
9 | struct GrammarRep {
10 | // The type name of the declaration given on initialization.
11 | let name : String
12 | // The struct access modifier
13 | let visibility : String
14 | // The name of the 'Root' typealias, indicating the top-level language element.
15 | let rootType : String
16 | // The defined production rules.
17 | let definedRules : [ProductionRule]
18 | // The JSON grammar representation
19 | let jsonText : String
20 | // The generated text defining the TSLanguage structure
21 | let languageSource : String
22 |
23 |
24 | // Initialize an instance from a struct declaration which conforms to the Grammar protocol.
25 | static func from(structDecl decl: StructDeclSyntax) throws -> Self {
26 | let name = decl.name.text
27 | let visibility = decl.visibility
28 |
29 | // Get the name of the Root typealias.
30 | let rootType = try decl.aliasType(for: "Root")?.text
31 | ?? { throw ExpansionError(node: decl, message: "missing typealias for `Root`") }()
32 |
33 | // Get the pattern for the 'word' symbol, if any.
34 | let word : (symbol: String, pattern: String)?
35 | if let wordBinding = decl.variableBindingWith(name: "word", type: "String?", isStatic: true) {
36 | guard let pattern = wordBinding.resultExpr?.as(StringLiteralExprSyntax.self)?.stringLiteral
37 | else { throw ExpansionError(node: wordBinding, message: "`word` must return a string literal") }
38 | word = ("Word", pattern)
39 | }
40 | else {
41 | word = nil
42 | }
43 |
44 | // Get the list of tokens for the 'extras' property, if any.
45 | let extras : [RawExpression]?
46 | if let extrasBinding = decl.variableBindingWith(name: "extras", type: "[Token]", isStatic: true) {
47 | guard let array = extrasBinding.resultExpr?.as(ArrayExprSyntax.self)
48 | else { throw ExpansionError(node: extrasBinding, message: "`extras` must return an array literal") }
49 | extras = try array.elements.map { element in
50 | let token = try Token(expr: element.expression)
51 | return token.rawExpression
52 | }
53 | }
54 | else {
55 | extras = nil
56 | }
57 |
58 | // Get the list of production rules from `static var productionRules : [ProductionRule]`,
59 | // which must be an array literal of calls to the ProductionRule init method.
60 | guard let rulesBinding = decl.variableBindingWith(name: "productionRules", type: "[ProductionRule]", isStatic: true)
61 | else { throw ExpansionError(node: decl, message: "missing static var `productionRules`") }
62 | guard let arrayExpr = rulesBinding.resultExpr?.as(ArrayExprSyntax.self)
63 | else { throw ExpansionError(node: rulesBinding, message: "`productionRules` must return an array literal") }
64 | let definedRules = try arrayExpr.elements.map({try ProductionRule($0.expression)})
65 |
66 | // Define a mapping of type names to symbol names for those production rules, along with
67 | // a translation method which throws for undefined types.
68 | let symbolsByType = try Dictionary(definedRules.map {($0.typeName, $0.symbolName)}) {t, _ in throw ExpansionError(node: rulesBinding, message: "multiple rules for '\(t)'")}
69 | func symbolLookup(_ typeName: String) throws -> String {
70 | try symbolsByType[typeName] ?? { throw ExpansionError(node: rulesBinding, message: "missing rule for type '\(typeName)'") }()
71 | }
72 |
73 | // Form a list of all production rules
74 | let allRules = [("start", RawExpression.symbol(try symbolLookup(rootType)))]
75 | + (try definedRules.flatMap({try $0.getSymbolNamesAndRawExpressions(symbolLookup(_:))}))
76 | + (word.map {[($0.symbol, RawExpression.pattern($0.pattern))]} ?? [])
77 |
78 | // Construct the JSON representation of the grammar for tree-sitter
79 | let jsonText =
80 | """
81 | {
82 | "name": "\(name)",
83 | "rules": {
84 | \(
85 | allRules
86 | .map({"\"\($0)\": " + $1.json})
87 | .joined(separator: ",\n ")
88 | )
89 | },
90 | \(word.map {"\"word\": \"\($0.symbol)\","} ?? "")
91 | "extras": [\(extras.map {$0.map(\.json).joined(separator: ", ")} ?? RawExpression.whitespace.json)],
92 | "conflicts": [],
93 | "precedences": [],
94 | "externals": [],
95 | "inline": [],
96 | "supertypes": []
97 | }
98 | """
99 |
100 | // Call tree-sitter generate
101 | let languageSource = try generateParserSource(for: jsonText, accessModifier: visibility)
102 |
103 | return Self(
104 | name: name,
105 | visibility: visibility,
106 | rootType: rootType,
107 | definedRules: definedRules,
108 | jsonText: jsonText,
109 | languageSource: languageSource
110 | )
111 | }
112 |
113 | /// The mapping of each 'case' symbol name to its generalization.
114 | var mappedSymbolPairs : some Sequence<(String, String)> {
115 | var pairs : [(String, String)] = []
116 | for rule in definedRules {
117 | guard case .multiple(let choicesByName) = rule.form
118 | else { continue }
119 | for name in choicesByName.keys {
120 | pairs.append((rule.alternateSymbolName(for: name), rule.symbolName))
121 | }
122 | }
123 | return pairs
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Sources/TSMacros/Plugin.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftCompilerPlugin
6 | import SwiftSyntaxMacros
7 |
8 |
9 | @main
10 | struct MacroPlugin : CompilerPlugin
11 | {
12 | let providingMacros: [Macro.Type] = [
13 | GrammarMacro.self,
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/TSMacros/PrecedenceRep.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 |
7 |
8 | enum Precedence {
9 | case none(Int)
10 | case left(Int)
11 | case right(Int)
12 |
13 | var jsonType : String {
14 | switch self {
15 | case .none : "PREC"
16 | case .left : "PREC_LEFT"
17 | case .right : "PREC_RIGHT"
18 | }
19 | }
20 |
21 | var jsonValue : Int {
22 | switch self {
23 | case .none(let v) : v
24 | case .left(let v) : v
25 | case .right(let v) : v
26 | }
27 | }
28 | }
29 |
30 |
31 | extension Precedence {
32 | /// Attempt to create an instance from a Swift syntax expression.
33 | init(exprSyntax: ExprSyntax) throws {
34 | switch exprSyntax.kind {
35 | case .functionCallExpr :
36 | let (name, args) = try exprSyntax.caseComponents
37 | switch (name, args.count) {
38 | case ("left", 1) :
39 | self = .left(try Int(exprSyntax: args[0].expression))
40 | case ("right", 1) :
41 | self = .right(try Int(exprSyntax: args[0].expression))
42 | default :
43 | throw ExpansionError(node: exprSyntax, message: "unsupported Precedence syntax")
44 | }
45 | case .integerLiteralExpr :
46 | self = .none(try Int(exprSyntax: exprSyntax))
47 | default :
48 | throw ExpansionError(node: exprSyntax, message: "unsupported Precedence syntax")
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/TSMacros/ProductionRuleRep.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 | import SwiftSyntaxMacros
7 |
8 |
9 | /// *ProductionRule* is the 'implementation' of the client-side type of the same name.
10 | struct ProductionRule {
11 | /// *Form* distinguishes between production rules which have single vs multiple expressions;
12 | /// note that the latter determines multiple rules in the generated grammar.
13 | enum Form {
14 | case single(Choice), multiple([String: Choice])
15 | }
16 |
17 | /// *Choice* pairs a syntax expression and a constructor with shared type signature.
18 | struct Choice {
19 | let expression : Expression
20 | let constructor : ClosureExprSyntax
21 |
22 | /// Attempt to create an instance from the given expression and closure; throw if
23 | /// either:
24 | /// - the closure does not have explicit parameter
25 | /// - the form of expression is not supported
26 | /// - the expression capture count does not match the constructor parameter count.
27 | init(expression e: Expression, constructor c: ClosureExprSyntax) throws {
28 | guard let parameterCount = c.signature?.parameterCount
29 | else { throw ExpansionError(node: c, message: "closures must have explicit parameter names") }
30 | guard e.signature.captureCount == parameterCount
31 | else { throw ExpansionError(node: c, message: "constructor parameter count (\(parameterCount)) does not match expression capture count (\(e.signature.captureCount))") }
32 | expression = e
33 | constructor = c
34 | }
35 |
36 | /// Return the text for an invocation of the constructor with argument values extracted
37 | /// from an implicit parse tree 'node' and 'context'...
38 | var invocationText : String {
39 | let invocationArgumentsText = switch expression.signature {
40 | case .symbol(let type) :
41 | extractionText(for: type)
42 | case .array(let eltType, .sep(let sep)) :
43 | "Array<\(eltType)>.extractElements(separatedBy: \"\(sep)\", from: node, in: context, using: {\(extractionText(for: eltType, with: "$0", in: "$1"))})"
44 | case .array(let eltType, .del(let del)) :
45 | "Array<\(eltType)>.extractElements(delimitedBy: \"\(del)\", from: node, in: context, using: {\(extractionText(for: eltType, with: "$0", in: "$1"))})"
46 | case .array(let eltType, .none) :
47 | "Array<\(eltType)>.extractElements(from: node, in: context, using: {\(extractionText(for: eltType, with: "$0", in: "$1"))})"
48 | case .tuple(let captures) :
49 | captures.enumerated()
50 | .map({ index, capture in
51 | let nodeText = "node[\"\(index)\"]"
52 | return capture.optional
53 | ? "{$0.isNull ? nil : \(extractionText(for: capture.symbol, with: "$0"))}(\(nodeText))"
54 | : extractionText(for: capture.symbol, with: nodeText)
55 | })
56 | .joined(separator: ", ")
57 | }
58 | return "\(constructor)(\(invocationArgumentsText))"
59 | }
60 |
61 | /// Return the text used to create an instance of the given type from the given node.
62 | func extractionText(for typeName: String, with nodeText: String = "node", in contextText: String = "context") -> String {
63 | "extract\(typeName)(from: \(nodeText), in: \(contextText))"
64 | }
65 | }
66 |
67 |
68 | /// The name of the associated language element type.
69 | let typeName : String
70 | /// The set of pairs of syntax expressions and constructors.
71 | let form : Form
72 |
73 |
74 | /// Return the text which defines the function to extract an instance of the associated type from a parse tree node and context.
75 | var extractionDeclText : String {
76 | """
77 | private static func extract\(typeName)(from node: TSNode, in context: ParsingContext) -> \(typeName) {
78 | \(extractionBodyText)
79 | }
80 | """
81 | }
82 |
83 | /// Indicates whether or not the associated symbol is hidden.
84 | var isSymbolHidden : Bool {
85 | if case .multiple = form { true } else { false }
86 | }
87 |
88 | /// Returns the symbol name for the associated type.
89 | var symbolName : String {
90 | isSymbolHidden ? "_" + typeName : typeName
91 | }
92 |
93 | /// Return the body text for the method which extracts an instance of the associated
94 | /// type from a given parse tree 'node' and 'context'.
95 | var extractionBodyText : String {
96 | switch form {
97 | case .single(let choice) :
98 | choice.invocationText
99 | case .multiple(let choicesByName) :
100 | """
101 | switch node.symbol {
102 | \(
103 | choicesByName.map({ key, choice in
104 | "case sym_\(alternateSymbolName(for: key)) : \(choice.invocationText)"
105 | })
106 | .joined(separator: "\n")
107 | )
108 | case let other:
109 | fatalError("unexpected symbol: \\(other)")
110 | }
111 | """
112 | }
113 | }
114 |
115 | /// Assuming the receiver has multiple choices, return the symbol name for the given choice name.
116 | func alternateSymbolName(for choiceName: String) -> String {
117 | typeName + "_" + choiceName
118 | }
119 |
120 | /// Return the implied grammar rules as a list of pairs of symbol names and raw syntax expressions.
121 | func getSymbolNamesAndRawExpressions(_ lookup: (String) throws -> String) throws -> [(symbolName: String, rawExpression: RawExpression)] {
122 | return switch form {
123 | case .single(let choice) :
124 | [(symbolName, try choice.expression.getRawExpression(lookup))]
125 | case .multiple(let choicesByName) :
126 | [(symbolName, .choice(choicesByName.map({name, _ in .symbol(alternateSymbolName(for: name))})))]
127 | + (try choicesByName.map({ name, choice in (alternateSymbolName(for: name), try choice.expression.getRawExpression(lookup)) }))
128 | }
129 | }
130 | }
131 |
132 |
133 | extension ProductionRule {
134 | /// Attempt to create an instance from a Swift syntax expression.
135 | init(_ expr: ExprSyntax) throws {
136 | guard let funcall = expr.as(FunctionCallExprSyntax.self), funcall.arguments.count == 2
137 | else { throw ExpansionError(node: expr, message: "expecting function call with two arguments") }
138 |
139 | // Get the produced type from the first argument, which must be of the form `T.self`
140 | guard let name = try funcall.arguments[0].expression.typeName
141 | else { throw ExpansionError(node: funcall.arguments[0].expression, message: "expecting type reference") }
142 | typeName = name
143 |
144 | // Distinguish single and multiple clause initializers
145 | let arg2 = funcall.arguments[1]
146 | switch arg2.label?.text {
147 | case .none : // init(_ type: T.Type, _ expression: Expression, constructor f: (repeat each A) throws -> T)
148 | guard let closure = funcall.trailingClosure
149 | else { throw ExpansionError(node: funcall, message: "expecting trailing closure") }
150 | form = .single(try Choice(expression: try Expression(exprSyntax: arg2.expression), constructor: closure))
151 | case "choicesByName" : // init(_ type: T.Type, choicesByName: [String: Choice])
152 | guard let dict = arg2.expression.as(DictionaryExprSyntax.self), case .elements(let elements) = dict.content
153 | else { throw ExpansionError(node: arg2.expression, message: "'syntaxExpressionsByCaseName' must return a non-empty dictionary") }
154 | let namesWithClauses = try elements
155 | .map({ element in
156 | guard let name = element.key.as(StringLiteralExprSyntax.self)?.stringLiteral
157 | else { throw ExpansionError(node: element.key, message: "dictionary keys must be string literals") }
158 | guard let funcall = element.value.as(FunctionCallExprSyntax.self), funcall.arguments.count == 1
159 | else { throw ExpansionError(node: element.value, message: "expecting a single-argument function call") }
160 | guard let closure = funcall.trailingClosure
161 | else { throw ExpansionError(node: funcall, message: "expecting a trailing closure") }
162 | let expr = try Expression(exprSyntax: funcall.arguments[0].expression)
163 | return (name, try Choice(expression: expr, constructor: closure))
164 | })
165 | form = .multiple(Dictionary(uniqueKeysWithValues: namesWithClauses))
166 | case .some(let other) :
167 | throw ExpansionError(node: arg2, message: "invalid argument label: \(other)")
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/Sources/TSMacros/PunctuationRep.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 |
7 |
8 | enum Punctuation {
9 | case sep(String)
10 | case del(String)
11 | }
12 |
13 |
14 | extension Punctuation {
15 | /// Attempt to create an instance from a Swift syntax expression.
16 | init(exprSyntax: ExprSyntax) throws {
17 | let (name, args) = try exprSyntax.caseComponents
18 | guard args.count == 1, let string = args[0].expression.as(StringLiteralExprSyntax.self)?.stringLiteral
19 | else { throw ExpansionError(node: args[0].expression, message: "expecting one string literal argument") }
20 | switch (name, args.count) {
21 | case ("sep", 1) :
22 | self = .sep(string)
23 | case ("del", 1) :
24 | self = .del(string)
25 | default :
26 | throw ExpansionError(node: exprSyntax, message: "unsupported Punctuation syntax")
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/TSMacros/RawExpression.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// Corresponds to the structure of rules in tree-sitter's grammar.json.
7 | indirect enum RawExpression {
8 | case blank
9 | case string(String)
10 | case pattern(String)
11 | case symbol(String)
12 | case seq([RawExpression])
13 | case choice([RawExpression])
14 | case field(String, RawExpression)
15 | case prec(Precedence, RawExpression)
16 | case `repeat`(RawExpression)
17 |
18 | static var whitespace : Self {
19 | .pattern("\\\\s")
20 | }
21 |
22 | var json : String {
23 | switch self {
24 | case .blank : """
25 | {"type": "BLANK"}
26 | """
27 | case .string(let value) : """
28 | {"type": "STRING", "value": "\(value)"}
29 | """
30 | case .pattern(let value) : """
31 | {"type": "PATTERN", "value": "\(value)"}
32 | """
33 | case .symbol(let name) : """
34 | {"type": "SYMBOL", "name": "\(name)"}
35 | """
36 | case .choice(let members) : """
37 | {"type": "CHOICE", "members": [\(members.map(\.json).joined(separator: ", "))]}
38 | """
39 | case .seq(let members) : """
40 | {"type": "SEQ", "members": [\(members.map(\.json).joined(separator: ", "))]}
41 | """
42 | case .prec(let prec, let content) : """
43 | {"type": "\(prec.jsonType)", "value": \(prec.jsonValue), "content": \(content.json)}
44 | """
45 | case .field(let name, let content) : """
46 | {"type": "FIELD", "name": "\(name)", "content": \(content.json)}
47 | """
48 | case .repeat(let content) : """
49 | {"type": "REPEAT", "content": \(content.json)}
50 | """
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/TSMacros/Signature.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 |
6 | /// *Signature* represents the 'type signature' of a syntax expression as an array of
7 | /// type names with an indication of optionality.
8 | enum Signature {
9 | typealias Capture = (symbol: String, optional: Bool)
10 |
11 | case symbol(String)
12 | case array(String, Punctuation?)
13 | case tuple([Capture])
14 |
15 | static var empty : Self
16 | { .tuple([]) }
17 |
18 | static var string : Self
19 | { .symbol("String") }
20 |
21 | /// Return the number of elements captured by the associated syntax expression.
22 | var captureCount : Int {
23 | switch self {
24 | case .symbol : 1
25 | case .array : 1
26 | case .tuple(let captures) : captures.count
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/TSMacros/TokenRep.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftSyntax
6 |
7 |
8 | indirect enum Token {
9 | case lit(String)
10 | case pat(String)
11 | case seq([Token])
12 | case choice([Token])
13 |
14 | /// Convert the receiver into a raw expression. Type names in sym cases are translated
15 | /// to symbol names by the given function, and all captures are implicitly wrapped in
16 | /// fields with increasing numeric names.
17 | var rawExpression : RawExpression {
18 | switch self {
19 | case .lit(let string) :
20 | .string(string)
21 | case .pat(let pattern) :
22 | .pattern(pattern)
23 | case .seq(let tokens) :
24 | .seq(tokens.map({$0.rawExpression}))
25 | case .choice(let tokens) :
26 | .choice(tokens.map({$0.rawExpression}))
27 | }
28 | }
29 | }
30 |
31 |
32 | extension Token {
33 | init(expr: ExprSyntax) throws {
34 | switch expr.kind {
35 | case .functionCallExpr :
36 | let (name, args) = try expr.caseComponents
37 | switch (name, args.count) {
38 | case ("lit", 1) :
39 | guard let argExpr = args[0].expression.as(StringLiteralExprSyntax.self)
40 | else { throw ExpansionError(node: args[0].expression, message: "expecting string literal") }
41 | self = try .init(stringLiteralExpr: argExpr)
42 | case ("pat", 1) :
43 | guard let argExpr = args[0].expression.as(RegexLiteralExprSyntax.self)
44 | else { throw ExpansionError(node: args[0].expression, message: "expecting regex literal") }
45 | self = .init(regexLiteralExpr: argExpr)
46 | case ("seq", 1) :
47 | guard let arrayExpr = args[0].expression.as(ArrayExprSyntax.self)
48 | else { throw ExpansionError(node: args[0].expression, message: "expecting array argument") }
49 | self = .seq(try arrayExpr.elements.map { try Self(expr: $0.expression) })
50 | case ("choice", 1) :
51 | guard let arrayExpr = args[0].expression.as(ArrayExprSyntax.self)
52 | else { throw ExpansionError(node: args[0].expression, message: "expecting array argument") }
53 | self = .choice(try arrayExpr.elements.map { try Self(expr: $0.expression) })
54 | case let other :
55 | throw ExpansionError(node: expr, message: "unsupported case: \(other)")
56 | }
57 | case .stringLiteralExpr :
58 | self = try .init(stringLiteralExpr: expr.cast(StringLiteralExprSyntax.self))
59 | case .regexLiteralExpr :
60 | self = .init(regexLiteralExpr: expr.cast(RegexLiteralExprSyntax.self))
61 | default :
62 | throw ExpansionError(node: expr, message: "unexpected expression syntax")
63 | }
64 | }
65 |
66 | init(stringLiteralExpr expr: StringLiteralExprSyntax) throws {
67 | guard let content = expr.stringLiteral
68 | else { throw ExpansionError(node: expr, message: "unsupported string literal expression") }
69 | self = .lit(content)
70 | }
71 |
72 | init(regexLiteralExpr expr: RegexLiteralExprSyntax) {
73 | let pattern = expr.regex.text.reduce(into: "") { result, char in
74 | if char == "\\" {
75 | result.append("\\\\")
76 | }
77 | else {
78 | result.append(char)
79 | }
80 | }
81 | self = .pat(pattern)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/App.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import AppKit
6 | import SwiftUI
7 |
8 |
9 | @main
10 | struct TSPlayground : App {
11 | @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
12 |
13 | struct ContentView : View {
14 | @StateObject var preferences = Preferences.shared
15 | @StateObject var session = CodeSession(grammar: MyGrammar())
16 |
17 | var body : some View {
18 | SplitView(orientation: .horizontal, splitRatio: $preferences.primarySplitRatio,
19 | firstView:
20 | CodeViewRepresentable(session: session)
21 | .environment(\.ctFont, preferences.defaultFont),
22 | secondView:
23 | SplitView(orientation: .vertical, splitRatio: $preferences.secondarySplitRatio,
24 | firstView: ParseTreeView(session: session)
25 | .tabItem { Label("Parse Tree", systemImage: "folder") },
26 | secondView: IssuesView(session: session)
27 | .tabItem { Label("Issues", systemImage: "questionmark.diamond") }
28 | )
29 | )
30 | .environment(\.font, Font(preferences.defaultFont))
31 | }
32 | }
33 |
34 | var body : some Scene {
35 | WindowGroup {
36 | ContentView()
37 | }
38 | }
39 | }
40 |
41 |
42 | class AppDelegate : NSObject, NSApplicationDelegate {
43 | @objc func selectedFontDidChange(_ sender: NSFontManager) {
44 | Preferences.shared.defaultFont = sender.convert(Preferences.shared.defaultFont)
45 | }
46 |
47 | func applicationDidFinishLaunching(_ notification: Notification) {
48 | // Register to observe changes to the selected font
49 | let fontManager = NSFontManager.shared
50 | fontManager.target = self
51 | fontManager.action = #selector(selectedFontDidChange(_:))
52 |
53 | // Install the font menu
54 | guard let fontMenu = fontManager.fontMenu(true)
55 | else { print("failed to create font menu"); return }
56 | guard let mainMenu = NSApp.mainMenu
57 | else { print("failed to get main menu"); return }
58 | let fontItem = NSMenuItem(title: "Font", action: nil, keyEquivalent: "")
59 | fontItem.submenu = fontMenu
60 | let editIndex = mainMenu.indexOfItem(withTitle: "Edit")
61 | mainMenu.insertItem(fontItem, at: editIndex >= 0 ? editIndex + 1 : mainMenu.numberOfItems)
62 |
63 | // Activate the app...
64 | NSApp.setActivationPolicy(.regular)
65 | NSApp.activate(ignoringOtherApps: true)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/CodeSession.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Combine
6 | import Foundation
7 | import SwiftUI
8 | import TSKit
9 |
10 |
11 | public class CodeSession : ObservableObject {
12 | public struct Issue : Identifiable {
13 | public let id : Int
14 | public let error : SyntaxError
15 | }
16 |
17 | public let grammar : any Grammar
18 | public let encoding : String.Encoding
19 |
20 | private let parser : TSParser
21 | private var tree : TSTree!
22 |
23 | let issuesSubject = CurrentValueSubject<[Issue], Never>([])
24 | let selectedRangeSubject = CurrentValueSubject?, Never>(nil)
25 |
26 | public init(grammar g: any Grammar, encoding e: String.Encoding = .utf8) {
27 | grammar = g
28 | encoding = e
29 | parser = g.createParser()
30 | textDidChange()
31 | }
32 |
33 | @Published public var text : String = "" {
34 | didSet { textDidChange() }
35 | }
36 |
37 | @Published public var selectedNode : TSNode? {
38 | didSet {
39 | let range = selectedNode.map {text.characterRange(forByteRange: $0.sourceByteRange, encoding: encoding)!} ?? nil
40 | selectedRangeSubject.value = range
41 | }
42 | }
43 |
44 | @Published public var selectedIssueIndex : Int? {
45 | didSet {
46 | let issue = selectedIssueIndex.map { issuesSubject.value[$0] }
47 | let range = issue.map {$0.error.range}
48 | selectedRangeSubject.value = range
49 | }
50 | }
51 |
52 | public var selectedRangePublisher : AnyPublisher?, Never> {
53 | selectedRangeSubject.eraseToAnyPublisher()
54 | }
55 |
56 | public var issuesPublisher : AnyPublisher<[Issue], Never> {
57 | issuesSubject.eraseToAnyPublisher()
58 | }
59 |
60 | private func textDidChange() {
61 | #if true
62 | tree = parser.parse(text, existingTree: tree)
63 | #else
64 | tree = parser.parse(text)!
65 | #endif
66 |
67 | issuesSubject.value = grammar.syntaxErrors(in: tree, for: text)
68 | .enumerated()
69 | .map {.init(id: $0, error: $1)}
70 | }
71 |
72 | public var rootNode : TSNode {
73 | tree.rootNode
74 | }
75 |
76 | // Return an attributed string indicating the node symbol name, or the corresponding source text if unnamed.
77 | public func attributedText(for node: TSNode, color: Color = .primary) -> AttributedString {
78 | //assert(node.tree == tree.opaqueTree)
79 | let string = node.isNamed ? grammar.symbolName(for: node).description : text.string(forByteRange: node.sourceByteRange, encoding: encoding)!
80 | var attstr = AttributedString(string + " " + (node.hasError ? "⚠︎" : "") + (node.state == .max ? "⁕" : ""))
81 | attstr.foregroundColor = color
82 | return attstr
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/CodeView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Combine
6 | import SwiftUI
7 | import TSKit
8 |
9 |
10 | public class CodeViewController : NSViewController, NSTextViewDelegate {
11 | typealias Issue = CodeSession.Issue
12 |
13 | let session : CodeSession
14 | var issues : [Issue] = []
15 | var editing : Bool = false
16 | var subscriptions : [AnyCancellable] = []
17 |
18 | init(session s: CodeSession) {
19 | session = s
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | var textView : NSTextView {
24 | view as! NSTextView
25 | }
26 |
27 | var textAttributes : [NSAttributedString.Key: Any] {
28 | return [
29 | .font: font,
30 | .foregroundColor: NSColor.textColor,
31 | ]
32 | }
33 |
34 | func attributes(for issue: Issue) -> [NSAttributedString.Key: Any] {
35 | let keyValuePairs : [(NSAttributedString.Key, Any)] = [
36 | (.underlineColor, NSColor.red),
37 | (.underlineStyle, NSNumber(value: NSUnderlineStyle.thick.rawValue)),
38 | (.toolTip, issue.error.describe(using: session.grammar)),
39 | ]
40 | return textAttributes.merging(keyValuePairs) { _, _ in fatalError() }
41 | }
42 |
43 | var font : CTFont = .init(.system, size: NSFont.systemFontSize) {
44 | didSet { updateContent() }
45 | }
46 |
47 | func updateContent() {
48 | // Note: we reconstruct attributed ranges with an explicit font to avoid the problem where the
49 | // value of textView.font is ignored both on application launch and after each editing change...
50 | let storage = textView.textStorage!
51 | storage.setAttributes(textAttributes, range: NSMakeRange(0, storage.string.count))
52 | for issue in issues {
53 | storage.setAttributes(attributes(for: issue), range: NSRange(issue.error.range))
54 | }
55 | textView.setNeedsDisplay(textView.bounds)
56 | }
57 |
58 |
59 | // MARK: NSViewController
60 |
61 | public override func loadView() {
62 | let textView = NSTextView()
63 | textView.translatesAutoresizingMaskIntoConstraints = false
64 | textView.linkTextAttributes = [:]
65 | textView.delegate = self
66 | view = textView
67 | }
68 |
69 | public override func viewWillAppear() {
70 | super.viewWillAppear()
71 |
72 | textView.string = session.text
73 | subscriptions = [
74 | session.selectedRangePublisher.sink { [weak self] range in
75 | guard let self else { return }
76 | // Note: our textView loses focus on selection changes in the Issues view; workaround by requesting focus
77 | if let nsrange = range.map({NSRange($0)}) {
78 | textView.setSelectedRange(nsrange)
79 | textView.scrollRangeToVisible(nsrange)
80 | }
81 | textView.window?.makeFirstResponder(textView)
82 | },
83 | session.issuesPublisher.sink { [weak self] issues in
84 | guard let self else { return }
85 | self.issues = issues
86 | self.updateContent()
87 | },
88 | ]
89 | }
90 |
91 | public override func viewDidDisappear() {
92 | super.viewDidDisappear()
93 |
94 | subscriptions = []
95 | }
96 |
97 |
98 | // MARK: NSTextViewDelegate
99 |
100 | public func textDidChange(_ notification: Notification) {
101 | assert(editing && notification.object as? NSTextView == .some(textView))
102 | session.text = textView.string
103 | }
104 |
105 | public func textDidBeginEditing(_ notification: Notification) {
106 | editing = true
107 | }
108 |
109 | public func textDidEndEditing(_ notification: Notification) {
110 | editing = false
111 | }
112 |
113 | public func textView(_: NSTextView, clickedOnLink link: Any, at index: Int) -> Bool {
114 | guard let string = link as? String, let index = Int(string)
115 | else { return false }
116 | guard (0 ..< issues.count).contains(index)
117 | else { print("\(#function) -- invalid issue index: \(index)"); return false }
118 |
119 | session.selectedIssueIndex = index
120 | return true
121 | }
122 |
123 |
124 | // MARK: NSCoding
125 |
126 | required init?(coder: NSCoder) {
127 | nil
128 | }
129 | }
130 |
131 |
132 | public struct CodeViewRepresentable : NSViewRepresentable {
133 | public let session : CodeSession
134 |
135 | @Environment(\.ctFont) private var ctFont : CTFont
136 |
137 | public init(session s: CodeSession) {
138 | session = s
139 | }
140 |
141 | public func makeCoordinator() -> CodeViewController {
142 | CodeViewController(session: session)
143 | }
144 |
145 | public func makeNSView(context: Context) -> NSTextView {
146 | return context.coordinator.textView
147 | }
148 |
149 | public func updateNSView(_ textView: NSTextView, context: Context) {
150 | context.coordinator.font = ctFont
151 | }
152 | }
153 |
154 |
155 | extension EnvironmentValues {
156 | struct CTFontEnvironmentKey : EnvironmentKey {
157 | static let defaultValue = CTFont(.system, size: 18)
158 | }
159 |
160 | public var ctFont : CTFont {
161 | get { self[CTFontEnvironmentKey.self] }
162 | set { self[CTFontEnvironmentKey.self] = newValue }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/IssuesView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftUI
6 |
7 |
8 | struct IssuesView : View {
9 | @StateObject var session : CodeSession
10 |
11 | @Environment(\.font) var font : Font?
12 |
13 | var body : some View {
14 | List(session.issuesSubject.value, selection: $session.selectedIssueIndex) { issue in
15 | Text(issue.error.describe(using: session.grammar))
16 | .font(font)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/MyGrammar.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TSKit
6 |
7 |
8 | @Grammar
9 | struct MyGrammar : Grammar {
10 | typealias Root = Expr
11 | static var productionRules : [ProductionRule] {
12 | return [
13 | .init(Digits.self, .pat("[0-9]+")) { digits in digits },
14 | .init(Expr.self, choicesByName: [
15 | "num": .init(.sym(Digits.self)) { digits in .num(digits) },
16 | "add": .init(.prec(.left(1), .seq([.sym(Expr.self), "+", .sym(Expr.self)]))) { lhs, rhs in .add(lhs, rhs) },
17 | "mul": .init(.prec(.left(2), .seq([.sym(Expr.self), "*", .sym(Expr.self)]))) { lhs, rhs in .mul(lhs, rhs) },
18 | "paren": .init(.seq(["(", .sym(Expr.self), ")"])) { expr in expr },
19 | ]),
20 | ]
21 | }
22 | }
23 |
24 |
25 | typealias Digits = String
26 |
27 | indirect enum Expr {
28 | case num(Digits)
29 | case add(Expr, Expr)
30 | case mul(Expr, Expr)
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/ParseTreeView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftUI
6 |
7 |
8 | struct ParseTreeView : View {
9 | @StateObject var session : CodeSession
10 |
11 | var body : some View {
12 | ScrollView {
13 | OutlineView(session.rootNode, id: \.identifier, children: \.children, selection: $session.selectedNode) { node, isSelected in
14 | Text(session.attributedText(for: node, color: isSelected ? .blue : .primary))
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/Preferences.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftUI
6 |
7 |
8 | class Preferences : ObservableObject {
9 | static var shared = Preferences()
10 |
11 | var defaults = UserDefaults.standard
12 | let defaultFontSizeKey = "defaultFontSize"
13 | let primarySplitRatioKey = "primarySplitRatio"
14 | let secondarySplitRatioKey = "secondarySplitRatio"
15 |
16 | @Published var defaultFont : NSFont { didSet { defaults.setValue(defaultFont.pointSize, forKey: defaultFontSizeKey)} }
17 | @Published var primarySplitRatio : CGFloat { didSet { defaults.setValue(primarySplitRatio, forKey: primarySplitRatioKey)} }
18 | @Published var secondarySplitRatio : CGFloat { didSet { defaults.setValue(secondarySplitRatio, forKey: secondarySplitRatioKey)} }
19 |
20 | private init() {
21 | UserDefaults.standard.register(defaults: [
22 | defaultFontSizeKey: NSFont.systemFontSize,
23 | primarySplitRatioKey: 0.75,
24 | secondarySplitRatioKey: 0.60,
25 | ])
26 |
27 | defaultFont = CTFont(.system, size: defaults.double(forKey: defaultFontSizeKey))
28 | primarySplitRatio = defaults.double(forKey: primarySplitRatioKey)
29 | secondarySplitRatio = defaults.double(forKey: secondarySplitRatioKey)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/TSKit-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 | import TSKit
7 |
8 |
9 | extension Grammar {
10 | func createParser() -> TSParser {
11 | TSParser(Self.language)
12 | }
13 | func syntaxErrors(in tree: TSTree, for text: String, encoding e: String.Encoding = .utf8) -> [SyntaxError] {
14 | Self.syntaxErrors(in: tree, for: text, encoding: e)
15 | }
16 | func symbolName(for node: TSNode) -> StaticString {
17 | Self.symbolName(for: node)
18 | }
19 | }
20 |
21 |
22 | // Extend tree-sitter's TSNode to enable use in SwiftUI's OutlineGroup.
23 | extension TSNode {
24 | // SwiftUI views which present collections require collection elements to have a stable identity...
25 | var identifier : UnsafeRawPointer {
26 | id
27 | }
28 |
29 | // OutlineGroup requires its content objects implement this method to distinguish leaves from expandable nodes.
30 | var children : [Self]? {
31 | let count = self.count
32 | guard count > 0 else { return nil }
33 | return (0 ..< count).map { self[$0] }
34 | }
35 | }
36 |
37 |
38 | // For debugging purposes
39 |
40 | import TreeSitter
41 |
42 | extension TSNode {
43 | var symbolName : String {
44 | String(cString: ts_node_grammar_type(self))
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/Utilities/AdjustableDivider.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftUI
6 |
7 |
8 | public struct AdjustableDivider : View {
9 | public enum Orientation { case horizontal, vertical }
10 |
11 | struct Thickness : ViewModifier {
12 | let thickness : CGFloat
13 | let orientation : Orientation
14 |
15 | public func body(content: Content) -> some View
16 | { orientation == .vertical ? content.frame(width: thickness) : content.frame(height: thickness) }
17 | }
18 |
19 | let orientation : Orientation
20 | let color : Color
21 | let thickness : CGFloat
22 | let onDrag : (CGFloat) -> Void
23 |
24 | public init(orientation o: Orientation, color c: Color = .init(white: 0.75), thickness t: CGFloat = 1, onDrag f: @escaping (CGFloat) -> Void) {
25 | orientation = o
26 | color = c
27 | thickness = t
28 | onDrag = f
29 | }
30 |
31 | var resizeCursor : NSCursor
32 | { orientation == .vertical ? .resizeLeftRight : .resizeUpDown }
33 |
34 | public var body: some View {
35 | Rectangle()
36 | .fill(color)
37 | .modifier(Thickness(thickness: thickness, orientation: orientation))
38 | .edgesIgnoringSafeArea(orientation == .horizontal ? .vertical : .horizontal)
39 | .onHover { if $0 { resizeCursor.push() } else { NSCursor.pop() } }
40 | .gesture(DragGesture(minimumDistance: 1, coordinateSpace: .local)
41 | .onChanged {
42 | onDrag(orientation == .vertical ? $0.location.x : $0.location.y)
43 | }
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/Utilities/Foundation-ext.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import Foundation
6 |
7 |
8 | public extension CGFloat {
9 | func clamped(to range: ClosedRange) -> CGFloat {
10 | guard self >= range.lowerBound else { return range.lowerBound }
11 | guard self <= range.upperBound else { return range.upperBound }
12 | return self
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/Utilities/OulineView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftUI
6 |
7 |
8 | // An alternative to OutlineGroup ...
9 |
10 | public struct OutlineView : View where ID : Hashable, Subview : View {
11 |
12 | struct Context {
13 | let id : KeyPath
14 | let children : KeyPath
15 | let selection : Binding?
16 | let content : (Node, Bool) -> Subview
17 | let indent : CGFloat
18 | let autoexpand : Bool
19 | }
20 |
21 | struct Item : View {
22 | let context : Context
23 | let node : Node
24 | let isRoot : Bool
25 |
26 | @State var isExpanded : Bool = true
27 |
28 | init(context: Context, node: Node, isRoot: Bool) {
29 | self.context = context
30 | self.node = node
31 | self.isRoot = isRoot
32 | self.isExpanded = context.autoexpand
33 | }
34 |
35 | var isSelected : Bool
36 | { context.selection.map { $0.wrappedValue?[keyPath: context.id] == .some(node[keyPath: context.id]) } ?? false }
37 |
38 | func toggleSelection()
39 | { context.selection?.wrappedValue = isSelected ? nil : node }
40 |
41 | public var body : some View {
42 | HStack {
43 | Spacer()
44 | .frame(width: isRoot ? 0 : context.indent)
45 | switch node[keyPath: context.children] {
46 | case .some(let subnodes) :
47 | DisclosureGroup(
48 | isExpanded: $isExpanded,
49 | content: {
50 | if isExpanded {
51 | ForEach(subnodes, id: context.id) { subnode in
52 | Self(context: context, node: subnode, isRoot: false)
53 | }
54 | }
55 | },
56 | label: {
57 | context.content(node, isSelected)
58 | .onTapGesture { toggleSelection() }
59 | }
60 | )
61 | case .none :
62 | context.content(node, isSelected)
63 | .onTapGesture { toggleSelection() }
64 | }
65 | Spacer()
66 | }
67 | }
68 | }
69 |
70 | private let root : Node
71 | private let context : Context
72 |
73 | public init(_ root: Node, id: KeyPath, children: KeyPath, selection: Binding? = nil, autoexpand: Bool = false, @ViewBuilder content: @escaping (Node, Bool) -> Subview) {
74 | self.root = root
75 | self.context = .init(id: id, children: children, selection: selection, content: content, indent: 12, autoexpand: autoexpand)
76 | }
77 |
78 | public var body : some View {
79 | VStack {
80 | Item(context: context, node: root, isRoot: true)
81 | Spacer()
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/TSPlayground/Utilities/SplitView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import SwiftUI
6 |
7 |
8 | public struct SplitView : View {
9 | public enum Orientation { case horizontal, vertical }
10 |
11 | let orientation : Orientation
12 | let firstView : FirstView
13 | let secondView : SecondView
14 | let splitRatio : Binding
15 |
16 | var dividerThickness = CGFloat(2)
17 | var dividerColor = Color.gray
18 | var minimumFirstLength = CGFloat(120)
19 | var minimumSecondLength = CGFloat(120)
20 |
21 | public init(orientation o: Orientation, splitRatio r: Binding, firstView v1: FirstView, secondView v2: SecondView) {
22 | orientation = o
23 | splitRatio = r
24 | firstView = v1
25 | secondView = v2
26 | }
27 |
28 | public var body: some View {
29 | GeometryReader { geometry in
30 | let frame = geometry.frame(in: .local)
31 | let length = (orientation == .horizontal ? frame.width : frame.height) - dividerThickness
32 | switch orientation {
33 | case .horizontal :
34 | HStack {
35 | firstView
36 | .frame(width: splitRatio.wrappedValue * length)
37 | AdjustableDivider(orientation: .vertical,
38 | color: dividerColor,
39 | thickness: dividerThickness,
40 | onDrag: { x in
41 | // Note: x is reltive to the minX of the divider rect (i.e. splitRatio * length)
42 | let x = (splitRatio.wrappedValue * length + x).clamped(to: frame.minX + minimumFirstLength ... frame.maxX - minimumSecondLength)
43 | splitRatio.wrappedValue = x / length
44 | }
45 | )
46 | secondView
47 | .frame(width: (1 - splitRatio.wrappedValue) * length)
48 | }
49 | case .vertical :
50 | VStack {
51 | firstView
52 | .frame(height: splitRatio.wrappedValue * length)
53 | AdjustableDivider(orientation: .horizontal,
54 | color: dividerColor,
55 | thickness: dividerThickness,
56 | onDrag: { y in
57 | let y = (splitRatio.wrappedValue * length + y).clamped(to: frame.minY + minimumFirstLength ... frame.maxY - minimumSecondLength)
58 | splitRatio.wrappedValue = y / length
59 | }
60 | )
61 | secondView
62 | .frame(height: (1 - splitRatio.wrappedValue) * length)
63 | }
64 | }
65 | }
66 | .frame(minWidth: minimumFirstLength + dividerThickness + minimumSecondLength)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Tests/ExprLang/ExprLang.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TSKit
6 |
7 |
8 | @Grammar
9 | struct ExprLang : Grammar {
10 | typealias Root = Expr
11 |
12 | static var productionRules : [ProductionRule] {
13 | return [
14 | .init(Expr.self, choicesByName: [
15 | "name": .init(.sym(Name.self)) { name in .name(name) },
16 | "value": .init(.sym(Value.self)) { value in .value(value) },
17 | "add": .init(.prec(.left(1), .seq([.sym(Expr.self), .alt(["+", "-"]), .sym(Expr.self)]))) { lhs, op, rhs in .op(op, [lhs, rhs]) },
18 | "mul": .init(.prec(.left(2), .seq([.sym(Expr.self), .alt(["*", "/"]), .sym(Expr.self)]))) { lhs, op, rhs in .op(op, [lhs, rhs]) },
19 | "pow": .init(.prec(.right(3), .seq([.sym(Expr.self), .alt(["^"]), .sym(Expr.self)]))) { lhs, op, rhs in .op(op, [lhs, rhs]) },
20 | "neg": .init(.prec(.left(4), .seq([.alt(["-"]), .sym(Expr.self)]))) { op, arg in .op(op, [arg]) },
21 | "apply": .init(.prec(5, .seq([.sym(Expr.self), "(", .opt(.sym(ExprList.self)), ")"]))) { fun, args in .apply(fun, args ?? []) },
22 | "paren": .init(.seq(["(", .sym(Expr.self), ")"])) { expr in expr },
23 | ]),
24 | .init(ExprList.self, .rep(.sym(Expr.self), .sep(","))) { exprs in exprs },
25 | .init(Name.self, .pat("[a-zA-Z_][0-9a-zA-Z_]*")) { string in Name(text: string) },
26 | .init(Value.self, .sym(Int.self)) { i in .init(int: i) },
27 | .init(Int.self, .pat("[0-9]+")) { string in Int(string)! },
28 | ]
29 | }
30 |
31 | static var word : String? {
32 | "([a-zA-Z_][0-9a-zA-Z_]* | [!#%&*+-/<=>^|~]+)"
33 | }
34 | }
35 |
36 |
37 | // MARK: -
38 |
39 | indirect enum Expr : Equatable {
40 | case name(Name)
41 | case value(Value)
42 | case apply(Expr, [Expr])
43 | static func op(_ name: String, _ args: [Expr]) -> Self {
44 | .apply(.name(Name(text: name)), args)
45 | }
46 | }
47 |
48 | extension Expr : ExpressibleByStringLiteral {
49 | init(stringLiteral text: String) {
50 | self = .name(Name(text: text))
51 | }
52 | }
53 |
54 | typealias ExprList = Array
55 |
56 |
57 | // MARK: -
58 |
59 | struct Name : Equatable {
60 | let text : String
61 | }
62 |
63 | extension Name : ExpressibleByStringLiteral {
64 | init(stringLiteral s: String) {
65 | self.init(text: s)
66 | }
67 | }
68 |
69 | // MARK: -
70 |
71 | struct Value : Equatable {
72 | let int : Int
73 | }
74 |
75 | extension Value : ExpressibleByIntegerLiteral {
76 | init(integerLiteral i: Int) {
77 | self.init(int: i)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/ExprLang/ExprLangTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 | import TSCommon
8 |
9 |
10 | class ExprLangTests : XCTestCase {
11 |
12 | /// Ensure the result of parsing is as expected for a variety of example terms...
13 | func testParsingSuccess() throws {
14 | let examples : [(text: String, term: Expr)] = [
15 | ("a",
16 | .name("a")),
17 | ("_aX2",
18 | .name("_aX2")),
19 | ("42",
20 | .value(42)),
21 | ("(a)",
22 | .name("a")),
23 | ("x + y",
24 | .apply("+", ["x", "y"])),
25 | ("x + y + z",
26 | .apply("+", [.apply("+", ["x", "y"]), "z"])),
27 | ("x + (y + z)",
28 | .apply("+", ["x", .apply("+", ["y", "z"])])),
29 | ("x ^ y ^ z",
30 | .apply("^", ["x", .apply("^", ["y", "z"])])),
31 | ("(x ^ y) ^ z",
32 | .apply("^", [.apply("^", ["x", "y"]), "z"])),
33 | ("-x",
34 | .apply("-", ["x"])),
35 | ("--x",
36 | .apply("-", [.apply("-", ["x"])])),
37 | ("x + y * z ^ -w",
38 | .apply("+", ["x", .apply("*", ["y", .apply("^", ["z", .apply("-", ["w"])])])])),
39 | ("f()",
40 | .apply("f", [])),
41 | ("f(x)",
42 | .apply("f", ["x"])),
43 | ("g(x, y + z, w)",
44 | .apply("g", ["x", .apply("+", ["y", "z"]), "w"])),
45 | ("f(x)(y)",
46 | .apply(.apply("f", ["x"]), ["y"])),
47 | ("f() + g()",
48 | .apply("+", [.apply("f", []), .apply("g", [])])),
49 | ]
50 | for eg in examples {
51 | XCTAssertEqual(try ExprLang.parse(text: eg.text), eg.term, eg.text)
52 | }
53 | }
54 |
55 | /// Ensure that parsing fails for various malformed strings...
56 | func testParsingFailure() throws {
57 | let examples : [String] = [
58 | "x + ", // missing rhs arg
59 | "x % y", // unknown symbol '%'
60 | "()", // no tuples
61 | "(a, b)", // no tuples
62 | "( x",
63 | "x )",
64 | ]
65 | for text in examples {
66 | do {
67 | _ = try ExprLang.parse(text: text)
68 | XCTFail("Failed to reject '\(text)'")
69 | }
70 | catch {
71 | continue
72 | }
73 | }
74 | }
75 |
76 | /// Ensure simple syntax errors are reported as expected...
77 | func testSyntaxErrors() throws {
78 | guard let sym_Expr = ExprLang.symbol(for: Expr.self)
79 | else { throw Exception("No Expr symbol?") }
80 |
81 | let examples : [(text: String, error: SyntaxError)] = [
82 | ("x +", .init(range: 3 ..< 3, kind: .missing(sym_Expr))),
83 | ("()", .init(range: 1 ..< 1, kind: .missing(sym_Expr))),
84 | ]
85 | let parser = TSParser(ExprLang.language)
86 | for eg in examples {
87 | let tree = parser.parse(eg.text)!
88 | let errors = ExprLang.syntaxErrors(in: tree, for: eg.text)
89 | XCTAssertEqual(errors, [eg.error])
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tests/TSKit/JsonTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 |
8 |
9 | enum JSONValue : Equatable {
10 | case object(JSONObject)
11 | case array(JSONArray)
12 | case string(JSONString)
13 | case number(JSONNumber)
14 | case bool(JSONBool)
15 | case null
16 | }
17 |
18 | typealias JSONObject = Dictionary
19 | typealias JSONMember = JSONObject.Element
20 | typealias JSONArray = Array
21 | typealias JSONString = String
22 | typealias JSONNumber = Double
23 | typealias JSONBool = Bool
24 |
25 |
26 | @Grammar
27 | struct JSONGrammar : Grammar {
28 | typealias Root = JSONValue
29 |
30 | static var productionRules : [ProductionRule] {
31 | return [
32 | .init(JSONValue.self, choicesByName: [
33 | "object": .init(.seq(["{", .opt(.sym(JSONObject.self)), "}"])) { mems in .object(mems ?? [:]) },
34 | "array": .init(.seq(["[", .opt(.sym(JSONArray.self)), "]"])) { elts in .array(elts ?? []) },
35 | "string": .init(.sym(JSONString.self)) { str in .string(str) },
36 | "number": .init(.sym(JSONNumber.self)) { num in .number(num) },
37 | "true": .init(.lit("true")) { () in .bool(true) },
38 | "false": .init(.lit("false")) { () in .bool(false) },
39 | "null": .init(.lit("null")) { () in .null },
40 | ]),
41 | .init(JSONObject.self, .rep(.sym(JSONMember.self), .sep(","))) { (mems: Array) in Dictionary(uniqueKeysWithValues: mems) },
42 | .init(JSONMember.self, .seq([.sym(JSONString.self), ":", .sym(JSONValue.self)])) { k, v in (k, v) },
43 | .init(JSONArray.self, .rep(.sym(JSONValue.self), .sep(","))) { elts in elts },
44 | .init(JSONNumber.self, .pat("(-?)(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?")) { str in .init(Double(str)!) },
45 | .init(JSONString.self, .pat("\"([^\"\\\\\\n]|[\\\\]([\\\\/bfnrt]|u[0-9a-fA-F]{4,4}))*\"")) { str in removingFirstAndLastCharacter(of: str) },
46 | ]
47 | }
48 |
49 | static var extras : [Token] {
50 | return [
51 | .pat(#/\s/#),
52 | .seq(["//", .pat(#/.*/#)]),
53 | // // NOTE: if block comments are enabled then Xcode fails to show macro expansion
54 | // .seq(["/*", .pat(#/[^*]*\*+(?:[^\/*][^*]*\*+)*/#), "/"]),
55 | ]
56 | }
57 | }
58 |
59 |
60 | class JSONTests : XCTestCase {
61 | func testBuild() throws { }
62 |
63 | func testSuccess() throws {
64 | let examples : [(text: String, value: JSONValue)] = [
65 | ("null", nil),
66 |
67 | ("true", true),
68 | ("false", false),
69 |
70 | ("0", 0),
71 | ("-1", -1),
72 | ("1.5", 1.5),
73 | ("1.5e3", 1.5e3),
74 | ("1.5e+3", 1.5e+3),
75 | //("1.5e-3", .number(1.5e-3)), // need approximate comparison
76 |
77 | ("\"\"", ""),
78 | ("\"heynow\"", "heynow"),
79 | ("\"\\n\"", "\\n"),
80 | ("\"\\u03bb\"", "\\u03bb"),
81 |
82 | ("[]", []),
83 | ("[1, \"two\", [null, true, false]]", [1, "two", [nil, true, false]]),
84 |
85 | ("{}", [:]),
86 | ("{\"a\": 1}", ["a": 1]),
87 | ("{\"a\": 1, \"b\": 2, \"c\": 3}", ["a": 1, "b": 2, "c": 3]),
88 | ("{\"\": {\"x\": null}}", ["": ["x": nil]]),
89 |
90 | ("""
91 | // heynow
92 | 42
93 | """, 42),
94 | ]
95 | for eg in examples {
96 | XCTAssertEqual(try JSONGrammar.parse(text: eg.text), eg.value, eg.text)
97 | }
98 | }
99 |
100 | func testFailure() throws {
101 | let examples : [String] = [
102 | "", // no text
103 | //"\"\n\"", // no newlines (NOTE: this fails, perhaps due to the default treatment of whitespace)
104 | "leading\"", // no leading quote
105 | "\"trailing", // no trailing quote
106 | "\"\\ux\"", // invalid utf8 character
107 | "01", // unnecessary leading zero
108 | "1.",
109 | ".2",
110 | ]
111 | for text in examples {
112 | do {
113 | _ = try JSONGrammar.parse(text: text)
114 | XCTFail("failed to reject invalid text: `\(text)`")
115 | }
116 | catch { }
117 | }
118 | }
119 | }
120 |
121 | // convenience
122 |
123 | extension JSONValue : ExpressibleByNilLiteral {
124 | init(nilLiteral: ()) {
125 | self = .null
126 | }
127 | }
128 |
129 | extension JSONValue : ExpressibleByBooleanLiteral {
130 | init(booleanLiteral: Bool) {
131 | self = .bool(booleanLiteral)
132 | }
133 | }
134 |
135 | extension JSONValue : ExpressibleByIntegerLiteral {
136 | init(integerLiteral: Int) {
137 | self = .number(Double(integerLiteral))
138 | }
139 | }
140 |
141 | extension JSONValue : ExpressibleByFloatLiteral {
142 | init(floatLiteral: Float) {
143 | self = .number(Double(floatLiteral))
144 | }
145 | }
146 |
147 | extension JSONValue : ExpressibleByStringLiteral {
148 | init(stringLiteral: String) {
149 | self = .string(stringLiteral)
150 | }
151 | }
152 |
153 | extension JSONValue : ExpressibleByArrayLiteral {
154 | init(arrayLiteral: JSONValue...) {
155 | self = .array(arrayLiteral)
156 | }
157 | }
158 |
159 | extension JSONValue : ExpressibleByDictionaryLiteral {
160 | init(dictionaryLiteral: (JSONString, JSONValue)...) {
161 | self = .object(Dictionary(uniqueKeysWithValues: dictionaryLiteral))
162 | }
163 | }
164 |
165 | func removingFirstAndLastCharacter(of string: String) -> String {
166 | var copy = string
167 | copy.removeFirst()
168 | copy.removeLast()
169 | return copy
170 | }
171 |
--------------------------------------------------------------------------------
/Tests/TSKit/LambdaTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 |
8 |
9 | @Grammar
10 | fileprivate struct LambdaCalculus : Grammar {
11 | typealias Root = Term
12 | static var productionRules : [ProductionRule] {
13 | return [
14 | .init(Term.self, choicesByName: [
15 | "name": .init(.sym(Name.self)) { x in
16 | .name(x)
17 | },
18 | "apply": .init(.prec(.left(1), .seq([.sym(Term.self), .sym(Term.self)]))) { t, u in
19 | .apply(t, u)
20 | },
21 | "lambda": .init(.seq(["λ", .sym(Name.self), ".", .sym(Term.self)])) { x, t in
22 | .lambda(x, t)
23 | },
24 | "paren": .init(.seq(["(", .sym(Term.self), ")"])) { t in
25 | t
26 | },
27 | ]),
28 | .init(Name.self, .pat("[a-zA-Z_]+")) { s
29 | in Name(text: s)
30 | },
31 | ]
32 | }
33 | }
34 |
35 | // --------------------------------------------------------------------------------
36 |
37 | class LambdaTests : XCTestCase {
38 | func testBuild() throws {
39 | }
40 |
41 | func testSuccess() throws {
42 | let examples : [(text: String, term: Term)] = [
43 | ("x", "x"),
44 | ("x y", .apply("x", "y")),
45 | ("(x)", "x"),
46 | ("x y z", .apply(.apply("x", "y"), "z")),
47 | ("x (y z)", .apply("x", .apply("y", "z"))),
48 | ("λx. y", .lambda("x", "y")),
49 | ("λx. y z", .lambda("x", .apply("y", "z"))),
50 | ("(λx.y) z", .apply(.lambda("x", "y"), "z")),
51 | ]
52 | for eg in examples {
53 | let term = try LambdaCalculus.parse(text: eg.text)
54 | XCTAssertEqual(eg.term, term, eg.text)
55 | }
56 | }
57 | }
58 |
59 | // --------------------------------------------------------------------------------
60 |
61 | fileprivate indirect enum Term : Equatable {
62 | case name(Name)
63 | case apply(Term, Term)
64 | case lambda(Name, Term)
65 | }
66 |
67 | fileprivate struct Name : Equatable {
68 | let text : String
69 | }
70 |
71 | // --------------------------------------------------------------------------------
72 |
73 | extension Term : ExpressibleByStringLiteral {
74 | init(stringLiteral: String) {
75 | self = .name(Name(text: stringLiteral))
76 | }
77 | }
78 |
79 | extension Name : ExpressibleByStringLiteral {
80 | init(stringLiteral: String) {
81 | text = stringLiteral
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/TSKit/ParserGenTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 | import TSCommon
8 | import TreeSitterCLI
9 |
10 |
11 | /// Test the callable interface to tree-sitter CLI.
12 | class ParserGenTests : XCTestCase {
13 | let validJsonText = """
14 | {
15 | "name": "TestSuccess",
16 | "rules": {
17 | "start": { "type": "SYMBOL", "name": "Word" },
18 | "Word": { "type": "PATTERN", "value": "[a-z]+" }
19 | },
20 | "extras": [],
21 | "conflicts": [],
22 | "precedences": [],
23 | "externals": [],
24 | "inline": [],
25 | "supertypes": []
26 | }
27 | """
28 |
29 | /// Ensure that non-empty text is returned for a trivial valid grammar.
30 | func testSuccess() throws {
31 | let swiftText = try generateParserSource(for: validJsonText)
32 | XCTAssert(swiftText.isEmpty == false)
33 | }
34 |
35 | func testAccessModifier() throws {
36 | for modifier in ["public", "package"] {
37 | let swiftText = try generateParserSource(for: validJsonText, accessModifier: modifier)
38 | XCTAssert(swiftText.contains(try Regex("\(modifier) static let symbolNames")))
39 | XCTAssert(swiftText.contains(try Regex("\(modifier) static let language")))
40 | }
41 | }
42 |
43 | /// Ensure that specifying an invalid ABI version if rejected.
44 | func testInvalidVersion() throws {
45 | do {
46 | _ = try generateParserSource(for: validJsonText, abi_version: UInt32.max)
47 | XCTFail("failed to throw")
48 | }
49 | catch let error as Exception {
50 | XCTAssertEqual(error.code, SWIFTGEN_ERROR_INVALID_VERSION)
51 | }
52 | }
53 |
54 | /// Ensure that specifying a non-UTF8 encoded by sequence is rejected.
55 | func testInvalidInput() throws {
56 | let nonUTF8Bytes = UnsafeBufferPointer.initialized(with: [0xff])
57 | let accessBytes = UnsafeBufferPointer.initialized(with: [])
58 | do {
59 | _ = try generateParserSource(for: nonUTF8Bytes, accessModifier: accessBytes)
60 | XCTFail("failed to throw")
61 | }
62 | catch let error as Exception {
63 | XCTAssertEqual(error.code, SWIFTGEN_ERROR_INVALID_INPUT)
64 | }
65 | }
66 |
67 | /// Ensure that an error is produced for an incomplete grammar (missing required entries).
68 | func testIncompleteGrammar() throws {
69 | do {
70 | _ = try generateParserSource(for: "{}")
71 | XCTFail("failed to throw")
72 | }
73 | catch let error as Exception {
74 | XCTAssertEqual(error.code, SWIFTGEN_ERROR_OTHER)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Tests/TSKit/StaticStringTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 |
8 |
9 | /// Test Comparable extension of StaticString.
10 | class StaticStringTests : XCTestCase {
11 | func testEqual() throws {
12 | let examples : [(lhs: StaticString, rhs: StaticString)] = [
13 | ("", ""),
14 | ("a", "a"),
15 | ("heynow", "heynow"),
16 | ]
17 | for eg in examples {
18 | XCTAssertEqual(eg.lhs, eg.rhs)
19 | }
20 | }
21 |
22 | func testNotEqual() throws {
23 | let examples : [(lhs: StaticString, rhs: StaticString)] = [
24 | ("", "a"),
25 | ("b", ""),
26 | ("this", "that"),
27 | ("heynow", "hey"),
28 | ("hey", "heynow"),
29 | ("x", "y"),
30 | ]
31 | for eg in examples {
32 | XCTAssertNotEqual(eg.lhs, eg.rhs)
33 | }
34 | }
35 |
36 | func testLessThan() throws {
37 | let examples : [(lhs: StaticString, rhs: StaticString)] = [
38 | ("", "a"),
39 | ("a", "b"),
40 | ("A", "a"),
41 | ("a", "ab"),
42 | ("heyno", "heynow"),
43 | ("heynow", "heynoz"),
44 | ]
45 | for eg in examples {
46 | XCTAssertLessThan(eg.lhs, eg.rhs)
47 | }
48 | }
49 |
50 | func testGreaterThan() throws {
51 | let examples : [(lhs: StaticString, rhs: StaticString)] = [
52 | ("a", ""),
53 | ("b", "a"),
54 | ("a", "B"),
55 | ("ab", "a"),
56 | ("heynow", "heyno"),
57 | ("heynoz", "heynow"),
58 | ]
59 | for eg in examples {
60 | XCTAssertGreaterThan(eg.lhs, eg.rhs)
61 | }
62 | }
63 |
64 | func testLessThanOrEqual() throws {
65 | let examples : [(lhs: StaticString, rhs: StaticString)] = [
66 | ("", ""),
67 | ("a", "a"),
68 | ("a", "b"),
69 | ("a", "aaa"),
70 | ("A", "a"),
71 | ]
72 | for eg in examples {
73 | XCTAssertLessThanOrEqual(eg.lhs, eg.rhs)
74 | }
75 | }
76 |
77 | func testGreaterThanOrEqual() throws {
78 | let examples : [(lhs: StaticString, rhs: StaticString)] = [
79 | ("", ""),
80 | ("a", "a"),
81 | ("b", "a"),
82 | ("aaa", "a"),
83 | ("a", "A"),
84 | ]
85 | for eg in examples {
86 | XCTAssertGreaterThanOrEqual(eg.lhs, eg.rhs)
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Tests/TSKit/StringTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 |
8 |
9 | class StringTests : XCTestCase {
10 | /// Ensure that our custom character count method agrees with the built-in functionality on whole strings.
11 | func testUTF8ByteCount() throws {
12 | // Strings with different-length UTF8 encodings (from https://en.wikipedia.org/wiki/UTF-8)
13 | let examples : [String] = [
14 | // 1 byte
15 | "$",
16 | // 2 bytes
17 | "£",
18 | "И",
19 | // 3 bytes
20 | "ह",
21 | "€",
22 | "한",
23 | // 4 bytes
24 | "𐍈",
25 | "\u{1096B3}",
26 | // ...
27 | "",
28 | "heynow",
29 | "$£Иह€한𐍈",
30 | ]
31 | for string in examples {
32 | let charCount = string.count
33 | let byteCount = string.utf8.count
34 | let charRange = string.characterRange(forByteRange: 0 ..< byteCount, encoding: .utf8)
35 | XCTAssertEqual(charRange, 0 ..< charCount, string)
36 | }
37 | }
38 |
39 | /// Ensure our character count method fails on invalid character ranges.
40 | func testUTF8ByteCountFailure() throws {
41 | let examples : [String] = [
42 | "£",
43 | "€",
44 | "𐍈",
45 | ]
46 | for s in examples {
47 | XCTAssertTrue(s.count == 1)
48 | let k = s.utf8.count
49 | XCTAssertEqual(s.characterRange(forByteRange: 0 ..< k, encoding: .utf8), 0 ..< 1)
50 | for i in 1 ..< k {
51 | XCTAssertEqual(s.characterRange(forByteRange: i ..< k, encoding: .utf8), nil)
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/TSKit/UnsafePointerTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 |
8 |
9 | /// Test the utility methods defined on *UnsafePointer* and *UnsafeBufferPointer* which are used to implement various static properties in the expansion of the *Grammar* macro.
10 | class UnsafePointerTests : XCTestCase {
11 | // Auxiliary type
12 | struct Position : Hashable { let row, column : Int }
13 |
14 | /// Test UnsafePointer.initialized(implicitlyZeroing:using:), which is used to implement the *language* property.
15 | func testValue() throws {
16 | struct Thing : Equatable {
17 | var a, b, c : Int
18 | var d: Double
19 | }
20 | let examples : [(value: Thing, initializer: (inout Thing) -> Void)] = [
21 | (.init(a: 0, b: 0, c: 0, d: 0), { _ in }),
22 | (.init(a: 1, b: 0, c: 3, d: 0), { $0.a = 1; $0.c = 3 }),
23 | (.init(a: 1, b: 2, c: 3, d: 4), { $0.a = 1; $0.b = 2; $0.c = 3; $0.d = 4 }),
24 | ]
25 | for eg in examples {
26 | let ptr = UnsafePointer.initialized(implicitlyZeroing: true, using: eg.initializer)
27 | XCTAssert(eg.value == ptr.pointee)
28 | ptr.deallocate()
29 | }
30 | }
31 |
32 | /// Test UnsafeBufferPointer.initialized(with:), which is used to create various static arrays.
33 | func testArrayOfValues() throws {
34 | let examples : [Array] = [
35 | .init(1 ... 10),
36 | [42],
37 | [],
38 | ]
39 | for array in examples {
40 | let buffer = UnsafeBufferPointer.initialized(with: array)
41 | XCTAssert(array.count == buffer.count)
42 | for i in 0 ..< array.count {
43 | XCTAssertEqual(array[i], buffer[i])
44 | }
45 | buffer.deallocate()
46 | }
47 | }
48 |
49 | /// Test UnsafeBufferPointer.initialized(count:zero:indexValuePairs:), which is used to implement the *ts_primary_state_ids*, *ts_lex_modes*, and *ts_external_scanner_symbol_map* properties.
50 | func testArrayOfIndexValuePairs() throws {
51 | let examples : [(count: Int, pairs: [(index: Int, value: Int)])] = [
52 | (10, [(1, 111), (5, 555), (9, 999)]),
53 | (10, (0 ..< 10).map {i in (i, i * 10)}),
54 | (0, []),
55 | ]
56 | for eg in examples {
57 | let buffer = UnsafeBufferPointer.initialized(count: eg.count, indexValuePairs: eg.pairs)
58 | XCTAssert(buffer.count == eg.count)
59 | let mapping = Dictionary(uniqueKeysWithValues: eg.pairs)
60 | for i in 0 ..< eg.count {
61 | XCTAssertEqual(buffer[i], mapping[i] ?? 0)
62 | }
63 | buffer.deallocate()
64 | }
65 | }
66 |
67 | /// Test UnsafeBufferPointer.initialized(rowCount:columnCount:zero:rowColumnValueTriples:), which is used to implement the *ts_alias_sequences* and *ts_external_scanner_states* properties.
68 | func testMatrixOfRowColumnValueTriples() throws {
69 | let examples : [(rowCount: Int, columnCount: Int, triples: [(row: Int, column: Int, value: Int)])] = [
70 | (5, 5, [(0, 0, 1), (1, 1, 2), (2, 2, 3), (3, 3, 4), (4, 4, 5), (0, 4, 6), (4, 0, 7)]),
71 | (2, 3, []),
72 | (0, 1, []),
73 | (1, 0, []),
74 | ]
75 | for eg in examples {
76 | let buffer = UnsafeBufferPointer.initialized(rowCount: eg.rowCount, columnCount: eg.columnCount, rowColumnValueTriples: eg.triples)
77 | XCTAssertEqual(buffer.count, eg.rowCount * eg.columnCount)
78 | let mapping = Dictionary(uniqueKeysWithValues: eg.triples.map {t in (key: Position(row: t.row, column: t.column), value: t.value)})
79 | for i in 0 ..< eg.rowCount {
80 | for j in 0 ..< eg.columnCount {
81 | XCTAssertEqual(buffer[i * eg.columnCount + j], mapping[Position(row: i, column: j)] ?? 0)
82 | }
83 | }
84 | buffer.deallocate()
85 | }
86 | }
87 |
88 | /// Test UnsafeBufferPointer.initialized(rowCount:columnCount:zero:columnValuesByRow:), which is used to implement the *ts_parse_table* property.
89 | func testMatrixOfColumnValuesByRow() throws {
90 | let examples : [(rowCount: Int, columnCount: Int, columnValuesByRow: Array<(row: Int, pairs: Array<(column: Int, value: Int)>)>)] = [
91 | (10, 10, (0 ..< 10).map {i in (i, (0 ..< 10).map {j in (j, i * j + 1)})}),
92 | (10, 10, []),
93 | (0, 10, []),
94 | (10, 0, []),
95 | (0, 0, []),
96 | ]
97 | for eg in examples {
98 | let buffer = UnsafeBufferPointer.initialized(rowCount: eg.rowCount, columnCount: eg.columnCount, columnValuesByRow: eg.columnValuesByRow)
99 | XCTAssertEqual(buffer.count, eg.rowCount * eg.columnCount)
100 | let mapping = Dictionary(uniqueKeysWithValues: eg.columnValuesByRow.flatMap { row, columnValues in
101 | columnValues.map { column, value in
102 | (key: Position(row: row, column: column), value: value)
103 | }
104 | })
105 | for i in 0 ..< eg.rowCount {
106 | for j in 0 ..< eg.columnCount {
107 | XCTAssertEqual(buffer[i * eg.columnCount + j], mapping[Position(row: i, column: j)] ?? 0)
108 | }
109 | }
110 | buffer.deallocate()
111 | }
112 | }
113 |
114 | /// Test UnsafeBufferPointer.arrayOfCSrings(_:), which is used to implement *symbolNames* and *fieldNames*.
115 | func testArrayOfCStrings() throws {
116 | let examples : [[StaticString?]] = [
117 | ["hey", "μ", "getoffa", "icloud", nil],
118 | [],
119 | ]
120 | for sstrings in examples {
121 | let cstrings = UnsafeBufferPointer.arrayOfCSrings(sstrings)
122 | XCTAssert(cstrings.count == sstrings.count)
123 | for i in 0 ..< cstrings.count {
124 | XCTAssert(sstrings[i].map {$0.hasPointerRepresentation} ?? true)
125 | switch (sstrings[i], cstrings[i]) {
126 | case (.some(let sstring), .some(let cstring)) :
127 | let length = sstring.utf8CodeUnitCount + 1 // +1 to include null terminator
128 | let sstring_cast = UnsafeRawPointer(sstring.utf8Start).bindMemory(to: Int8.self, capacity: length)
129 | XCTAssertEqual(strncmp(sstring_cast, cstring, sstring.utf8CodeUnitCount + 1), 0);
130 | case (.none, .none) :
131 | break
132 | default :
133 | XCTFail()
134 | }
135 | }
136 | cstrings.deallocate()
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Tests/TypedLang/TypedLang.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import TSKit
6 | import TSCommon
7 | import XCTest
8 |
9 |
10 | // MARK: - language types -
11 |
12 | struct Block : Equatable {
13 | let decls : [Def]
14 | let expr : Expr
15 | }
16 |
17 | enum Def : Equatable {
18 | case `typedef`(Typedef)
19 | case `enum`(Enum)
20 | case `let`(Let)
21 | case `fun`(Fun)
22 | }
23 |
24 | typealias DefList = [Def]
25 |
26 | struct Enum : Equatable {
27 | let name : Name
28 | let cases : [EnumCase]
29 | }
30 |
31 | struct EnumCase : Equatable {
32 | let name : Name
33 | let params : [Param]
34 | }
35 |
36 | typealias EnumCaseList = [EnumCase]
37 |
38 | indirect enum Expr : Equatable {
39 | case name(Name)
40 | case numb(Int)
41 | case apply(Expr, Expr)
42 | case lambda([Param], TypeExpr, Expr)
43 | case mu(Name, [Param], TypeExpr, Expr)
44 | case tuple([Expr])
45 | case project(Expr, Int)
46 | case match(Expr, [MatchCase])
47 | case block(Block)
48 | static func paren(_ exprs: [Expr]?) -> Self
49 | { {$0.count == 1 ? $0[0] : .tuple($0)}(exprs ?? []) }
50 | static func infix(_ lhs: Expr, _ op: String, _ rhs: Expr) -> Self
51 | { .apply(.name(Name(stringLiteral: op)), .tuple([lhs, rhs])) }
52 | static func prefix(_ op: String, _ arg: Expr) -> Self
53 | { .apply(.name(Name(stringLiteral: op)), arg) }
54 | }
55 |
56 | typealias ExprList = [Expr]
57 |
58 | struct Fun : Equatable {
59 | let name : Name
60 | let params : [Param]
61 | let type : TypeExpr
62 | let expr : Expr
63 | }
64 |
65 | struct Let : Equatable {
66 | let param : Param
67 | let expr : Expr
68 | }
69 |
70 | struct MatchCase : Equatable {
71 | let name : Name
72 | let params : [Name]
73 | let expr : Expr
74 | }
75 |
76 | typealias MatchCaseList = [MatchCase]
77 |
78 | struct Name : Equatable {
79 | let text : String
80 | }
81 |
82 | typealias NameList = [Name]
83 |
84 | struct Param : Equatable {
85 | let name : Name
86 | let type : TypeExpr
87 | }
88 |
89 | typealias ParamList = [Param]
90 |
91 | indirect enum TypeExpr : Equatable {
92 | case name(Name)
93 | case apply(Name, [TypeExpr])
94 |
95 | static func arrow(_ src: Self, _ trg: Self) -> Self {
96 | .apply("->", [src, trg])
97 | }
98 | }
99 |
100 | typealias TypeExprList = [TypeExpr]
101 |
102 | struct Typedef : Equatable {
103 | let name : Name
104 | let type : TypeExpr
105 | }
106 |
107 |
108 | // MARK: - grammar -
109 |
110 | @Grammar
111 | struct TypedLang : Grammar {
112 | typealias Root = Block
113 |
114 | static var productionRules : [ProductionRule] {
115 | return [
116 | .init(Block.self, .seq([.opt(.sym(DefList.self)), .sym(Expr.self)])) { defs, expr in
117 | Block(decls: defs ?? [], expr: expr)
118 | },
119 | .init(Def.self, choicesByName: [
120 | "typedef": .init(.seq([.sym(Typedef.self)])) { x in Def.typedef(x) },
121 | "enum": .init(.seq([.sym(Enum.self)])) { x in Def.enum(x) },
122 | "let": .init(.seq([.sym(Let.self)])) { x in Def.let(x) },
123 | "fun": .init(.seq([.sym(Fun.self)])) { x in Def.fun(x) },
124 | ]),
125 | .init(DefList.self, .rep(.sym(Def.self), .del(";"))) { defs in
126 | defs
127 | },
128 | .init(Enum.self, .seq(["enum", .sym(Name.self), "{", .sym(EnumCaseList.self), "}"])) { name, caselist in
129 | Enum(name: name, cases: caselist)
130 | },
131 | .init(EnumCase.self, .seq([.sym(Name.self), .opt(.seq(["(", .sym(ParamList.self), ")"]))])) { name, plist in
132 | EnumCase(name: name, params: plist ?? [])
133 | },
134 | .init(EnumCaseList.self, .rep(.sym(EnumCase.self), .sep(","))) { exprs in
135 | exprs
136 | },
137 | .init(Expr.self, choicesByName: [
138 | "name": .init(.sym(Name.self)) { name in
139 | .name(name)
140 | },
141 | "numb" : .init(.sym(Int.self)) { int in
142 | .numb(int)
143 | },
144 | "call" : .init(.prec(.left(9), .seq([.sym(Expr.self), "(", .opt(.sym(ExprList.self)), ")"]))) { fun, args in
145 | .apply(fun, .paren(args))
146 | },
147 | "lambda" : .init(.seq(["!", "(", .opt(.sym(ParamList.self)), ")", "->", .sym(TypeExpr.self), ".", .sym(Expr.self)])) { plist, rtype, expr in
148 | .lambda(plist ?? [], rtype, expr)
149 | },
150 | "mu" : .init(.seq(["!", .sym(Name.self), "(", .opt(.sym(ParamList.self)), ")", "->", .sym(TypeExpr.self), ".", .sym(Expr.self)])) { name, plist, rtype, expr in
151 | .mu(name, plist ?? [], rtype, expr)
152 | },
153 | "paren" : .init(.seq(["(", .opt(.sym(ExprList.self)), ")"])) { elist in
154 | .paren(elist)
155 | },
156 | "project" : .init(.prec(.left(8), .seq([.sym(Expr.self), ".", .sym(Int.self)]))) { expr, index in
157 | .project(expr, index)
158 | },
159 | // match f(x) { nil() => 1, cons(h,t) => 2 }
160 | "match" : .init(.seq(["match", .sym(Expr.self), "{", .sym(MatchCaseList.self), "}"])) { expr, caselist in
161 | .match(expr, caselist)
162 | },
163 | "block" : .init(.seq(["{", .sym(Block.self), "}"])) { block in
164 | .block(block)
165 | },
166 | "eql" : .init(.prec(.left(1), .seq([.sym(Expr.self), .alt(["==", "<", ">", "<=", ">="]), .sym(Expr.self)]))) { lhs, op, rhs in
167 | .infix(lhs, op, rhs)
168 | },
169 | "or" : .init(.prec(.left(2), .seq([.sym(Expr.self), .alt(["|", "|"]), .sym(Expr.self)]))) { lhs, op, rhs in
170 | .infix(lhs, op, rhs)
171 | },
172 | "and" : .init(.prec(.left(3), .seq([.sym(Expr.self), .alt(["&&"]), .sym(Expr.self)]))) { lhs, op, rhs in
173 | .infix(lhs, op, rhs)
174 | },
175 | "add" : .init(.prec(.left(4), .seq([.sym(Expr.self), .alt(["+", "-"]), .sym(Expr.self)]))) { lhs, op, rhs in
176 | .infix(lhs, op, rhs)
177 | },
178 | "mul" : .init(.prec(.left(5), .seq([.sym(Expr.self), .alt(["*", "/", "%"]), .sym(Expr.self)]))) { lhs, op, rhs in
179 | .infix(lhs, op, rhs)
180 | },
181 | "pow" : .init(.prec(.right(6), .seq([.sym(Expr.self), .alt(["^"]), .sym(Expr.self)]))) { lhs, op, rhs in
182 | .infix(lhs, op, rhs)
183 | },
184 | "neg" : .init(.prec(7, .seq([.alt(["-"]), .sym(Expr.self)]))) { op, arg in
185 | .prefix(op, arg)
186 | },
187 | ]),
188 | .init(ExprList.self, .rep(.sym(Expr.self), .sep(","))) { exprs in
189 | exprs
190 | },
191 | .init(Fun.self, .seq(["fun", .sym(Name.self), "(", .opt(.sym(ParamList.self)), ")", "->", .sym(TypeExpr.self), "{", .sym(Expr.self), "}"])) { name, params, rtype, body in
192 | Fun(name: name, params: params ?? [], type: rtype, expr: body)
193 | },
194 | .init(Int.self, .pat("[0-9]+")) { string in
195 | Int(string)!
196 | },
197 | .init(Let.self, .seq(["let", .sym(Param.self), "=", .sym(Expr.self)])) { param, expr in
198 | Let(param: param, expr: expr)
199 | },
200 | .init(MatchCase.self, .seq([.sym(Name.self), "(", .opt(.sym(NameList.self)), ")", "=>", .sym(Expr.self)])) { name, params, expr in
201 | MatchCase(name: name, params: params ?? [], expr: expr)
202 | },
203 | .init(MatchCaseList.self, .rep(.sym(MatchCase.self), .sep(","))) { cases in
204 | cases
205 | },
206 | .init(Name.self, .pat("[a-zA-Z_][0-9a-zA-Z_]*")) { string in
207 | Name(text: string)
208 | },
209 | .init(NameList.self, .rep(.sym(Name.self), .sep(","))) { names in
210 | names
211 | },
212 | .init(Param.self, .seq([.sym(Name.self), ":", .sym(TypeExpr.self)])) { name, type in
213 | Param(name: name, type: type)
214 | },
215 | .init(ParamList.self, .rep(.sym(Param.self), .sep(","))) { params in
216 | params
217 | },
218 | .init(TypeExpr.self, choicesByName: [
219 | "name": .init(.sym(Name.self)) { name in
220 | .name(name)
221 | },
222 | "const": .init(.prec(1, .seq([.sym(Name.self), "(", .opt(.sym(TypeExprList.self)), ")"]))) { name, types in
223 | .apply(name, types ?? [])
224 | },
225 | "tuple": .init(.seq(["(", .opt(.sym(TypeExprList.self)), ")"])) { types in
226 | {$0.count == 1 ? $0[0] : .apply("()", $0)}(types ?? [])
227 | },
228 | "func": .init(.prec(.right(1), .seq([.sym(TypeExpr.self), "->", .sym(TypeExpr.self)]))) { src, trg in
229 | .arrow(src, trg)
230 | },
231 | ]),
232 | .init(TypeExprList.self, .rep(.sym(TypeExpr.self), .sep(","))) { texprs in
233 | texprs
234 | },
235 | .init(Typedef.self, .seq(["typedef", .sym(Name.self), "=", .sym(TypeExpr.self)])) { name, type in
236 | Typedef(name: name, type: type)
237 | }
238 | ]
239 | }
240 |
241 | static var word : String? {
242 | "([a-zA-Z_][0-9a-zA-Z_]* | [!#%&*+-/<=>^|~]+)"
243 | }
244 | }
245 |
246 |
247 | #if false
248 | // Note: we can no longer use static properties to define precedence values,
249 | // because our macro has no means of interpeting those property names.
250 | // As an alternative we can either:
251 | // - provide a tie-in with TS precedence groups
252 | // - extend Grammar with a means of defining precedence values by name...
253 | extension ProductionRule.Precedence {
254 | static var eql : Self { .left(1) }
255 | static var or : Self { .left(2) }
256 | static var and : Self { .left(3) }
257 | static var add : Self { .left(4) }
258 | static var mult : Self { .left(5) }
259 | static var power : Self { .right(6) }
260 | static var neg : Self { .none(7) }
261 | static var proj : Self { .left(8) }
262 | static var apply : Self { .left(9) }
263 | }
264 | #endif
265 |
266 |
267 | // MARK: - testing convenience -
268 |
269 | extension Block {
270 | init(_ text: String) throws {
271 | self = try TypedLang.parse(text: text)
272 | }
273 | }
274 |
275 | extension Def {
276 | init(_ text: String) throws {
277 | // Parse a block with the declaration text followed by a trivial expression.
278 | let block = try Block(text + "; 1")
279 | guard block.decls.count == 1
280 | else { throw Exception("requires a single declaration") }
281 | self = block.decls[0]
282 | }
283 | }
284 |
285 | extension Expr {
286 | init(_ text: String) throws {
287 | self = try Block(text).expr
288 | }
289 |
290 | static func infix_op(_ op: String, _ lhs: Self, _ rhs: Self) -> Self
291 | { .apply(.name(Name(stringLiteral: op)), .tuple([lhs, rhs])) }
292 |
293 | static func prefix_op(_ op: String, _ arg: Self) -> Self
294 | { .apply(.name(Name(stringLiteral: op)), arg) }
295 | }
296 |
297 | extension Expr : ExpressibleByStringLiteral {
298 | init(stringLiteral s: String)
299 | { self = .name(.init(stringLiteral: s)) }
300 | }
301 |
302 | extension Expr : ExpressibleByIntegerLiteral {
303 | init(integerLiteral n: Int)
304 | { self = .numb(n) }
305 | }
306 |
307 | extension Name : ExpressibleByStringLiteral {
308 | init(stringLiteral: StringLiteralType) {
309 | text = stringLiteral
310 | }
311 | }
312 |
313 | extension Param {
314 | init(_ n: Name, _ t: TypeExpr)
315 | { self.init(name: n, type: t) }
316 | }
317 |
318 | extension TypeExpr : ExpressibleByStringLiteral {
319 | init(stringLiteral s: String)
320 | { self = .name(.init(stringLiteral: s)) }
321 | }
322 |
--------------------------------------------------------------------------------
/Tests/TypedLang/TypedLangTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import XCTest
6 | import TSKit
7 |
8 |
9 | class TypedLangTests : XCTestCase
10 | {
11 | /// Test the translation of various parse trees into *Expr* instances.
12 | func testExpr() throws {
13 | let examples : [(text: String, expr: Expr)] = [
14 | ("a",
15 | .name("a")),
16 | ("1",
17 | .numb(1)),
18 | ("a + b",
19 | .infix_op("+", "a", "b")),
20 | ("-4",
21 | .prefix_op("-", 4)),
22 | ("()",
23 | .tuple([])),
24 | ("(a)",
25 | .name("a")),
26 | ("(a, b)",
27 | .tuple(["a", "b"])),
28 | ("1 + a * 3 ^ -b",
29 | .infix_op("+", 1, .infix_op("*", "a", .infix_op("^", 3, .prefix_op("-", "b"))))),
30 | ("f()",
31 | .apply("f", .tuple([]))),
32 | ("f(1)",
33 | .apply("f", 1)),
34 | ("f(1, 2, 3)",
35 | .apply("f", .tuple([1, 2, 3]))),
36 | ("f(g(x))",
37 | .apply("f", .apply("g", "x"))),
38 | ("!() -> Int. 1",
39 | .lambda([], "Int", 1)),
40 | ("!f() -> Int. 1",
41 | .mu("f", [], "Int", 1)),
42 | ("!(x:Int) -> Int. x + 1",
43 | .lambda([Param("x", "Int")], "Int", .infix_op("+", "x", 1))),
44 | ("!(f: Int -> Int, x: Int) -> Bool. f(x) == 0",
45 | .lambda([Param("f", .arrow("Int", "Int")), Param("x", "Int")], "Bool", .infix_op("==", .apply("f", "x"), 0))),
46 | ("!f(x:Int) -> Int. f(x)",
47 | .mu("f", [Param("x", "Int")], "Int", .apply("f", "x"))),
48 | ("(!(x: Int) -> Int. x + 1)(2)",
49 | .apply(.lambda([Param("x", "Int")], "Int", .infix_op("+", "x", 1)), 2)),
50 | ("p.1(x)",
51 | .apply(.project("p", 1), "x")),
52 | ("f(p.1)",
53 | .apply("f", .project("p", 1))),
54 | ("p.1 + p.2",
55 | .infix_op("+", .project("p", 1), .project("p", 2))),
56 | ("match f(x) { nil() => 1, cons(h,t) => 2 }",
57 | .match(.apply("f", "x"), [
58 | .init(name: "nil", params: [], expr: 1),
59 | .init(name: "cons", params: ["h", "t"], expr: 2),
60 | ])),
61 | ("{ 1 }",
62 | .block(.init(decls: [], expr: 1))),
63 | ("{ let x: Int = 1; x + 2 }",
64 | .block(.init(decls: [.let(.init(param: .init("x", "Int"), expr: 1))], expr: .infix_op("+", "x", 2)))),
65 | ]
66 | for eg in examples {
67 | let expr = try Expr(eg.text)
68 | XCTAssertEqual(expr, eg.expr, eg.text)
69 | }
70 | }
71 |
72 | func testDecl() throws {
73 | let examples : [(text: String, decl: Def)] = [
74 | // Let
75 | ("let x: Int = 1 + 2",
76 | .let(.init(param: .init("x", "Int"), expr: .infix_op("+", 1, 2)))),
77 | // Fun
78 | ("fun f() -> Int { 1 }",
79 | .fun(.init(name: "f", params: [], type: "Int", expr: 1))),
80 | ("fun f(x: X) -> X { f(x) }",
81 | .fun(.init(name: "f", params: [.init("x", "X")], type: "X", expr: .apply("f", "x")))),
82 | ("fun f(x: X, y: Y) -> Z { x + y }",
83 | .fun(.init(name: "f", params: [.init("x", "X"), .init("y", "Y")], type: "Z", expr: .infix_op("+", "x", "y")))),
84 | // Enum
85 | ("enum Bool { false, true }",
86 | .enum(.init(name: "Bool", cases: [
87 | .init(name: "false", params: []),
88 | .init(name: "true", params: []),
89 | ]))),
90 | ("enum OptionT { none, some(x: T) }",
91 | .enum(.init(name: "OptionT", cases: [
92 | .init(name: "none", params: []),
93 | .init(name: "some", params: [.init("x", "T")])
94 | ]))),
95 | ("enum ListT { nil, cons(head: T, tail: ListT) }",
96 | .enum(.init(name: "ListT", cases: [
97 | .init(name: "nil", params: []),
98 | .init(name: "cons", params: [.init("head", "T"), .init("tail", "ListT")])
99 | ]))),
100 | ("typedef MyType = Int",
101 | .typedef(.init(name: "MyType", type: "Int"))),
102 | ]
103 | for eg in examples {
104 | let decl = try Def(eg.text)
105 | XCTAssertEqual(decl, eg.decl, eg.text)
106 | }
107 | }
108 |
109 | func testBlock() throws {
110 | let examples : [(text: String, block: Block)] = [
111 | ("x * 2",
112 | .init(decls: [], expr: .infix_op("*", "x", 2))),
113 | ("let x: Int = 1; x + 2",
114 | .init(decls: [.let(.init(param: .init("x", "Int"), expr: 1))],
115 | expr: .infix_op("+", "x", 2))),
116 | ("let x: Int = 1; let y: Int = 2; x + y",
117 | .init(decls: [
118 | .let(.init(param: .init("x", "Int"), expr: 1)),
119 | .let(.init(param: .init("y", "Int"), expr: 2)),
120 | ],
121 | expr: .infix_op("+", "x", "y"))),
122 | ]
123 | for eg in examples {
124 | let block = try Block(eg.text)
125 | XCTAssertEqual(block, eg.block, eg.text)
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/build_xcframework.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | =begin
4 |
5 | Build TreeSitterCLI.xcframework for the installed rustc targets, minus those specified by --exclude options.
6 |
7 | Options:
8 | -e, --exclude TERM exclude targets whose name, architecture or platform matches the given term
9 | -i, --identity ID code signing identity
10 | -n, --no-sign don't codesign
11 | -t, --tree-sitter PATH the location of the tree-sitter (fork) checkout
12 |
13 | =end
14 |
15 | require 'optparse'
16 | require 'set'
17 |
18 |
19 | working_dir = `echo $PWD`.strip
20 |
21 | # rust comiler target
22 |
23 | class Target
24 | attr_reader :name, :arch, :platform
25 | def initialize(name)
26 | @name = name
27 | elements = name.split("-", 3)
28 | @arch = elements[0]
29 | @platform = elements[2]
30 | end
31 | def match(term)
32 | @name == term || @arch == term || @platform.include?(term)
33 | end
34 | end
35 |
36 | # convenience
37 |
38 | def exec(cmd)
39 | puts(cmd); `#{cmd}`
40 | end
41 |
42 | # Get the installed targets
43 |
44 | targets = `rustup target list --installed`.lines.map{|line|line.strip}.filter{|name|name.include?("-apple-")}.map{|name|Target.new(name)}
45 | if targets.empty?
46 | abort("no installed targets for rustc")
47 | end
48 |
49 | # Extract the command-line options
50 |
51 | exclude = Set[]
52 | tree_sitter_dir = ""
53 | codesign = true
54 | identity = ""
55 |
56 | OptionParser.new do |parser|
57 | parser.banner = "Usage: build_xcframework [options]"
58 | parser.on('-e', '--exclude TERM') { |term|
59 | exclude.add(term)
60 | }
61 | parser.on('-i', '--identity ID') { |id|
62 | identity = id
63 | }
64 | parser.on('-n', '--no-sign') {
65 | codesign = false
66 | }
67 | parser.on('-t', '--tree-sitter PATH') { |path|
68 | if !File.directory?(path)
69 | abort("no such directory: #{path}")
70 | end
71 | tree_sitter_dir = path
72 | }
73 | end.parse!
74 |
75 | if !ARGV.empty?
76 | abort("invalid argument")
77 | end
78 |
79 | # Look for the tree-sitter checkout if unspecified
80 |
81 | if tree_sitter_dir == ""
82 | ["..", "#{working_dir}/.build/checkouts"].each { |prefix|
83 | dir = "#{prefix}/tree-sitter"
84 | if File.directory?(dir)
85 | tree_sitter_dir = dir
86 | break
87 | end
88 | }
89 | if tree_sitter_dir == ""
90 | abort("can't infer location of tree-sitter checkout")
91 | end
92 | end
93 |
94 | # Partition the targets by platform, except those which match the filter options
95 |
96 | targets_by_platform = Hash.new { |hash, platform| set = Set[]; hash[platform] = set; set }
97 | targets.each { |target|
98 | matches = exclude.filter { |term| target.match(term) }
99 | if matches.empty?
100 | targets_by_platform[target.platform].add(target)
101 | end
102 | }
103 | if targets_by_platform.empty?
104 | abort("no build targets")
105 | end
106 |
107 | # Compile targets and build a fat library for each platform
108 |
109 | manifest_path = "#{tree_sitter_dir}/cli/Cargo.toml"
110 | target_dir = "#{working_dir}/.build/cargo"
111 | lib_name = "libtree_sitter_cli.a"
112 |
113 | targets_by_platform.each { |platform, targets|
114 | targets.each { |target|
115 | exec("cargo rustc --release --manifest-path #{manifest_path} --lib --crate-type=staticlib --target-dir #{target_dir} --target #{target.name}")
116 | }
117 | fat_dir = "#{target_dir}/#{platform}"
118 | library_paths = targets.map{|target| "#{target_dir}/#{target.name}/release/#{lib_name}"}
119 | if !File.directory?(fat_dir)
120 | exec("mkdir #{fat_dir}")
121 | end
122 | exec("lipo #{library_paths.join(" ")} -create -output #{fat_dir}/#{lib_name}")
123 | }
124 |
125 | # Bundle the fat libraries as an xcframework
126 |
127 | framework_path = "#{working_dir}/XCFrameworks/TreeSitterCLI.xcframework"
128 |
129 | if File.directory?(framework_path)
130 | exec("rm -rf #{framework_path}")
131 | end
132 |
133 | library_args = targets_by_platform.keys.map { |platform| "-library #{target_dir}/#{platform}/#{lib_name} -headers #{tree_sitter_dir}/cli/include" }.join(" ")
134 | exec("xcodebuild -create-xcframework #{library_args} -output #{framework_path}")
135 |
136 | # Sign the xcframework
137 |
138 | if codesign
139 | if identity == ""
140 | identity = "\"Apple Development:\""
141 | end
142 | exec("codesign --timestamp -s #{identity} #{framework_path}")
143 | end
144 |
--------------------------------------------------------------------------------