Skip to content

Commit 646946d

Browse files
milsemannatecook1000finagolfinoleamartini51
authored
[swift/main] Merge performance improvements into swift/main (#705)
* Atomically load the lowered program (#610) Since we're atomically initializing the compiled program in `Regex.Program`, we need to pair that with an atomic load. Resolves #609. * Add tests for line start/end word boundary diffs (#616) The `default` and `simple` word boundaries have different behaviors at the start and end of strings/lines. These tests validate that we have the correct behavior implemented. Related to issue #613. * Add tweaks for Android * Fix documentation typo (#615) * Fix abstract for Regex.dotMatchesNewlines(_:). (#614) The old version looks like it was accidentally duplicated from anchorsMatchLineEndings(_:) just below it. * Remove `RegexConsumer` and fix its dependencies (#617) * Remove `RegexConsumer` and fix its dependencies This eliminates the RegexConsumer type and rewrites its users to call through to other, existing functionality on Regex or in the Algorithms implementations. RegexConsumer doesn't take account of the dual subranges required for matching, so it can produce results that are inconsistent with matches(of:) and ranges(of:), which were rewritten earlier. rdar://102841216 * Remove remaining from-end algorithm methods This removes methods that are left over from when we were considering from-end algorithms. These aren't tested and may not have the correct semantics, so it's safer to remove them entirely. * Improve StringProcessing and RegexBuilder documentation (#611) This includes documentation improvements for core types/methods, RegexBuilder types along with their generated variadic initializers, and adds some curation. It also includes tests of the documentation code samples. * Set availability for inverted character class test (#621) This feature depends on running with a Swift 5.7 stdlib, and fails when that isn't available. * Add type annotations in RegexBuilder tests These changes work around a change to the way result builders are compiled that removes the ability for result builder closure outputs to affect the overload resolution elsewhere in an expression. Workarounds for rdar://104881395 and rdar://104645543 * Workaround for fileprivate array issue A recent compiler change results in fileprivate arrays sometimes not keeping their buffers around long enough. This change avoids that issue by removing the fileprivate annotations from the affected type. * Fix an issue where named character classes weren't getting converted in the result builder. <rdar://104480703> * Stop at end of search string in TwoWaySearcher (#631) When searching for a substring that doesn't exist, it was possible for TwoWaySearcher to advance beyond the end of the search string, causing a crash. This change adds a `limitedBy:` parameter to that index movement, avoiding the invalid movement. Fixes rdar://105154010 * Correct misspelling in DSL renderer (#627) vertial -> vertical rdar://104602317 * Fix output type mismatch with RegexBuilder (#626) Some regex literals (and presumably other `Regex` instances) lose their output type information when used in a RegexBuilder closure due to the way the concatenating builder calls are overloaded. In particular, any output type with labeled tuples or where the sum of tuple components in the accumulated and new output types is greater than 10 will be ignored. Regex internals don't make this distinction, however, so there ends up being a mismatch between what a `Regex.Match` instance tries to produce and the output type of the outermost regex. For example, this code results in a crash, because `regex` is a `Regex<Substring>` but the match tries to produce a `(Substring, number: Substring)`: let regex = Regex { ZeroOrMore(.whitespace) /:(?<number>\d+):/ ZeroOrMore(.whitespace) } let match = try regex.wholeMatch(in: " :21: ") print(match!.output) To fix this, we add a new `ignoreCapturesInTypedOutput` DSLTree node to mark situations where the output type is discarded. This status is propagated through the capture list into the match's storage, which lets us produce the correct output type. Note that we can't just drop the capture groups when building the compiled program because (1) different parts of the regex might reference the capture group and (2) all capture groups are available if a developer converts the output to `AnyRegexOutput`. let anyOutput = AnyRegexOutput(match) // anyOutput[1] == "21" // anyOutput["number"] == Optional("21") Fixes #625. rdar://104823356 Note: Linux seems to crash on different tests when the two customTest overloads have `internal` visibility or are called. Switching one of the functions to be generic over a RegexComponent works around the issue. * Revert "Merge pull request #628 from apple/result_builder_changes_workaround" This reverts commit 7e059b7, reversing changes made to 3ca8b13. * Use `some` syntax in variadics This supports a type checker fix after the change in how result builder closure parameters are type-checked. * Type checker workaround: adjust test * Further refactor to work around type checker regression * Align availability macro with OS versions (#641) * Speed up general character class matching (#642) Short-circuit Character.isASCII checks inside built in character class matching. Also, make benchmark try a few more times before giving up. * Test for \s matching CRLF when scalar matching (#648) * General ascii fast paths for character classes (#644) General ASCII fast-paths for builtin character classes * Remove the unsupported `anyScalar` case (#650) We decided not to support the `anyScalar` character class, which would match a single Unicode scalar regardless of matching mode. However, its representation was still included in the various character class types in the regex engine, leading to unreachable code and unclear requirements when changing or adding new code. This change removes that representation where possible. The `DSLTree.Atom.CharacterClass` enum is left unchanged, since it is marked `@_spi(RegexBuilder) public`. Any use of that enum case is handled with a `fatalError("Unsupported")`, and it isn't produced on any code path. * Fix range-based quantification fast path (#653) The fast path for quantification incorrectly discards the last save position when the quantification used up all possible trips, which is only possible with range-based quantifications (e.g. `{0,3}`). This bug shows up when a range-based quantifier matches the maximum - 1 repetitions of the preceding pattern. For example, the regex `/a{0,2}a/` should succeed as a full match any of the strings "aa", "aaa", or "aaaa". However, the pattern fails to match "aaa", since the save point allowing a single "a" to match the first `a{0,2}` part of the regex is discarded. This change only discards the last save position when advancing the quantifier fails due to a failure to match, not maxing out the number of trips. * Add in ASCII fast-path for anyNonNewline (#654) * Avoid long expression type checks (#657) These changes remove several seconds of type-checking time from the RegexBuilder test cases, bringing all expressions under 150ms (on the tested computer). * Processor cleanup (#655) Clean up and refactor the processor * Simplify instruction fetching * Refactor metrics out, and void their storage in release builds *Put operations onto String * Fix `firstRange(of:)` search (#656) Calls to `ranges(of:)` and `firstRange(of:)` with a string parameter actually use two different string searching algorithms. `ranges(of:)` uses the "z-searcher" algorithm, while `firstRange(of:)` uses a two-way search. Since it's better to align on a single path for these searches, the z-searcher has lower requirements, and the two-way search implementation has a correctness bug, this change removes the two-way search algorithm and uses z-search for `firstRange(of:)`. The correctness bug in `firstRange(of:)` appears only when searching for the second (or later) occurrence of a substring, which you have to be fairly deliberate about. In the example below, the substring at offsets `7..<12` is missed: let text = "ADACBADADACBADACB" // ===== -----===== let pattern = "ADACB" let firstRange = text.firstRange(of: pattern)! // firstRange ~= 0..<5 let secondRange = text[firstRange.upperBound...].firstRange(of: pattern)! // secondRange ~= 12..<17 This change also removes some unrelated, unused code in Split.swift, in addition to removing an (unused) usage of `TwoWaySearcher`. rdar://92794248 * Bug fix and hot path for quantified `.` (#658) Bug fix in newline hot path, and apply hot path to quantified dot * Run scalar-semantic benchmark variants (#659) Run scalar semantic benchmarks * Refactor operations to be on String (#664) Finish refactoring logic onto String * Provide unique generic method parameter names (#669) This is getting warned on in the 5.9 compiler, will be an error starting in Swift 6. * Enable quantification optimizations for scalar semantics (#671) * Quantified scalar semantic matching * Fix doc comment for trimPrefix and trimmingPrefix funcs (#673) * Update availability for the 5.8 release (#680) * Optimize search for start-anchored regexes (#682) When a regex is anchored to the start of a subject, there's no need to search throughout a string for the pattern when searching for the first match: a prefix match is sufficient. This adds a regex compilation-time check about whether a match can only be found at the start of a subject, and then uses that to choose whether to defer to `prefixMatch` from within `firstMatch`. * Fix misuse of `XCTSkip()` (#685) * Handle boundaries when matching in substrings (#675) * Handle boundaries when matching in substrings Some of our existing matching routines use the start/endIndex of the input, which is basically never the right thing to do. This change revises those checks to use the search bounds, by either moving the boundary check out of the matching method, or if the boundary is a part of what needs to be matched (e.g. word boundaries have different behavior at the start/end than in the middle of a string) the search bounds are passed into the matching method. Testing is currently handled by piggy-backing on the existing match tests; we should add more tests to handle substring- specific edge cases. * Handle sub-character substring boundaries This change passes the end boundary down into matching methods, and uses it to find the actual character that is part of the input substring, even if the substring's end boundary is in the middle of a grapheme cluster. Substrings cannot have sub-Unicode scalar boundaries as of Swift 5.7; we can remove a check for this when matching an individual scalar. * Overhaul quantification fast-path (#689) Overhaul quantification save points and fast path logic, for significant wins in simplicity and performance. * adopt the stdlib’s pattern for atomic lazy references - avoids reliance on a pointer conversion * pass a pointer instead of inout conversion - this function is imported in a way that causes the compiler to not detect it as a C function * Update Sources/_StringProcessing/Regex/Core.swift comment spelling fix * Adds SPI for a NSRE compatibility mode option (#698) NSRegularExpression matches at the Unicode scalar level, but also matches `\r\n` sequences with a single `.` when single-line mode is enabled. This adds a `_nsreCompatibility` property that enables both of those behaviors, and implements support for the special case handling of `.`. * Add ASCII fast-path ASCII character class matching (#690) Uses quickASCIICharacter to speed up ASCII character class matching. 2x speedup for EmailLookahead_All and many, many others. 10% regression in AnchoredNotFound_First and related. --------- Co-authored-by: Nate Cook <[email protected]> Co-authored-by: Butta <[email protected]> Co-authored-by: Ole Begemann <[email protected]> Co-authored-by: Alex Martini <[email protected]> Co-authored-by: Alejandro Alonso <[email protected]> Co-authored-by: David Ewing <[email protected]> Co-authored-by: Dave Ewing <[email protected]> Co-authored-by: Valeriy Van <[email protected]> Co-authored-by: Jonathan Grynspan <[email protected]> Co-authored-by: Guillaume Lessard <[email protected]> Co-authored-by: Guillaume Lessard <[email protected]>
1 parent 42678d5 commit 646946d

File tree

15 files changed

+278
-235
lines changed

15 files changed

+278
-235
lines changed

Package.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ let availabilityDefinition = PackageDescription.SwiftSetting.unsafeFlags([
1111
"-Xfrontend",
1212
"-define-availability",
1313
"-Xfrontend",
14-
"SwiftStdlib 5.8:macOS 9999, iOS 9999, watchOS 9999, tvOS 9999",
14+
"SwiftStdlib 5.8:macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4",
15+
"-Xfrontend",
16+
"-define-availability",
17+
"-Xfrontend",
18+
"SwiftStdlib 5.9:macOS 9999, iOS 9999, watchOS 9999, tvOS 9999",
1519
])
1620

1721
/// Swift settings for building a private stdlib-like module that is to be used
@@ -128,7 +132,8 @@ let package = Package(
128132
.product(name: "ArgumentParser", package: "swift-argument-parser"),
129133
"_RegexParser",
130134
"_StringProcessing"
131-
]),
135+
],
136+
swiftSettings: [availabilityDefinition]),
132137
.executableTarget(
133138
name: "RegexBenchmark",
134139
dependencies: [

Sources/RegexTester/RegexTester.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import _RegexParser
1414
import _StringProcessing
1515

1616
@main
17-
@available(macOS 9999, *)
17+
@available(SwiftStdlib 5.8, *)
1818
struct RegexTester: ParsableCommand {
1919
typealias MatchFunctionType = (String) throws -> Regex<AnyRegexOutput>.Match?
2020

Sources/_RegexParser/Regex/AST/MatchingOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ extension AST {
4444

4545
// Swift-only default possessive quantifier
4646
case possessiveByDefault // t.b.d.
47-
47+
4848
// NSRegularExpression compatibility special-case
4949
case nsreCompatibleDot // no AST representation
5050
}

Sources/_StringProcessing/Algorithms/Algorithms/Trim.swift

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,11 @@ extension RangeReplaceableCollection {
8181
// MARK: Fixed pattern algorithms
8282

8383
extension Collection where Element: Equatable {
84-
/// Returns a new collection of the same type by removing initial elements
85-
/// that satisfy the given predicate from the start.
86-
/// - Parameter predicate: A closure that takes an element of the sequence
87-
/// as its argument and returns a Boolean value indicating whether the
88-
/// element should be removed from the collection.
84+
/// Returns a new collection of the same type by removing `prefix` from the start
85+
/// of the collection.
86+
/// - Parameter prefix: The collection to remove from this collection.
8987
/// - Returns: A collection containing the elements of the collection that are
90-
/// not removed by `predicate`.
88+
/// not removed by `prefix`.
9189
@available(SwiftStdlib 5.7, *)
9290
public func trimmingPrefix<Prefix: Sequence>(
9391
_ prefix: Prefix
@@ -97,11 +95,8 @@ extension Collection where Element: Equatable {
9795
}
9896

9997
extension Collection where SubSequence == Self, Element: Equatable {
100-
/// Removes the initial elements that satisfy the given predicate from the
101-
/// start of the sequence.
102-
/// - Parameter predicate: A closure that takes an element of the sequence
103-
/// as its argument and returns a Boolean value indicating whether the
104-
/// element should be removed from the collection.
98+
/// Removes `prefix` from the start of the collection.
99+
/// - Parameter prefix: The collection to remove from this collection.
105100
@available(SwiftStdlib 5.7, *)
106101
public mutating func trimPrefix<Prefix: Sequence>(
107102
_ prefix: Prefix
@@ -111,11 +106,8 @@ extension Collection where SubSequence == Self, Element: Equatable {
111106
}
112107

113108
extension RangeReplaceableCollection where Element: Equatable {
114-
/// Removes the initial elements that satisfy the given predicate from the
115-
/// start of the sequence.
116-
/// - Parameter predicate: A closure that takes an element of the sequence
117-
/// as its argument and returns a Boolean value indicating whether the
118-
/// element should be removed from the collection.
109+
/// Removes `prefix` from the start of the collection.
110+
/// - Parameter prefix: The collection to remove from this collection.
119111
@available(SwiftStdlib 5.7, *)
120112
public mutating func trimPrefix<Prefix: Sequence>(
121113
_ prefix: Prefix
@@ -127,11 +119,11 @@ extension RangeReplaceableCollection where Element: Equatable {
127119
// MARK: Regex algorithms
128120

129121
extension BidirectionalCollection where SubSequence == Substring {
130-
/// Returns a new collection of the same type by removing `prefix` from the
131-
/// start.
132-
/// - Parameter prefix: The collection to remove from this collection.
122+
/// Returns a new collection of the same type by removing the initial elements
123+
/// that matches the given regex.
124+
/// - Parameter regex: The regex to remove from this collection.
133125
/// - Returns: A collection containing the elements that does not match
134-
/// `prefix` from the start.
126+
/// `regex` from the start.
135127
@_disfavoredOverload
136128
@available(SwiftStdlib 5.7, *)
137129
public func trimmingPrefix(_ regex: some RegexComponent) -> SubSequence {

Sources/_StringProcessing/ByteCodeGen.swift

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -465,16 +465,16 @@ fileprivate extension Compiler.ByteCodeGen {
465465
assert(high != 0)
466466
assert((0...(high ?? Int.max)).contains(low))
467467

468-
let extraTrips: Int?
468+
let maxExtraTrips: Int?
469469
if let h = high {
470-
extraTrips = h - low
470+
maxExtraTrips = h - low
471471
} else {
472-
extraTrips = nil
472+
maxExtraTrips = nil
473473
}
474474
let minTrips = low
475-
assert((extraTrips ?? 1) >= 0)
475+
assert((maxExtraTrips ?? 1) >= 0)
476476

477-
if tryEmitFastQuant(child, updatedKind, minTrips, extraTrips) {
477+
if tryEmitFastQuant(child, updatedKind, minTrips, maxExtraTrips) {
478478
return
479479
}
480480

@@ -492,19 +492,19 @@ fileprivate extension Compiler.ByteCodeGen {
492492
decrement %minTrips and fallthrough
493493
494494
loop-body:
495-
<if can't guarantee forward progress && extraTrips = nil>:
495+
<if can't guarantee forward progress && maxExtraTrips = nil>:
496496
mov currentPosition %pos
497497
evaluate the subexpression
498-
<if can't guarantee forward progress && extraTrips = nil>:
498+
<if can't guarantee forward progress && maxExtraTrips = nil>:
499499
if %pos is currentPosition:
500500
goto exit
501501
goto min-trip-count control block
502502
503503
exit-policy control block:
504-
if %extraTrips is zero:
504+
if %maxExtraTrips is zero:
505505
goto exit
506506
else:
507-
decrement %extraTrips and fallthrough
507+
decrement %maxExtraTrips and fallthrough
508508
509509
<if eager>:
510510
save exit and goto loop-body
@@ -531,12 +531,12 @@ fileprivate extension Compiler.ByteCodeGen {
531531
/* fallthrough */
532532
"""
533533

534-
// Specialization based on `extraTrips` for 0 or unbounded
534+
// Specialization based on `maxExtraTrips` for 0 or unbounded
535535
_ = """
536536
exit-policy control block:
537-
<if extraTrips == 0>:
537+
<if maxExtraTrips == 0>:
538538
goto exit
539-
<if extraTrips == .unbounded>:
539+
<if maxExtraTrips == .unbounded>:
540540
/* fallthrough */
541541
"""
542542

@@ -569,12 +569,12 @@ fileprivate extension Compiler.ByteCodeGen {
569569
minTripsReg = nil
570570
}
571571

572-
let extraTripsReg: IntRegister?
573-
if (extraTrips ?? 0) > 0 {
574-
extraTripsReg = builder.makeIntRegister(
575-
initialValue: extraTrips!)
572+
let maxExtraTripsReg: IntRegister?
573+
if (maxExtraTrips ?? 0) > 0 {
574+
maxExtraTripsReg = builder.makeIntRegister(
575+
initialValue: maxExtraTrips!)
576576
} else {
577-
extraTripsReg = nil
577+
maxExtraTripsReg = nil
578578
}
579579

580580
// Set up a dummy save point for possessive to update
@@ -606,7 +606,7 @@ fileprivate extension Compiler.ByteCodeGen {
606606
let startPosition: PositionRegister?
607607
let emitPositionChecking =
608608
(!optimizationsEnabled || !child.guaranteesForwardProgress) &&
609-
extraTrips == nil
609+
maxExtraTrips == nil
610610

611611
if emitPositionChecking {
612612
startPosition = builder.makePositionRegister()
@@ -616,7 +616,7 @@ fileprivate extension Compiler.ByteCodeGen {
616616
}
617617
try emitNode(child)
618618
if emitPositionChecking {
619-
// in all quantifier cases, no matter what minTrips or extraTrips is,
619+
// in all quantifier cases, no matter what minTrips or maxExtraTrips is,
620620
// if we have a successful non-advancing match, branch to exit because it
621621
// can match an arbitrary number of times
622622
builder.buildCondBranch(to: exit, ifSamePositionAs: startPosition!)
@@ -629,20 +629,20 @@ fileprivate extension Compiler.ByteCodeGen {
629629
}
630630

631631
// exit-policy:
632-
// condBranch(to: exit, ifZeroElseDecrement: %extraTrips)
632+
// condBranch(to: exit, ifZeroElseDecrement: %maxExtraTrips)
633633
// <eager: split(to: loop, saving: exit)>
634634
// <possesive:
635635
// clearSavePoint
636636
// split(to: loop, saving: exit)>
637637
// <reluctant: save(restoringAt: loop)
638638
builder.label(exitPolicy)
639-
switch extraTrips {
639+
switch maxExtraTrips {
640640
case nil: break
641641
case 0: builder.buildBranch(to: exit)
642642
default:
643-
assert(extraTripsReg != nil, "logic inconsistency")
643+
assert(maxExtraTripsReg != nil, "logic inconsistency")
644644
builder.buildCondBranch(
645-
to: exit, ifZeroElseDecrement: extraTripsReg!)
645+
to: exit, ifZeroElseDecrement: maxExtraTripsReg!)
646646
}
647647

648648
switch updatedKind {
@@ -672,12 +672,12 @@ fileprivate extension Compiler.ByteCodeGen {
672672
_ child: DSLTree.Node,
673673
_ kind: AST.Quantification.Kind,
674674
_ minTrips: Int,
675-
_ extraTrips: Int?
675+
_ maxExtraTrips: Int?
676676
) -> Bool {
677677
let isScalarSemantics = options.semanticLevel == .unicodeScalar
678678
guard optimizationsEnabled
679679
&& minTrips <= QuantifyPayload.maxStorableTrips
680-
&& extraTrips ?? 0 <= QuantifyPayload.maxStorableTrips
680+
&& maxExtraTrips ?? 0 <= QuantifyPayload.maxStorableTrips
681681
&& kind != .reluctant else {
682682
return false
683683
}
@@ -687,7 +687,7 @@ fileprivate extension Compiler.ByteCodeGen {
687687
guard let bitset = ccc.asAsciiBitset(options) else {
688688
return false
689689
}
690-
builder.buildQuantify(bitset: bitset, kind, minTrips, extraTrips, isScalarSemantics: isScalarSemantics)
690+
builder.buildQuantify(bitset: bitset, kind, minTrips, maxExtraTrips, isScalarSemantics: isScalarSemantics)
691691

692692
case .atom(let atom):
693693
switch atom {
@@ -696,17 +696,17 @@ fileprivate extension Compiler.ByteCodeGen {
696696
guard let val = c._singleScalarAsciiValue else {
697697
return false
698698
}
699-
builder.buildQuantify(asciiChar: val, kind, minTrips, extraTrips, isScalarSemantics: isScalarSemantics)
699+
builder.buildQuantify(asciiChar: val, kind, minTrips, maxExtraTrips, isScalarSemantics: isScalarSemantics)
700700

701701
case .any:
702702
builder.buildQuantifyAny(
703-
matchesNewlines: true, kind, minTrips, extraTrips, isScalarSemantics: isScalarSemantics)
703+
matchesNewlines: true, kind, minTrips, maxExtraTrips, isScalarSemantics: isScalarSemantics)
704704
case .anyNonNewline:
705705
builder.buildQuantifyAny(
706-
matchesNewlines: false, kind, minTrips, extraTrips, isScalarSemantics: isScalarSemantics)
706+
matchesNewlines: false, kind, minTrips, maxExtraTrips, isScalarSemantics: isScalarSemantics)
707707
case .dot:
708708
builder.buildQuantifyAny(
709-
matchesNewlines: options.dotMatchesNewline, kind, minTrips, extraTrips, isScalarSemantics: isScalarSemantics)
709+
matchesNewlines: options.dotMatchesNewline, kind, minTrips, maxExtraTrips, isScalarSemantics: isScalarSemantics)
710710

711711
case .characterClass(let cc):
712712
// Custom character class that consumes a single grapheme
@@ -715,19 +715,19 @@ fileprivate extension Compiler.ByteCodeGen {
715715
model: model,
716716
kind,
717717
minTrips,
718-
extraTrips,
718+
maxExtraTrips,
719719
isScalarSemantics: isScalarSemantics)
720720
default:
721721
return false
722722
}
723723
case .convertedRegexLiteral(let node, _):
724-
return tryEmitFastQuant(node, kind, minTrips, extraTrips)
724+
return tryEmitFastQuant(node, kind, minTrips, maxExtraTrips)
725725
case .nonCapturingGroup(let groupKind, let node):
726726
// .nonCapture nonCapturingGroups are ignored during compilation
727727
guard groupKind.ast == .nonCapture else {
728728
return false
729729
}
730-
return tryEmitFastQuant(node, kind, minTrips, extraTrips)
730+
return tryEmitFastQuant(node, kind, minTrips, maxExtraTrips)
731731
default:
732732
return false
733733
}

0 commit comments

Comments
 (0)