Skip to content

Instantly share code, notes, and snippets.

@omochi
Created April 25, 2022 10:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save omochi/d5ac436e4a7541d69ee195bfcc6ee2fe to your computer and use it in GitHub Desktop.
Save omochi/d5ac436e4a7541d69ee195bfcc6ee2fe to your computer and use it in GitHub Desktop.

slidenumber: true autoscale: true

SwiftSyntaxを

うまく使おう

わいわいswiftc #35

@omochimetaru


Swiftコードを読み取りたい

  • メソッドを自動生成したい
  • 他の言語に変換したい

SwiftSyntaxを使えば簡単?

そうでもない


今日の話

  • BinarySwiftSyntax を作った
  • SwiftTypeReader を作った

SwiftSyntaxおさらい

  • Swiftコンパイラ内部に、新しく綺麗に作り直されたパーサモジュール libSyntax がある

  • コード編集などを考慮したAPIを持つ

  • それをSwiftから使うためのライブラリが SwiftSyntax 1

  • SwiftPMパッケージとして提供


謎のバージョン選択

inline


パーサ本体は別配布

  • 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)

これでコード生成し放題?

そうでもない


SwiftSyntaxは難しい


Swift AST Explorer 3 @k_katsumi

inline


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を使ってコード生成をしよう

Footnotes

  1. https://github.com/apple/swift-syntax

  2. https://github.com/omochi/BinarySwiftSyntax

  3. https://swift-ast-explorer.com

  4. https://github.com/omochi/SwiftTypeReader

  5. https://github.com/omochi/SE0295Polyfill

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment