slidenumber: true autoscale: true
- メソッドを自動生成したい
- 他の言語に変換したい
- BinarySwiftSyntax を作った
- SwiftTypeReader を作った
-
Swiftコンパイラ内部に、新しく綺麗に作り直されたパーサモジュール
libSyntax
がある -
コード編集などを考慮したAPIを持つ
-
それをSwiftから使うためのライブラリが
SwiftSyntax
1 -
SwiftPMパッケージとして提供
-
SwiftSyntax
はあくまでただのlibSyntax
へのSwiftブリッジ -
libSyntax
はXcodeと一緒に配布 → 実行環境によってバージョンが異なる -
SwiftSyntax
のバージョンを固定できても、実行環境のlibSyntax
のバージョンが固定できない
Aさん「このコードジェネレータはSwiftSyntaxを使っているから、Xcode13.3を xcode-select
で指定してから実行してください。」
Bさん「まだXcode13.2しか入ってないです。」
Cさん 「Xcode14.0 betaで作業してるから切り替えたくないです。」
-
libSyntax
も一緒に配布する -
SwiftPMの binary target なら、xcframework の中に
libSyntax
を埋め込める -
ついでにソース部分のビルド時間もカットできる
BinarySwiftSyntax 2
-
SwiftSyntaxをビルドするxcodeprojを作成
-
手元のXcodeから
libSyntax
を抽出
$ cp "$(xcode-select -p)"/Toolchains/XcodeDefault.xctoolchain
/usr/lib/swift/macosx/lib_InternalSwiftSyntaxParser.dylib SwiftSyntax/Deps
- xcframeworkにする
$ xcodebuild archive \
-project SwiftSyntax.xcodeproj \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO
$ xcodebuild -create-xcframework \
-framework build/UninstalledProducts/macosx/SwiftSyntax.framework \
-output dist/Xcode12.5/SwiftSyntax.xcframework
簡単に使える
// package.swift
let package = Package(name: "foo",
products: [ .executable(name: "foo", targets: ["foo"]) ],
dependencies: [
.package(url: "https://github.com/omochi/BinarySwiftSyntax", branch: "main")
],
targets: [
.target(name: "foo",
dependencies: [
// v ここでバージョン指定できる
.product(name: "SwiftSyntax-Xcode13.0", package: "BinarySwiftSyntax")
])
]
)
// main.swift
import SwiftSyntax
let sourceFile: SourceFileSyntax = try SyntaxParser.parse(source: source)
Swift AST Explorer 3 @k_katsumi
let file = try SyntaxParser.parse(source: source)
for statement in file.statements {
if let structDecl = statement.item.as(StructDeclSyntax.self) {
...
}
}
.item
?.as(StructDeclSyntax.self)
?
for decl in structDecl.members.members {
if let varDecl = decl.decl.as(VariableDeclSyntax.self) {
...
}
}
.members.members
?decl.decl
?.as(VariableDeclSyntax.self)
?
-
奇妙なAPIが難しい
-
as
メソッドによるダウンキャストが難しい (swift-ast-explorer必須) -
Xcodeが重くて定義がよく見えない (型が多すぎる?)
-
SwiftSyntaxは構文レベルのライブラリなので、自由度や表現能力が高いが、その分複雑で難しい
-
C++向けに作った
libSyntax
とのブリッジの関係で(?)設計にクセがある -
構文レベルのライブラリなので、型情報の名前解決など、意味論レベルはサポートされない
-
用途に応じたライブラリを一つ挟んで使いたい
SwiftTypeReader 4
-
SwiftSyntaxをラップして、型情報を扱いやすいAPIで提供するライブラリ
-
型以外の情報 (関数の本文など) は扱わない
func testSimple() throws {
let result = try XCTReadTypes("""
struct S {
var a: Int?
}
"""
)
let s = try XCTUnwrap(result.module.types[safe: 0]?.struct)
XCTAssertEqual(s.name, "S")
XCTAssertEqual(s.location, Location([.module(name: "main")]))
XCTAssertEqual(s.storedProperties.count, 1)
let a = try XCTUnwrap(s.storedProperties[safe: 0])
XCTAssertEqual(a.name, "a")
let aType = try XCTUnwrap(a.type().struct)
XCTAssertEqual(aType.module?.name, "Swift")
XCTAssertEqual(aType.name, "Optional")
XCTAssertEqual(try aType.genericArguments().count, 1)
let aWrappedType = try XCTUnwrap(aType.genericArguments()[safe: 0]?.struct)
XCTAssertEqual(aWrappedType.module?.name, "Swift")
XCTAssertEqual(aWrappedType.name, "Int")
}
.storedProperties
func testEnum() throws {
let result = try XCTReadTypes("""
enum E {
case a
case b(Int)
case c(x: Int, y: Int)
}
"""
)
let e = try XCTUnwrap(result.module.types[safe: 0]?.enum)
do {
let c = try XCTUnwrap(e.caseElements[safe: 0])
XCTAssertEqual(c.name, "a")
}
do {
let c = try XCTUnwrap(e.caseElements[safe: 1])
XCTAssertEqual(c.name, "b")
let x = try XCTUnwrap(c.associatedValues[safe: 0])
XCTAssertNil(x.name)
XCTAssertEqual(try x.type().name, "Int")
}
do {
let c = try XCTUnwrap(e.caseElements[safe: 2])
XCTAssertEqual(c.name, "c")
let x = try XCTUnwrap(c.associatedValues[safe: 0])
XCTAssertEqual(x.name, "x")
XCTAssertEqual(try x.type().name, "Int")
let y = try XCTUnwrap(c.associatedValues[safe: 1])
XCTAssertEqual(y.name, "y")
XCTAssertEqual(try y.type().name, "Int")
}
}
.caseElements
.associatedValues
func testGenericParameter() throws {
let result = try XCTReadTypes("""
struct S<T> {
var a: T
}
"""
)
let s = try XCTUnwrap(result.module.types[safe: 0]?.struct)
XCTAssertEqual(s.name, "S")
XCTAssertEqual(s.genericParameters.count, 1)
let t = try XCTUnwrap(s.genericParameters[safe: 0])
XCTAssertEqual(t.name, "T")
XCTAssertEqual(s.storedProperties.count, 1)
let a = try XCTUnwrap(s.storedProperties[safe: 0])
XCTAssertEqual(a.name, "a")
let at = try XCTUnwrap(a.type().genericParameter)
XCTAssertEqual(
at.location,
Location([
.module(name: "main"),
.type(name: "S")
])
)
}
.genericParameter.location
利用例: SE0295Polyfill 5
- SE-0295 (enumのcodable対応) をコード生成するライブラリ
- Swift5.5 がリリースされたので既に引退
enum Command: Codable {
case load(key: String)
case store(key: String, value: Int)
}
extension Command {
enum CodingKeys: Swift.CodingKey {
case load
case store
}
enum LoadCodingKey: Swift.CodingKey {
case key
}
enum StoreCodingKey: Swift.CodingKey {
case key
case value
}
}
extension Command {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .load(key: let key):
var nestedContainer = container.nestedContainer(keyedBy: LoadCodingKey.self, forKey: .load)
try nestedContainer.encode(key, forKey: .key)
case .store(key: let key, value: let value):
var nestedContainer = container.nestedContainer(keyedBy: StoreCodingKey.self, forKey: .store)
try nestedContainer.encode(key, forKey: .key)
try nestedContainer.encode(value, forKey: .value)
}
}
}
extension Command {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.allKeys.count != 1 {
let context = DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Invalid number of keys found, expected one."
)
throw DecodingError.typeMismatch(Command.self, context)
}
switch container.allKeys.first.unsafelyUnwrapped {
case .load:
let nestedContainer = try container.nestedContainer(keyedBy: LoadCodingKey.self, forKey: .load)
let key = try nestedContainer.decode(String.self, forKey: .key)
self = .load(key: key)
case .store:
let nestedContainer = try container.nestedContainer(keyedBy: StoreCodingKey.self, forKey: .store)
let key = try nestedContainer.decode(String.self, forKey: .key)
let value = try nestedContainer.decode(Int.self, forKey: .value)
self = .store(key: key, value: value)
}
}
}
- SwiftSyntaxは
libSyntax
を同梱してほしい - SwiftTypeReaderを使ってコード生成をしよう