Skip to content

Instantly share code, notes, and snippets.

@milseman
Created January 10, 2018 19:49
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save milseman/bb39ef7f170641ae52c13600a512782f to your computer and use it in GitHub Desktop.
Save milseman/bb39ef7f170641ae52c13600a512782f to your computer and use it in GitHub Desktop.
State of String: ABI, Performance, Ergonomics, and You!

State of String: ABI, Performance, Ergonomics, and You!

Hello, I’ve been working on implementing, optimizing, and improving String in preparation for ABI stability, and I thought I’d share the current status of String in Swift 5 and some potential directions to go. This is the product of conversations with open source contributors and my colleagues on the Swift standard library team at Apple.

The primary area of focus is stabilizing String’s ABI, but we’d be remiss if we couldn’t also fit in performance and ergonomic improvements. String’s ergonomics in particular is one area where we think the status quo is woefully inadequate, and over half of this email is devoted to that topic. At the end, there’s a section about a community initiative that we hope can help users of String as well as guide future development.

(Note: I’m sending this to swift-dev because much of the contents revolve around implementation concerns. I’ll also cross-reference on swift-evolution and swift-users. See also the StringManifesto from Swift 4 era, which elaborates more on the philosophy and justification of many of the things discussed here.)

String ABI

String’s ABI consists of its in-memory layout, any methods that are public or callable from inlineable methods, and any interpretation of its bit pattern by inlineable methods. The challenge in designing String’s ABI is determining what avenues are worth keeping open to pursue in the future and what should be closed off for good to ensure performance.

In a non-ABI-stable world, the Swift optimizer is able to peer deeply into the bowels of String’s implementation to perform analyses and optimizations. But, ABI stability introduces resilience barriers through which the optimizer cannot peer. Keeping reasonable performance while stabilizing an ABI requires careful crafting of the String type as well as reevaluating many implementation choices. This area is critical to String’s long term health, even though it has little user-visibility in the short term.

For example, as an implementation detail, String uses UTF-16 as its underlying encoding. For values that fit within the first 7-bit subset of UTF-16 (that is, ASCII), String uses 1-byte backing storage to save space. UTF-16 provides the best interop story for application development and interacting with ICU. But, other domains such as scripting, systems programming, and server-side development strongly prefer UTF-8 and transcoding may not serve their performance needs. If this implementation detail can remain behind a resilience barrier, that is if the performance cost is not too high, then String could evolve in the future to natively support backing UTF-8 storage.

The details are still in flux, and likely not of interest to most readers. Much of the re-gutting work is happening on the appropriately-named StringGuts branch.

String Performance

Strings are pervasive in any system and their performance matters. We’re planning on two major efforts to improve performance this release: comparison improvements and small-string optimizations. Additionally, internal to the standard library, we’re introducing and using unmanaged strings and some performance flags, which may be worth surfacing as API for highly-performance-sensitive uses.

String Comparison

Strings should compare consistently, with all the nice properties we know and love such as transitivity. String honors Unicode canonical equivalence, i.e. comparison is modulo-normalization. This means that ự (U+1EF1: LATIN SMALL LETTER U WITH HORN AND DOT BELOW) compares equivalent to ư ◌̣ (U+01B0 U+0323: LATIN SMALL LETTER U WITH HORN, COMBINING DOT BELOW) and u ◌̣ ◌̛ (U+0075 U+0323 U+031B: LATIN SMALL LETTER U, COMBINING DOT BELOW, COMBINING HORN), and all the other wonderful wacky permutations thereof.

String’s ordering is not sufficient for human presentation, as there is no ordering that’s appropriate for all languages or uses. For example, ö comes before z in German, but not in Swedish. String’s comparison operators (== and <) do not attempt to provide locale-specific orderings, but instead a universal ordering, that is one which is useful for maintaining programmer invariants such as the sorted-ness of a data structure. String’s choice to honor Unicode canonical equivalence strikes a balance between universality and efficiency while helping most programmers avoid common pitfalls in dealing with Unicode.

(Quick note on localization: Localization is not within the purview of the standard library as it’s better provided by the platform, i.e. Foundation. If you’re presenting an ordering to a user, use a locale-aware method from Foundation such as localizedStandardCompare().)

Given that ordering is not fit for human consumption, but rather machine processing, it might as well be fast. The current ordering differs on Darwin and Linux platforms, with Linux in particular suffering from poor performance due to choice of ordering (UCA with DUCET) and older versions of ICU. Instead, String Comparison Prototype provides a simpler ordering that allows for many common-case fast-paths and optimizations. For all the Unicode enthusiasts out there, this is the lexicographical ordering of NFC-normalized UTF-16 code units.

Small String Optimization

Many strings are small. It is wasteful and inefficient to store them on the heap and track their lifetimes when the String struct could store it inline, i.e. encoded directly in the struct’s bit pattern. Substrings, if they cover a small span of a potentially-large String, can avoid retaining ownership of the storage and instead encode their contents inline with an offset to map indices.

NSString has leveraged this kind of optimization on 64 bit platforms to great effect through tagged-pointer representations. Swift’s String has the advantage of being a struct and, depending on the final ABI, having more bits and flags available. We will probably have multiple small string representations.

For example, assuming a 16-byte String struct and 8 bits used for flags and discriminators (including discriminators for which small form a String is in), 120 bits are available for a small string payload. 120 bits can hold 7 UTF-16 code units, which is sufficient for most graphemes and many common words and separators. 120 bits can also fit 15 ASCII/UTF-8 code units without any packing, which suffices for many programmer/system strings (which have a strong skew towards ASCII).

We may also want a compact 5-bit encoding for formatted numbers, such as 64-bit memory addresses in hex, Int.max in base-10, and Double in base-10, which would require 18, 19, and 24 characters respectively. 120 bits with a 5-bit encoding would fit all of these. This would speed up the creation and interpolation of many strings containing numbers.

Final details are still in exploration. If the construction and interpretation of small bit patterns can remain behind a resilience barrier, new forms could be added in the future. However, the performance impact of this would need to be carefully evaluated.

Unmanaged Strings

One of the ways that the standard library internally is dealing with the upcoming optimization barriers is by restructuring much of String’s implementation to be on top of an unmanaged form. These are rough analogues to UnsafeBufferPointer, i.e. a pointer and length with flags and string-semantic operations defined on them. Like small strings, these still have type String as they are just a different form encoded in the struct’s bit pattern.

Such unmanaged strings can be used when the lifetime of the storage is known to outlive all uses. As such, they don’t need to participate in reference counting. In the future, perhaps with ownership annotations or sophisticated static analysis, Swift could convert managed strings into unmanaged ones as an optimization when it knows the contents would not escape. Similarly in the future, a String constructed from a Substring that is known to not outlive the Substring’s owner can use an unmanaged form to skip copying the contents. This would benefit String’s status as the recommended common-currency type for API design.

Some kind of unmanaged string construct is an often-requested feature for highly performance-sensitive code. We haven’t thought too deeply about how best to surface this as API and it can be sliced and diced many ways. Some considerations:

  • Does it make sense to introduce a new type (or types) here, or is it better to base everything on a sort of withUnsafeCodeUnits-like construct that supplies UnsafeBufferPointers?
  • How does 1-byte or 2-byte backing storage surface? Some options:
    • Closures receive optional arguments. E.g. withUnsafeUTF16CodeUnits passes an UnsafeBufferPointer<UInt16>? to its closure. String would also have to be queryable as to the nature of the underlying storage, and those queries also become new API and ABI.
    • The backing storage is abstracted away sufficiently. I.e. the 1-byte or 2-byte decision is tracked by the type provided by e.g. withUnsafeCodeUnits. This trades a little performance for simpler usage. This would probably require a new type which in turn is new API and ABI.
    • Operations are given an enum of the different forms. That enum also becomes API as well as ABI.
      • Is the enum open or closed? Can this evolve over time?
    • Note that whether the underlying storage is 1-byte or 2-byte is not necessarily stable version-to-version, e.g. if String utilized UTF-8 storage for mostly-ASCII Strings in the future.
  • Many Strings are effectively immutable and immortal, as in, the String’s lifetime will definitely outlive all potential uses (e.g. String literals). Would this work also encompass uses of StaticString? Can they be unified?
  • What about a low-level API for efficiently building Strings? Could the user call reserveCapacity and then get a buffer pointer to excess capacity, updating count along the way?
  • If this is niche and specifically for performance, should it be UnsafeString instead and skip bounds checks in release configurations?
  • The APIs need to be designed and named. UnmanagedString’s subscript loads raw code units, but it also provides the functionality to perform grapheme breaking (as String is layered on top of it). How can this be designed in a way that implies semantic symmetry with String?
  • How would small strings operate? Should the APIs fail on them, or receive a scratch buffer to spill them into, or be abstracted by the type?

Performance Flags

Many operations such as decoding, comparison (under the new model), and grapheme breaking have fast-paths for common situations where the portion of the String under consideration does not exhibit the full complexity of Unicode. We plan on introducing flags denoting whether the entire String has these properties, allowing the implementation to just execute these fast-paths without repeatedly querying the underlying code units and with better instruction locality. Unsetting a flag will always preserve semantics, just slightly regress performance (the fast-paths will remain). Some flags include:

  • isTriviallyEncoded: If all scalars are comprised of a single code unit, then decoding is trivial: just zero-extend.
  • isCanonical: If a String is already in our preferred canonical form for comparison purposes, then honoring canonical equivalence is trivial.
  • isSingleScalarGrapheme: If all graphemes are composed of a single scalar, then grapheme breaking is trivial.

String is a struct and these flags are stored inline, so they can only be set upon creation or inside a mutable method. In general, it’s not worth expensive checks to set these flags or onerous bookkeeping to maintain them, but a best-effort can be made when it’s cheap to do so. Since unsetting a flag is semantics-preserving, flags can be reserved for future use by always setting them to 0 in the current version. In this way, flags can be added in a backwards-compatible way, where older versions ignore them.

For highly performance-sensitive code, it might be worth having a mutating method to perform a linear scan and update these flags. This should be a very fast check to update inline flags, and exposing it as API should probably also expose getters to query flags. We haven’t thought deeply about how best to surface this for judicious use without creating an easily mis-applied API littered throughout code bases.

Miscellaneous

There are many other tweaks and improvements possible under the new ABI prior to stabilization, such as:

  • Guaranteed nul-termination for String’s storage classes for faster C bridging.
  • Unification of String and Substring’s various Views.
  • Some form of opaque string, such as an existential, which can be used as an extension point.
  • More compact/performant indices.

String Ergonomics

Ergonomics is an area that’s not been well-served by String. Collection conformance and multi-line literals in Swift 4 were a welcome first step. But, trying to write a script, solve a classic whiteboard interview question, or participate in a coding competition can quickly become an exercise in frustration.

String ergonomics is a long term, multi-release endeavor. It has to continually compete for focus against near-term critical needs such as ABI stability and performance. The below provides some potential focus areas in the Swift 5 timeframe, though we probably won’t be able to focus on them all.

API Gaps

String’s APIs have a lot of holes in them, and we’re hoping the effort mentioned later in this email can help expose them. Filling these gaps is always in-scope for any release.

Character Properties

One immediately obvious gap is Character, which lacks basic queries. For example, it should be possible to write something like myStr.filter { !$0.isWhitespace } to get a string with all whitespace removed.

While this can quickly get bogged down in the details (Q: “Does the Mongolian vowel separator count as whitespace?” A: Depends on the version of Unicode!), there should be obvious, well-understood, generally useful queries that Character can answer. This area has been well-trodden by CharacterSet and its static properties can provide guidance on useful queries. Some potential queries would include isWhitespace, isNewline, isPunctuation, isAlphaNumeric, etc. These should probably also be present on UnicodeScalar.

These might be useful on other types such as Substring, where they would apply to the whole slice. E.g., it should be possible to write myStr.split(separator: ";").filter { $0.isAlphaNumeric } rather than myStr.split(separator: ";").filter { !$0.contains { !$0.isAlphaNumeric } }

Note: In no way would these properties obviate the need for CharacterSet, as CharacterSet API is independently useful and the right model for many whole-string operations.

Interpolation: Building Strings Up

String interpolation, especially when used in conjunction with other language features such as multi-line string literals, present a compelling, visual way to construct strings. For example, the following theoretical printout of a benchmark result:

print("""
  Result: \(benchmark.result)
  Execution time:
    user(ms): \(benchmark.userTime.milliseconds)
    system(ms): \(benchmark.systemTime.milliseconds)
    wall(ms): \(benchmark.wallTime.milliseconds)
  """)

String interpolation has many issues and limitations:

  • Current interface is inefficient. Individual segments are turned into Strings before interpolation. While small string optimizations alleviate some of this, larger segments incur unneeded heap traffic and tracking.
  • While it is fairly customizable, the interface is clunky and deprecated. Brent Royal-Gordon has clearly articulated the problems and provided potential solutions in Fix ExpressibleByStringInterpolation. This approach does need to be adjusted to address the performance issue above.
  • Interpolated expressions are supplied inside the literal, meaning they cannot be passed around like format strings without extra boilerplate (e.g. a wrapping function).

Additionally, some concept of string formatters would greatly aid the ergonomics of constructing strings, whether inside interpolations or not.

Regex: Tearing Strings Apart

Perhaps the most pressing area in need of ergonomic aid is tearing strings apart to extract information embedded within them. This can take many forms, and we would like to focus the discussion on language-level support for string matching literals, aka “regexes”. (Obligatory note that regexes here may or may not have a loose relationship to regular expressions in formal language theory, depending on details.)

This wouldn’t be worth doing if all it afforded was replacing “foo” with /foo/ inside calls to NSRegularExpression. Regexes become compelling when viewed in conjunction with other language constructs such as switches (i.e. a new kind of Pattern), an ability to bind to variables, interpolate other matchers, a rich type system, etc.

Before delving into details on feature set, semantics, and implementation, here’s a quick tour of how they could fit into Swift.

One Possible Approach

This is one, brief and hand-wavy, approach to adding regexes to Swift. This is not a proposal, and all syntax is a total strawman meant for illustration purposes only. The regex literal syntax below is an adaptation of Chris Lattner’s strawman syntax. In this syntax, whitespace is insignificant.

Compile Regex Literals into Regex<T>
let usPhoneNumber = /
  (let area: Int? <- \d{3}?) -
  (let routing: Int <- \d{3}) -
  (let local: Int <- \d{4}) /
print(type(of: usPhoneNumber)) // => Regex<(area: Int?, routing: Int, local: Int)>
...
let (_, routing, local) = try usPhoneNumber.match(myStr)!
print(type(of: routing)) // => Int

If no type is specified, the default is Substring covering the match.

Types in Captures Conform to Some Protocol
extension Int: RegexSubmatchableiblewobble {
  init?(regexSubmatch: Substring)
}
Captures and Interpolation Propagated Through Type System
let countryCode = / \+ (let country: Int <- \d+) /
print(type(of: countryCode)) // => Regex<(country: Int)>

let betterUSPhoneNumber = /
  (let country: Int? <- countryCode?)
  (let (area, routing, local) <- usPhoneNumber) /
print(type(of: betterUSPhoneNumber)) 
  // => Regex<(country: Int?, area: Int?, routing: Int, local: Int)>

let nestedCapture = /
  (let country: Int? <- countryCode?)
  (let number <- usPhoneNumber) /
print(type(of: nestedCapture)) 
  // => Regex<(country: Int?, number: (area: Int?, routing: Int, local: Int))> 

This alone is not sufficient to use regexes in pattern matching, as ~= returns a bool rather than a tuple describing a match. For that, we need to generalize pattern matching in Swift.

Pattern Matching Through Conformance to Pattern
protocol Pattern {
  associatedtype In
  associatedtype Out
  func match(_: In) -> Out?
}

~= can still be checked if needed for compatibility. Alternatively, a default implementation of match could be provided in terms of ~=, where Out is Void, i.e. only match success or failure. We may want a default implementation of match generic over any collection that In is a SubSequence of, to address the Slice vs Collection or Substring vs String divide.

Syntax for Destructing Matching

Introduce some kind of syntax for a new value-binding-pattern that performs destructing. E.g.

let myValue: T = ...
let pattern: MyTPattern<(U,U)> = ... // matches a T to (U, U)
let otherPattern: OtherTPattern<V> = ... // matches a T to V
switch myValue {
  case (let a: Int, let b: Int) <- pattern: ...
  case let pair <- pattern: ... // pair has type (U,U)
  case let d: Double <- otherPattern: ...
  case let num <- otherPattern: // num has type V
}

which also (hand-wavy timey-wimey) invokes some failable init on Int taking a U, Double taking a V, etc., guided by some protocol.

Regexes are Just Patterns
struct Regex<T> {
  var program: CompiledRegex<T>
}
extension Regex: Pattern {
  typealias In = Substring
  typealias Out = T
  func match(_ s: In) -> Out? {
    // magic goes here
  }
}

Regex literals may have special syntax, but otherwise regexes are not a special case in the language and libraries can take advantage of generalized destructing matching. Similarly, character classes (which may be user defined, e.g. with a closure), could be Patterns over Characters.

Regexes Provide Variants
extension Regex {
  var caseInsensitive: Regex<T> { get }
  var allMatches: Regex<[T]> { get }
  var lineBased: Regex<[T]> { get }
  // ... matches with ranges, maybe even a Match object, etc.
  // ... lazy allMatches variant, etc.
}
Final Contrived Example (for Illustrative Purposes)
switch myStringlyTypedNumber {
case let (_, area?, routing, local) <- betterUSPhoneNumber: // ...
case let hexNumStr <- 
	/ 0 x (let num <- [0-9 A-F]+) /.caseInsensitive: // ...
case let (mantissa: Double, exponent: Int) <- scientificNumber: // ...
}

Feature Set

The examples above merely illustrate how regex literals could appear, but we need to carefully design the feature set in accordance with Swift’s goals and principles as a language.

One goal of Swift is to eventually be better at string processing than Perl, so let’s take a look at Perl. Perl’s history is full of interesting insights. Perl organically evolved into the de-facto standard for regex-based string processing culminating in Perl 5. More importantly, they reflected on the “mess” and cleaned it up in Perl 6. Perl 6 also has a PEG-like grammar system and we think similar approaches may be a part of Swift’s long-term future. For now, we’re focusing on regexes, although future considerations do impact the desired feature set.

Swift contrasts with Perl in that Swift places greater importance on clarity and simplicity over raw expressive power and extreme configurability. However, these are some features Swift should outright steal from Perl 6.

  • Free form syntax. Whitespace is insignificant, comments can occur inside literals, etc., all of which aids readability
  • Named Captures. If something’s worth capturing, it’s worth giving it a name.
  • Multi-line matching. A new line is just another character, not a match terminator.
  • Regex interpolation. Refactor complex regexes into simpler ones (with names!) and interpolate them. This is perhaps the most Swifty aspect of Perl 6 regexes!
  • Miscellaneous features common to modern regex systems, such as:
    • Quantifier modifiers, e.g. frugal (“lazy”), ratcheting (“possessive”)
    • Non-capturing grouping, ratcheting (“atomic”) grouping
    • Separator-quantification (“modified quantifier”)
    • String literals
    • Rich set of character classes (more on that next)
Character Classes

We will want a broad array of builtin pre-defined character classes for use in regexes. Additionally, users should be able to construct and define their own, whether from CharacterSets (i.e. enumeration) or through an arbitrary closure of type (Character) -> Bool. There should be syntax to use user-defined character classes in a regex literal.

Character classes should provide many set-algebraic operations such as union, intersection, inversion, mutual-exclusion (“xor”), and set difference. (Note that for our purposes, the standard library’s SetAlgebra protocol is not appropriate for character classes, as SetAlgebra requires conformances to be able to check for subset relationships which is intractable, if not undecidable, for closures over graphemes.)

Arguable Features

Some literal syntactic features common in other regex systems may be unnecessary or a bad fit for Swift.

  1. Substitution literals (e.g. sed-like s/regex/replacement/g)
    • If String had better substitution APIs, such as one that took a Regex<T> and a closure from (T) -> String alongside options such as .all or .firstOnly, that might be sufficient and/or clearer anyways.
  2. Adverbs (e.g. the i meaning case-insensitive outside the literal’s delimiters)
    • These could be Regex variants accessible via computed properties, which may be clearer
  3. Anonymous (numbered) captures
    • In general, if something’s worth capturing it’s worth giving it a name.
    • A construct like (let _ = \d+) could produce an unlabeled tuple element.
    • Tuples (the representation for literal captures) support 0-based access already, e.g. myCaptures.3.
  4. Enumerated character class ranges for non-ASCII graphemes (the - in [A-Z])
    • Convenient for ASCII, but meaningless for graphemes.
    • Good built-in classes and convenient ways for users to combine and enumerate their own may even obviate the need for ranges for ASCII.
Forbidden Features (with Alternatives)

There are some regex features that we argue should be disallowed, or at least carefully constrained. Regexes are powerful, specifying simple textual analysis in a concise syntax, but are easily misused in ways that add exponentially-compounding complexity in non-obvious ways. This misuse contrasts with Swift’s values of balancing expressivity with clarity, where complex behavior should be explicitly expressed in code.

However, we think there’s so much flexibility in what can accomplished without the most general form of these features that it might not be an issue in practice.

1. Arbitrarily-Complex Look-Around Assertions

Assertions are the ability for a regex to “peek” ahead or behind at surrounding characters without consuming them as part of a match. They can be positive or negative. Arbitrarily-complex assertions include the ability to do captures as part of an assertion, unknown-length assertions satisfied by subpatterns, etc., which can have non-intuitively complex interactions with other parts of the regex.

Alternative: The following constrained assertions could be permissible:

  1. Anchors (zero-width assertions), which test for positions within a string, e.g. ^ and $.

  2. Fixed-length character-class or closure-based assertions, which help avoid common pitfalls in negation as well as aid readability. This would permit fixed-length arbitrary computation explicitly expressed in code. These might also encompass boundaries.

  3. Invoking a closure on the string prefix/suffix prior/after the assertion point. This would permit variable-length arbitrary computation, but explicitly expressed in code.

2. Arbitrary Uses of Backreferences

Backreferences allow testing a later portion of a string against a previously-matched portion. Unfortunately, in their most general form, they require exploration of the entire search space and make matching NP-complete, that is take exponential time. Very simple uses of backreferences just want to check for an unambiguous capture after the fact, for which a where clause suffices. Very complex uses are better served with an exponential loop explicitly expressed in code.

Alternative: There is a range of convenient uses between these two extremes, which we may be able to tractably address. The feasibility of these is still under investigation, but some potential constrained backreferences may include:

  1. A “ratcheting” backreference, where upon matching (i.e. the current section of the string satisfies the backreference), the referred-to capture is “frozen” in place, so that alternative assignments need not be explored.
  2. A delimiter-checking construct. While eventually we want to encourage the user to write a real grammar/parser (assuming some future grammar/parser description system in Swift), simple delimiter matching can greatly aid readability. This might even be general and powerful enough to define tasks such as pairing XML tags and ignoring escaped delimiters or delimiters inside literals. Ideally, this would be done in a direction conducive to a future parsing solution.

Semantics

Matching semantics should roughly corresponding to UTS #18 Level 2 of Unicode support. That is, “any character” would be any Swift Character (a grapheme), matching obeys canonical equivalence, etc. This is consistent with String’s overall philosophy of universality and harm reduction. This does require that the implementation use Unicode rules and tables provided at run time, e.g. for grapheme breaking.

Regex interpolation should effectively “inline” the interpolated regex, i.e. formal concatenation. We will probably also want different combination semantics for combining patterns in general (including regexes), possibly in conjunction with a future parsing solution. For example, a form of alternation that is short-circuiting and some form of concatenation that is “ratcheting”, where something like (myRegex1 || myRegex2) ++ myRegex3 tries myRegex1 first, and if successful myRegex2 is never tried regardless of whether myRegex3 succeeds or not. This is similar to the semantics of PEGs or parser-combinators and much more closely reflects programmer intuition for parsing.

We’ll need to nail down a lot of important details:

  • How are alternations matched? LTM?
  • What are the built in character classes and what do they cover?
    • What Unicode properties to expose?
    • Should there be API symmetry with Character queries mentioned above?
  • What are the built in anchors, fixed-length assertions, and can they be customized?
  • What is the type of a quantified capture? An array of the capture’s declared type?
  • What guarantees about runtime and space complexity do we provide, if any?
  • While we may have variants for whole-string, partial-string, line-by-line, etc., what should the defaults be?

Implementation

With static knowledge, many regexes can be compiled into efficient, minimal state machines (albeit with Unicode rules and tables as input at run time). Virtual machine approaches can make it easier to specify and guarantee behavior while also being much more extensible. They also may alleviate some ABI-stability concerns, as it’s easier to keep a bytecode stable while growing new features.

Delivering ergonomic regexes in Swift 5 should not be blocked on having an efficient implementation available immediately. However, long term, it is important to have a predictable and consistent performance model to prevent unanticipated exponential behavior. Thus, implementation concerns are relevant in scoping the initial feature set (e.g. regarding backreferences).

Some regexes are amenable to task-parallel strategies (i.e. threads), data-parallel strategies (i.e. vector or GPU), or both. Swift will probably grow new strategies and combinations of strategies over time. Implementations can also take advantage of the aforementioned performance flags (e.g. isCanonical) to greatly speed up matches.

The interface to a regex engine will be part of Swift’s ABI, including whatever intermediate representation regexes are compiled into. This could be a simple regex AST, a virtual machine bytecode, etc., but it needs to be extensible to accommodate any potential future additions and implementation strategies.

Recap: Potential Additions for Swift 5

  • Some form of unmanaged or unsafe Strings, and corresponding APIs
  • Exposing performance flags, and some way to request a scan to populate them
  • API gaps
  • Character and UnicodeScalar properties, such as isNewline
  • Generalizing, and optimizing, String interpolation
  • Regex literals, Regex type, and generalized pattern match destructuring
  • Substitution APIs, in conjunction with Regexes.

Community

With Swift’s mailing lists moving to Discourse, this allows the opportunity for better community engagement and cross-referencing posts. We plan on starting an area under the “Using Swift” category focusing on idiomatic and/or performant solutions to string programming needs. These might be anything from simple programming problems to make-my-parser-faster challenges.

While this provides a community benefit by showing how best to use String, our ulterior motive is to mine posts looking for benchmarks, API gaps, and representative code that new features could improve. We’ll also, of course, want to bikeshed the name! My vote is for “String Dojo”, but there’s probably a reason I don’t normally name things.

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