Skip to content

Commit fea6fe2

Browse files
authored
RegexBuilder quantifiers take an optional behavior (#293)
1 parent b959d0a commit fea6fe2

File tree

9 files changed

+355
-243
lines changed

9 files changed

+355
-243
lines changed

Sources/RegexBuilder/DSL.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,27 +120,29 @@ extension DSLTree.Node {
120120
@available(SwiftStdlib 5.7, *)
121121
static func repeating(
122122
_ range: Range<Int>,
123-
_ behavior: QuantificationBehavior,
123+
_ behavior: QuantificationBehavior?,
124124
_ node: DSLTree.Node
125125
) -> DSLTree.Node {
126126
// TODO: Throw these as errors
127127
assert(range.lowerBound >= 0, "Cannot specify a negative lower bound")
128128
assert(!range.isEmpty, "Cannot specify an empty range")
129+
130+
let kind: DSLTree.QuantificationKind = behavior.map { .explicit($0.astKind) } ?? .default
129131

130132
switch (range.lowerBound, range.upperBound) {
131133
case (0, Int.max): // 0...
132-
return .quantification(.zeroOrMore, behavior.astKind, node)
134+
return .quantification(.zeroOrMore, kind, node)
133135
case (1, Int.max): // 1...
134-
return .quantification(.oneOrMore, behavior.astKind, node)
136+
return .quantification(.oneOrMore, kind, node)
135137
case _ where range.count == 1: // ..<1 or ...0 or any range with count == 1
136138
// Note: `behavior` is ignored in this case
137-
return .quantification(.exactly(.init(faking: range.lowerBound)), .eager, node)
139+
return .quantification(.exactly(.init(faking: range.lowerBound)), .default, node)
138140
case (0, _): // 0..<n or 0...n or ..<n or ...n
139-
return .quantification(.upToN(.init(faking: range.upperBound)), behavior.astKind, node)
141+
return .quantification(.upToN(.init(faking: range.upperBound)), kind, node)
140142
case (_, Int.max): // n...
141-
return .quantification(.nOrMore(.init(faking: range.lowerBound)), behavior.astKind, node)
143+
return .quantification(.nOrMore(.init(faking: range.lowerBound)), kind, node)
142144
default: // any other range
143-
return .quantification(.range(.init(faking: range.lowerBound), .init(faking: range.upperBound)), behavior.astKind, node)
145+
return .quantification(.range(.init(faking: range.lowerBound), .init(faking: range.upperBound)), kind, node)
144146
}
145147
}
146148
}

Sources/RegexBuilder/Variadics.swift

Lines changed: 253 additions & 187 deletions
Large diffs are not rendered by default.

Sources/VariadicsGenerator/VariadicsGenerator.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,10 @@ struct VariadicsGenerator: ParsableCommand {
378378
\(params.disfavored)\
379379
public init<\(params.genericParams)>(
380380
_ component: Component,
381-
_ behavior: QuantificationBehavior = .eagerly
381+
_ behavior: QuantificationBehavior? = nil
382382
) \(params.whereClauseForInit) {
383-
self.init(node: .quantification(.\(kind.astQuantifierAmount), behavior.astKind, component.regex.root))
383+
let kind: DSLTree.QuantificationKind = behavior.map { .explicit($0.astKind) } ?? .default
384+
self.init(node: .quantification(.\(kind.astQuantifierAmount), kind, component.regex.root))
384385
}
385386
}
386387
@@ -389,10 +390,11 @@ struct VariadicsGenerator: ParsableCommand {
389390
\(defaultAvailableAttr)
390391
\(params.disfavored)\
391392
public init<\(params.genericParams)>(
392-
_ behavior: QuantificationBehavior = .eagerly,
393+
_ behavior: QuantificationBehavior? = nil,
393394
@\(concatBuilderName) _ component: () -> Component
394395
) \(params.whereClauseForInit) {
395-
self.init(node: .quantification(.\(kind.astQuantifierAmount), behavior.astKind, component().regex.root))
396+
let kind: DSLTree.QuantificationKind = behavior.map { .explicit($0.astKind) } ?? .default
397+
self.init(node: .quantification(.\(kind.astQuantifierAmount), kind, component().regex.root))
396398
}
397399
}
398400
@@ -404,7 +406,7 @@ struct VariadicsGenerator: ParsableCommand {
404406
public static func buildLimitedAvailability<\(params.genericParams)>(
405407
_ component: Component
406408
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
407-
.init(node: .quantification(.\(kind.astQuantifierAmount), .eager, component.regex.root))
409+
.init(node: .quantification(.\(kind.astQuantifierAmount), .default, component.regex.root))
408410
}
409411
}
410412
""" : "")
@@ -488,7 +490,7 @@ struct VariadicsGenerator: ParsableCommand {
488490
) \(params.whereClauseForInit) {
489491
assert(count > 0, "Must specify a positive count")
490492
// TODO: Emit a warning about `repeatMatch(count: 0)` or `repeatMatch(count: 1)`
491-
self.init(node: .quantification(.exactly(.init(faking: count)), .eager, component.regex.root))
493+
self.init(node: .quantification(.exactly(.init(faking: count)), .default, component.regex.root))
492494
}
493495
494496
\(defaultAvailableAttr)
@@ -499,15 +501,15 @@ struct VariadicsGenerator: ParsableCommand {
499501
) \(params.whereClauseForInit) {
500502
assert(count > 0, "Must specify a positive count")
501503
// TODO: Emit a warning about `repeatMatch(count: 0)` or `repeatMatch(count: 1)`
502-
self.init(node: .quantification(.exactly(.init(faking: count)), .eager, component().regex.root))
504+
self.init(node: .quantification(.exactly(.init(faking: count)), .default, component().regex.root))
503505
}
504506
505507
\(defaultAvailableAttr)
506508
\(params.disfavored)\
507509
public init<\(params.genericParams), R: RangeExpression>(
508510
_ component: Component,
509511
_ expression: R,
510-
_ behavior: QuantificationBehavior = .eagerly
512+
_ behavior: QuantificationBehavior? = nil
511513
) \(params.repeatingWhereClause) {
512514
self.init(node: .repeating(expression.relative(to: 0..<Int.max), behavior, component.regex.root))
513515
}
@@ -516,7 +518,7 @@ struct VariadicsGenerator: ParsableCommand {
516518
\(params.disfavored)\
517519
public init<\(params.genericParams), R: RangeExpression>(
518520
_ expression: R,
519-
_ behavior: QuantificationBehavior = .eagerly,
521+
_ behavior: QuantificationBehavior? = nil,
520522
@\(concatBuilderName) _ component: () -> Component
521523
) \(params.repeatingWhereClause) {
522524
self.init(node: .repeating(expression.relative(to: 0..<Int.max), behavior, component().regex.root))

Sources/_StringProcessing/ByteCodeGen.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,20 @@ extension Compiler.ByteCodeGen {
364364

365365
mutating func emitQuantification(
366366
_ amount: AST.Quantification.Amount,
367-
_ kind: AST.Quantification.Kind,
367+
_ kind: DSLTree.QuantificationKind,
368368
_ child: DSLTree.Node
369369
) throws {
370-
let kind = kind.applying(options)
370+
let updatedKind: AST.Quantification.Kind
371+
switch kind {
372+
case .explicit(let kind):
373+
updatedKind = kind
374+
case .syntax(let kind):
375+
updatedKind = kind.applying(options)
376+
case .default:
377+
updatedKind = options.isReluctantByDefault
378+
? .reluctant
379+
: .eager
380+
}
371381

372382
let (low, high) = amount.bounds
373383
switch (low, high) {
@@ -496,7 +506,7 @@ extension Compiler.ByteCodeGen {
496506
}
497507

498508
// Set up a dummy save point for possessive to update
499-
if kind == .possessive {
509+
if updatedKind == .possessive {
500510
builder.pushEmptySavePoint()
501511
}
502512

@@ -542,7 +552,7 @@ extension Compiler.ByteCodeGen {
542552
to: exit, ifZeroElseDecrement: extraTripsReg!)
543553
}
544554

545-
switch kind {
555+
switch updatedKind {
546556
case .eager:
547557
builder.buildSplit(to: loopBody, saving: exit)
548558
case .possessive:

Sources/_StringProcessing/PrintAsPattern.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,14 @@ extension AST.Quantification.Kind {
397397
}
398398
}
399399
}
400+
401+
extension DSLTree.QuantificationKind {
402+
var _patternBase: String {
403+
switch self {
404+
case .explicit(let kind), .syntax(let kind):
405+
return kind._patternBase
406+
case .default:
407+
return ".eager"
408+
}
409+
}
410+
}

Sources/_StringProcessing/Regex/ASTConversion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ extension AST.Node {
123123
case let .quantification(v):
124124
let child = v.child.dslTreeNode
125125
return .quantification(
126-
v.amount.value, v.kind.value, child)
126+
v.amount.value, .syntax(v.kind.value), child)
127127

128128
case let .quote(v):
129129
return .quotedLiteral(v.literal)

Sources/_StringProcessing/Regex/DSLTree.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extension DSLTree {
5656

5757
case quantification(
5858
AST.Quantification.Amount,
59-
AST.Quantification.Kind,
59+
QuantificationKind,
6060
Node)
6161

6262
case customCharacterClass(CustomCharacterClass)
@@ -103,6 +103,16 @@ extension DSLTree {
103103
}
104104

105105
extension DSLTree {
106+
@_spi(RegexBuilder)
107+
public enum QuantificationKind {
108+
/// The default quantification kind, as set by options.
109+
case `default`
110+
/// An explicitly chosen kind, overriding any options.
111+
case explicit(AST.Quantification.Kind)
112+
/// A kind set via syntax, which can be affected by options.
113+
case syntax(AST.Quantification.Kind)
114+
}
115+
106116
@_spi(RegexBuilder)
107117
public struct CustomCharacterClass {
108118
var members: [Member]

Sources/_StringProcessing/Regex/Options.swift

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,32 @@ extension RegexComponent {
5959
wrapInOption(.singleLine, addingIf: dotMatchesNewlines)
6060
}
6161

62+
/// Returns a regular expression where the start and end of input
63+
/// anchors (`^` and `$`) also match against the start and end of a line.
64+
///
65+
/// This method corresponds to applying the `m` option in a regular
66+
/// expression literal. For this behavior in the `RegexBuilder` syntax, see
67+
/// ``Anchor.startOfLine``, ``Anchor.endOfLine``, ``Anchor.startOfInput``,
68+
/// and ``Anchor.endOfInput``.
69+
///
70+
/// - Parameter matchLineEndings: A Boolean value indicating whether `^` and
71+
/// `$` should match the start and end of lines, respectively.
72+
public func anchorsMatchLineEndings(_ matchLineEndings: Bool = true) -> Regex<RegexOutput> {
73+
wrapInOption(.multiline, addingIf: matchLineEndings)
74+
}
75+
76+
/// Returns a regular expression where quantifiers are reluctant by default
77+
/// instead of eager.
78+
///
79+
/// This method corresponds to applying the `U` option in a regular
80+
/// expression literal.
81+
///
82+
/// - Parameter useReluctantQuantifiers: A Boolean value indicating whether
83+
/// quantifiers should be reluctant by default.
84+
public func reluctantQuantifiers(_ useReluctantQuantifiers: Bool = true) -> Regex<RegexOutput> {
85+
wrapInOption(.reluctantByDefault, addingIf: useReluctantQuantifiers)
86+
}
87+
6288
/// Returns a regular expression that matches with the specified semantic
6389
/// level.
6490
///
@@ -128,39 +154,6 @@ public struct RegexSemanticLevel: Hashable {
128154
}
129155
}
130156

131-
// Options that only affect literals
132-
@available(SwiftStdlib 5.7, *)
133-
extension RegexComponent {
134-
/// Returns a regular expression where the start and end of input
135-
/// anchors (`^` and `$`) also match against the start and end of a line.
136-
///
137-
/// This method corresponds to applying the `m` option in a regular
138-
/// expression literal, and only applies to regular expressions specified as
139-
/// literals. For this behavior in the `RegexBuilder` syntax, see
140-
/// ``Anchor.startOfLine``, ``Anchor.endOfLine``, ``Anchor.startOfInput``,
141-
/// and ``Anchor.endOfInput``.
142-
///
143-
/// - Parameter matchLineEndings: A Boolean value indicating whether `^` and
144-
/// `$` should match the start and end of lines, respectively.
145-
public func anchorsMatchLineEndings(_ matchLineEndings: Bool = true) -> Regex<RegexOutput> {
146-
wrapInOption(.multiline, addingIf: matchLineEndings)
147-
}
148-
149-
/// Returns a regular expression where quantifiers are reluctant by default
150-
/// instead of eager.
151-
///
152-
/// This method corresponds to applying the `U` option in a regular
153-
/// expression literal, and only applies to regular expressions specified as
154-
/// literals. In the `RegexBuilder` syntax, pass a ``QuantificationBehavior``
155-
/// value to any quantification method to change its behavior.
156-
///
157-
/// - Parameter useReluctantCaptures: A Boolean value indicating whether
158-
/// quantifiers should be reluctant by default.
159-
public func reluctantCaptures(_ useReluctantCaptures: Bool = true) -> Regex<RegexOutput> {
160-
wrapInOption(.reluctantByDefault, addingIf: useReluctantCaptures)
161-
}
162-
}
163-
164157
// MARK: - Helper method
165158

166159
@available(SwiftStdlib 5.7, *)

Tests/RegexBuilderTests/RegexDSLTests.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,24 @@ class RegexDSLTests: XCTestCase {
262262
}
263263
.ignoringCase(false)
264264
}
265+
266+
try _testDSLCaptures(
267+
("abcdef123", ("abcdef123", "a", "123")),
268+
matchType: (Substring, Substring, Substring).self, ==) {
269+
Capture {
270+
// Reluctant behavior due to option
271+
OneOrMore(.anyOf("abcd"))
272+
.reluctantQuantifiers()
273+
}
274+
ZeroOrMore("a"..."z")
275+
276+
Capture {
277+
// Eager behavior due to explicit parameter, despite option
278+
OneOrMore(.digit, .eagerly)
279+
.reluctantQuantifiers()
280+
}
281+
ZeroOrMore(.digit)
282+
}
265283
}
266284

267285
func testQuantificationBehavior() throws {
@@ -293,7 +311,7 @@ class RegexDSLTests: XCTestCase {
293311
OneOrMore(.word)
294312
Capture(.digit)
295313
ZeroOrMore(.any)
296-
}.reluctantCaptures()
314+
}.reluctantQuantifiers()
297315
}
298316
}
299317
#endif

0 commit comments

Comments
 (0)