├── .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 | --------------------------------------------------------------------------------