元同僚(😭)の @giginet 氏のリモート送別会中に雑談でSwift 5.7の正規表現の話をしていたらじゃあわいわいSwiftcでソースコードリーディング会やってくださいよ〜ということで何となく参加が決まった。 時差の問題があるため、わいわいswiftcは初参加だしSwiftのコンパイラーについてもほとんど何も知らないので弱々人材なのでお手柔らかにお願いします。
apple/swift-experimental-string-processingというリポジトリについて何が起こっているのかを紹介。コードリーディング会なので途中から各自読みつつ考えたことなどを議論してもらいながら進められたら良いと思う。自分が答えられるかどうかはわからないけど随時どんどん突っ込んでもらいわいわいしていきましょう。
Swift5.7でのRegexの実装に少し詳しくなって、このイベント以降参加者自身でコードを読み進められるような状態になるのが目標。 関連する仕様やそこそこ複雑な技術・実装が多いので100%完全理解するのはこの会では諦める。特にUnicodeの仕様や正規表現のマッチングエンジンの実装方法そのものなどは大変。
- Satoshi Namai / 生井智司
- https://twitter.com/ainame
- https://github.com/ainame
普段はCookpad limitedというクックパッド株式会社の子会社の英国支店で働いていて、アプリの機能開発をしたりCI/CDや各種自動化だったり、 基盤寄りの仕事をしている。オフィスはロンドンではなくブリストルという地方都市ですが結構良いところ。 アプリの機能開発・基盤系のポジション両方で募集中なので、興味がある方は@ainameまでDM or Meetyからカジュアル雑談しましょう。
- https://www.cookpadteam.com/careers
- https://apply.workable.com/cookpad/j/D68FA3EC4B/
- https://meety.net/matches/BHfdWgSGfAxr
- Xcode 14 betaをダウンロードしてunxip
- swift.orgから5.7のsnapshotをダウンロードしてXcode 14 betaで使えるようにする https://www.swift.org/download/
- git clone https://github.com/apple/swift-experimental-string-processing.git
- swift/releases/5.7ブランチをチェックアウト
- 適当にMonterey上で実行して遊ぶだけならsnapshotを入れてiOS simulator(iOS 16)上で実行すれば良い(?他に良いやり方があれば教えてください)
- プロジェクトの全部のターゲットをコンパイルをするためにはmacOSをVentura betaにあげると良い(Xcode 13が使えなくなるので注意)
Swift 5.7の新しい正規表現について何も知らないよという人は以下のWWDCのセッションを見る
- Meet Swift Regex https://developer.apple.com/videos/play/wwdc2022-110357
- Swift Regex: Beyond the basics https://developer.apple.com/videos/play/wwdc2022-110358
apple/swift-experimental-string-processing
の各種実装を理解するために以下のswift-evolutionのproposalsを先に目を通しておくと便利。
(この辺の話をまとめてiOSDCで話そうと考えています。)
- 0348-buildpartialblock https://github.com/apple/swift-evolution/blob/main/proposals/0348-buildpartialblock.md
- 0350-regex-type-overview https://github.com/apple/swift-evolution/blob/main/proposals/0350-regex-type-overview.md
- 0351-regex-builder https://github.com/apple/swift-evolution/blob/main/proposals/0351-regex-builder.md
- 0354-regex-literals https://github.com/apple/swift-evolution/blob/main/proposals/0354-regex-literals.md
- 0355-regex-syntax-run-time-construction https://github.com/apple/swift-evolution/blob/main/proposals/0355-regex-syntax-run-time-construction.md
- 0357-regex-string-processing-algorithms https://github.com/apple/swift-evolution/blob/main/proposals/0357-regex-string-processing-algorithms.md
- 0363-unicode-for-string-processing https://github.com/apple/swift-evolution/blob/main/proposals/0363-unicode-for-string-processing.md
apple/swift-experimental-string-processing
でいろいろなアプローチを試した結果がswift-evolutionのproposalsに至ったのだと推測しています。
An early experimental general-purpose pattern matching engine for Swift.
と書いてある。最初期は宣言的な文字列処理をするためのパターンマッチングエンジンというところから始まっているのが分かる。 https://forums.swift.org/t/declarative-string-processing-overview/52459
_RegexParser
and_StringProcessing
are specially integrated modules that are built as part of apple/swift. Specifically,_RegexParser
contains the parser for regular expression literals and is built both as part of the compiler and as a core library._CUnicode
and_StringProcessing
are built together as a core library named_StringProcessing
.
_RegexParser
と_StringProcessing
は特別にapple/swiftに統合されていて特に_RegexParser
はコアライブラリだけではなく正規表現リテラルをパースするためのコンパイラーとしても組み込まれている。
_CUnicode
は_StringProcessing
の一部としてコアライブラリに組み込まれている。
Module | Swift toolchain component |
---|---|
_RegexParser |
SwiftCompilerSources/Sources/_RegexParser and stdlib/public/_RegexParser |
_CUnicode |
stdlib/public/_StringProcessing |
_StringProcessing |
stdlib/public/_StringProcessing |
- mainで開発されている
- apple/swiftに統合していくためにapple/swiftのmainやrelease/5.7に対応するブランチも存在している
実際のライブラリ部分だけではなく動作確認やベンチマーク用のスクリプトのターゲットも含まれている。
- _RegexParser
- _CUnicode
- _StringProcessing
- RegexTests
- RegexBuilderTests
- MatchingEngineTests
- VariadicsGenerator
- PatternConverter
- RegexTester
Exercises are tasks that various approaches or conformers can tackle. The idea is to support naive, hand-written, regex, PEG, consumer, and arbitrary 3rd party libraries to demonstrate their approaches and "compete" with each other.
This particular formulation is a straw-person and we may rearchitect it a bit.
開発初期段階でいろんな文字列処理の手法を比較するために作られたターゲット。 もう使われていなさそう。
- Exercises
- ExercisesTests
SPM準拠のSources
とTests
の構成 + swift-evolutionのためのdraftなどが入っているDocumentation
、ちょっとしたPythonのスクリプトが入っているUtils
。
あとCMakeでビルド出来るようになっている。
% tree -d -L 2 .
.
├── CMakeFiles
│ └── 3.22.2
├── Documentation
│ └── Evolution
├── Sources
│ ├── Exercises
│ ├── PatternConverter
│ ├── RegexBenchmark
│ ├── RegexBuilder
│ ├── TestSupport
│ ├── VariadicsGenerator
│ ├── _CUnicode
│ ├── _RegexParser
│ └── _StringProcessing
├── Tests
│ ├── ExercisesTests
│ ├── MatchingEngineTests
│ ├── Prototypes
│ ├── PrototypesTests
│ ├── RegexBuilderTests
│ ├── RegexTests
│ └── UnicodeTests
└── Utils
└── gen-unicode-data
swift-evolutionからリンクされている.mdファイルがあったりドラフトが置かれていたりするが、BigPicture.mdというファイルが初期コミットで書かれていて当時の意気込みなどが伺える。 https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Documentation/BigPicture.md
I've been finding it helpful to think of our long-term goal as making Swift awesome at string processing, data processing, and "event processing" (working title, suggestions welcome). These are not rigid or clear-cut distinct domains (they actually blend together in extremity) so much as they are 3 interesting "regions" in this design space. Thinking about these regions helps clarify what tasks we're enabling and helps push us towards more general solutions.
Each of these regions share technical fundamentals, but present novel performance and API design challenges. I hope that keeping the big picture in mind will help guide the design process towards pragmatic trade-offs and robust solutions.
By "string processing" (at least in the context of this document), I mean processing with the Unicode-rich semantics of Swift's String and Character types. By "data processing", I mean efficient processing done at a binary semantics level, even if such data happens to be viewable as text. By "event processing" (working title, suggestions welcome), I mean being able to detect and respond to patterns over ephemeral "events" issued from an asynchronous source.
We want to be able to compose, layer, and even interweave different kinds of processing together. And, we want these areas to be library-extensible, so that libraries can provide custom behavior through custom protocol conformances. For example, custom String interpolation is extended by libraries for logging, sanitizing, templating, and many other applications. Similarly, there are myriad formulations of pattern matching and we want to enable libraries to provide powerful new abstractions.
一番最初期には正規表現(Regex)という単語が含まれていなかったのでリポジトリ名の通り本当に文字列処理全般を改善するのが狙いだったように見える。 以下の3つの挑戦的なエリアで改善を目指している。俺の考えた最強の文字列処理みたいになっている。
- String processing
- Data processing
- Event processing
Unicode-richなSwiftのString
に基づいた文字列処理、例えばextended grapheme clusters(複数のUnicodeのcode pointで構成されている文字など)が正しく扱えるとか。
例:
Matching "🧟♀️" using . wildcard |
Matched | Remaining content |
---|---|---|
String | 🧟♀️ | "" |
String.UnicodeScalarView | U+1F9DF | U+200D U+2640 |
String.UTF8View | F0 | 9F A7 9F E2 80 8D E2 99 80 |
また文字列処理をライブラリで拡張しやすいようにprotocolを提供したり、パーサーをcomposableにすることで例えばFoundationのFormatterのFormatStyleみたいなものでローカライズの対応が出来るようにしたり。
さらにCollection
に基づく文字列処理も一般化も狙いたい(String
/Substring
はBidirectionalCollection
なので)と言っているが、今のところ実現はしてなさそう。
String processingの話とは逆に、目に見える文字列としての文字列処理とデータの処理は異なる。例えばCSVファイルの中にU+0301
(アクセント記号)だけの列があったとする
... , U+301 , ...
U+0301
はextended grapheme clustersとして前の文字列にアクセント記号として見た目上結合するのでただの文字列として処理するならば結合した1文字として扱うべきだが、
CSVファイルのData processingとしては,
はfield separatorとして扱われるべきという話。
irb(main):004:0> "X\u{0301}"
=> "X́"
irb(main):005:0> ",\u{0301}"
=> ",́"
Swiftの文字列処理ではこのbinary semantics-levelとSwiftのString
のsemanticsでのパースを提供できるようにしたい。他の例では
JSONのパース処理は全体をbinary semanctics-levelでパースしてDictionary
に変換するが、コンテンツはString
のsemanticsで取り扱うなど。
またAsyncSequence
をソースとして扱うような場合にBacktrackingなどの挙動の制限や、Collection
の処理をどのようにあつかpeek
/consume
patternなどが適用できるのかどうか。いろいろ考えることが増え、Event processingという考えに至っている。
例えばサーバーが任意のユーザーの入力が事前に承認されているかどうかをチェックして承認されていた場合のみ正しく処理して、そうでない場合は任意のフックを呼び出してサーバーを停止してセキュリティレポートを作成するなど、非同期のAPIに対しての設計上の挑戦があると言っているが、具体的に何か現時点で実装されているのかどうかは良くわかりませんでした(by ainame)。
Parser combinatorのような関数を呼び出したり組み合わせたりしてコードをただ書くだけのような体験を提供したい→ Regex Builderが出来た?
apple/swiftにおけるICUへの依存を無くすための実装をこのリポジトリでの開発向けに重複して持っていてapple/swiftに統合されるときには重複がなくなっているのかも?(推測) apple/swift内では同じようなコードがC++で実装されているがこちらはC。
- 関連issues
- apple/swift#52935
- https://forums.swift.org/t/icu-usage-in-swift/20473 (Linux向けのSwiftをビルドするのにICUへの依存があるせいでautotoolsが必要になるのが厄介という別の観点の議論)
- ICU(NSRegularExpressionで使われている)は正準等価(Canonical Equivalent)での文字列比較に対応していないためSwiftのStringのモデルと異なるという課題があった
- ICUにはUCD(Unicode Character Database)というデータが含まれていて良い感じにアクセス出来るインターフェイスがあるがそれをSwift向けに最適化したデータとしてScriptData.hにハードコーディングしてある
_ RegexリテラルのParser。コンパイラーからとランタイムのRegexのinitializerで使われている。 https://github.com/apple/swift/blob/main/stdlib/public/RegexParser/CMakeLists.txt
- Lexerへのブリッジ
- コンパイラーへのインターフェイスを提供している箇所 https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_RegexParser/Regex/Parse/CompilerInterface.swift#L41-L44
- Swiftでのラッパー関数 https://github.com/apple/swift/blob/4e1a1b82b974f6fd9b711c3d9a81de76ddf275fa/SwiftCompilerSources/Sources/Parse/Regex.swift#L76-L98
- Lexerのstatic変数で参照を保持(C++) https://github.com/apple/swift/blob/4e1a1b82b974f6fd9b711c3d9a81de76ddf275fa/lib/Parse/Lexer.cpp#L36-L40
parse<S: StringProtocol>(_:S,_:SyntaxOptions)
というASTを返すグローバル関数が実装されていてそれがRegex<Output>.init
からも呼ばれていたりする https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_RegexParser/Regex/Parse/Parse.swift#L648-L653Source
に入力を持たせてparseする
public struct Source {
var input: Input // prototype上では一旦 typealias Input = String となっている
var bounds: Range<Input.Index>
}
struct Regex<Output>
やマッチングのロジックなどが実装されているメイン箇所。
https://github.com/apple/swift/blob/main/stdlib/public/StringProcessing/CMakeLists.txt
% tree -L 1 Sources/_StringProcessing
Sources/_StringProcessing
├── Algorithms
├── ByteCodeGen.swift
├── CMakeLists.txt
├── Capture.swift
├── Compiler.swift
├── ConsumerInterface.swift
├── Engine
├── Executor.swift
├── MatchingOptions.swift
├── PrintAsPattern.swift
├── Regex
├── Unicode
├── Utility
└── _CharacterClassModel.swift
% tree -L 1 Sources/_StringProcessing/Algorithms
Sources/_StringProcessing/Algorithms
├── Algorithms # Collectionのextension
├── Consumers #
├── Matching
└── Searchers
public struct Regex<Output>
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Regex/Core.swift
let program: Program
にコンパイル済みの正規表現が格納されている
struct Regex<Output>.Program
struct DSLTree
を保持しているstructDSTLTree
は_RegexParser.AST
とは別のTree表現で、_RegexParser.AST
から変換出来るしRegexBuilder
でRegex<Output>
を組み立てる時にも使われている
Regex<Output>.init(_ pattern:_String) where Output == AnyRegexOutput
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Regex/AnyRegexOutput.swift#L142-L147
- initializerに文字列を渡す場合、capture groupは型消去された状態(?)になるためAnyRegexOutput.swiftにpublicなinitializerが定義されている
public protocol RegexComponent<RegexOutput>
RegexBuilder
を作る時に重要なプロトコル (primary associated typeが早速使われている?) https://github.com/apple/swift-evolution/blob/main/proposals/0346-light-weight-same-type-syntax.mdRegex<Output>
自身がconformしていて、Regex<Output>
はRegexComponent<RegexOutput>
の組み合わせで出来ている
public struct Regex<Output>.Match
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Regex/Match.swift
- Matchした結果は
AnyRegexOutput
で保持されているがdynamic member lookupで参照する際にはRegex.Output
の型にcastされて返る - Tupleの型として取り出す際に難しそうなことをしている https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Regex/Match.swift#L38-L40
正規表現のマッチングエンジンは大きく分けるとDFA(決定性有限オートマトン)型とVM型に分かれるらしくSwiftは後者のVM型での実装。 https://qiita.com/yyu/items/84b1a00459408d1a7321 https://qiita.com/reonyanarticle/items/bc2e683fad476ec17310
DFA型にすべきかかどうかなどの議論はswift-evolutionでも軽く言及されていてcode sizeの面でVM型のbytecodeにした方がサイズが小さく済むので良いと言われている(?)。 ただし最適化はfuture workとされている。 https://github.com/apple/swift-evolution/blob/0823dc6fc678e3a91a812a49b1830f61abc2e04a/proposals/0350-regex-type-overview.md#future-work-static-optimization-and-compilation
struct Executor
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Executor.swift
Regex<Output>.firstMatch(in:)
から呼び出されているエントリポイントRegex<Output>.program
を実行するイメージRegex<Output>.Program
をMEProgram
(ME = Macthing Engine?)というIR(intermediary representation)に変換して扱う
func _match(
_ input: String,
in subjectBounds: Range<String.Index>,
mode: MatchMode = .wholeString
) throws -> Regex<Output>.Match? {
let executor = Executor(program: regex.program.loweredProgram)
return try executor.match(input, in: subjectBounds, mode)
}
class Compiler
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Compiler.swift
DSLTree
をByteCodeGen
に渡してMEProgram
に変換してくれるやつstruct ByteCodeGen
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/ByteCodeGen.swift
Regex<Output>.Program
をIRに変換するためにMEProgram.Builder
というstructにInstruction
などを詰め込んでいく処理- ちなみにDFAなどの
struct Engine
struct Processor
- https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Engine/Processor.swift
ByteCode
になった[Instruction]
を実際に実行している- match系のメソッドが呼ばれるとaccept/failの状態になるまで
Processor.cycle()
で[Instruction]
を実行し続ける https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Engine/Consume.swift#L44-L54
SE-0357 https://github.com/apple/swift-evolution/blob/main/proposals/0357-regex-string-processing-algorithms.md で提案されているCollection
の
各種メソッドにおける正規表現の利用などの実装が https://github.com/apple/swift-experimental-string-processing/blob/swift/release/5.7/Sources/_StringProcessing/Algorithms
に実装されている。
let bool = "Hello World!".contains(/Hello .*!*/) // => true
Collection
に対してはSearcherを経由してRegexを実行することで、操作に応じて最適なアルゴリズムで検索処理を呼び出せるようになっているように見える。
https://github.com/apple/swift-evolution/blob/main/proposals/0357-regex-string-processing-algorithms.md#string-and-collection-algorithm-additions
Result builderを使ったDSL。SwiftのRegexではRegex.Output
がTupleで表現されているがCaputreの数だけTuple内の型が必要になる。
それによってResult Builder実装に必要なbuildBlockメソッドに必要なgenericsの型の数の組み合わせが爆発してしまう。
SE-0348によってResult Builderの実装が楽になっている。
自前でマッチングエンジンを実装しているためTryCapture
やCapture
に渡せるtransform
引数のclosureがIntstruction.Payload
に格納されて
closure内でnil
を返した時にbacktrackを起こせるようになっている。
おわり