Skip to content

Commit 52da66c

Browse files
authored
Add repeating functions to DSL (#161)
These allow you to spell literal expressions like `a{3,}` or `a{2,5}` while using the Regex DSL.
1 parent b7a0196 commit 52da66c

File tree

4 files changed

+492
-106
lines changed

4 files changed

+492
-106
lines changed

Sources/VariadicsGenerator/VariadicsGenerator.swift

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ struct VariadicsGenerator: ParsableCommand {
151151
print("\(kind.rawValue) ", terminator: "", to: &standardError)
152152
emitQuantifier(kind: kind, arity: arity)
153153
}
154+
print("repeating ", terminator: "", to: &standardError)
155+
emitRepeating(arity: arity)
154156
print(to: &standardError)
155157
}
156158

@@ -307,69 +309,140 @@ struct VariadicsGenerator: ParsableCommand {
307309
}
308310
}
309311
}
312+
313+
struct QuantifierParameters {
314+
var disfavored: String
315+
var genericParams: String
316+
var whereClause: String
317+
var quantifiedCaptures: String
318+
var matchType: String
319+
320+
var repeatingWhereClause: String {
321+
whereClause.isEmpty
322+
? "where R.Bound == Int"
323+
: whereClause + ", R.Bound == Int"
324+
}
325+
326+
init(kind: QuantifierKind, arity: Int) {
327+
self.disfavored = arity == 0 ? "@_disfavoredOverload\n" : ""
328+
self.genericParams = {
329+
var result = ""
330+
if arity > 0 {
331+
result += "W"
332+
result += (0..<arity).map { ", C\($0)" }.joined()
333+
result += ", "
334+
}
335+
result += "Component: \(regexProtocolName)"
336+
return result
337+
}()
338+
339+
let captures = (0..<arity).map { "C\($0)" }
340+
let capturesJoined = captures.joined(separator: ", ")
341+
self.whereClause = arity == 0 ? "" :
342+
"where Component.Match == (W, \(capturesJoined))"
343+
self.quantifiedCaptures = {
344+
switch kind {
345+
case .zeroOrOne, .zeroOrMore:
346+
return captures.map { "\($0)?" }.joined(separator: ", ")
347+
case .oneOrMore:
348+
return capturesJoined
349+
}
350+
}()
351+
self.matchType = arity == 0
352+
? baseMatchTypeName
353+
: "(\(baseMatchTypeName), \(quantifiedCaptures))"
354+
}
355+
}
310356

311357
func emitQuantifier(kind: QuantifierKind, arity: Int) {
312358
assert(arity >= 0)
313-
let genericParams: String = {
314-
var result = ""
315-
if arity > 0 {
316-
result += "W"
317-
result += (0..<arity).map { ", C\($0)" }.joined()
318-
result += ", "
319-
}
320-
result += "Component: \(regexProtocolName)"
321-
return result
322-
}()
323-
let captures = (0..<arity).map { "C\($0)" }
324-
let capturesJoined = captures.joined(separator: ", ")
325-
let whereClause: String = arity == 0 ? "" :
326-
"where Component.Match == (W, \(capturesJoined))"
327-
let quantifiedCaptures: String = {
328-
switch kind {
329-
case .zeroOrOne, .zeroOrMore:
330-
return captures.map { "\($0)?" }.joined(separator: ", ")
331-
case .oneOrMore:
332-
return capturesJoined
333-
}
334-
}()
335-
let matchType = arity == 0 ? baseMatchTypeName : "(\(baseMatchTypeName), \(quantifiedCaptures))"
359+
let params = QuantifierParameters(kind: kind, arity: arity)
336360
output("""
337-
\(arity == 0 ? "@_disfavoredOverload" : "")
338-
public func \(kind.rawValue)<\(genericParams)>(
361+
\(params.disfavored)\
362+
public func \(kind.rawValue)<\(params.genericParams)>(
339363
_ component: Component,
340364
_ behavior: QuantificationBehavior = .eagerly
341-
) -> \(regexTypeName)<\(matchType)> \(whereClause) {
365+
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
342366
.init(node: .quantification(.\(kind.astQuantifierAmount), behavior.astKind, component.regex.root))
343367
}
344368
345-
\(arity == 0 ? "@_disfavoredOverload" : "")
346-
public func \(kind.rawValue)<\(genericParams)>(
369+
\(params.disfavored)\
370+
public func \(kind.rawValue)<\(params.genericParams)>(
347371
_ behavior: QuantificationBehavior = .eagerly,
348372
@RegexBuilder _ component: () -> Component
349-
) -> \(regexTypeName)<\(matchType)> \(whereClause) {
373+
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
350374
.init(node: .quantification(.\(kind.astQuantifierAmount), behavior.astKind, component().regex.root))
351375
}
352376
353-
\(arity == 0 ? "@_disfavoredOverload" : "")
354-
public postfix func \(kind.operatorName)<\(genericParams)>(
377+
\(params.disfavored)\
378+
public postfix func \(kind.operatorName)<\(params.genericParams)>(
355379
_ component: Component
356-
) -> \(regexTypeName)<\(matchType)> \(whereClause) {
380+
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
357381
.init(node: .quantification(.\(kind.astQuantifierAmount), .eager, component.regex.root))
358382
}
359383
360384
\(kind == .zeroOrOne ?
361385
"""
362386
extension RegexBuilder {
363-
public static func buildLimitedAvailability<\(genericParams)>(
387+
public static func buildLimitedAvailability<\(params.genericParams)>(
364388
_ component: Component
365-
) -> \(regexTypeName)<\(matchType)> \(whereClause) {
389+
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
366390
.init(node: .quantification(.\(kind.astQuantifierAmount), .eager, component.regex.root))
367391
}
368392
}
369393
""" : "")
370394
371395
""")
372396
}
397+
398+
func emitRepeating(arity: Int) {
399+
assert(arity >= 0)
400+
// `repeat(..<5)` has the same generic semantics as zeroOrMore
401+
let params = QuantifierParameters(kind: .zeroOrMore, arity: arity)
402+
// TODO: Could `repeat(count:)` have the same generic semantics as oneOrMore?
403+
// We would need to prohibit `repeat(count: 0)`; can only happen at runtime
404+
405+
output("""
406+
\(params.disfavored)\
407+
public func repeating<\(params.genericParams)>(
408+
_ component: Component,
409+
count: Int
410+
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
411+
assert(count > 0, "Must specify a positive count")
412+
// TODO: Emit a warning about `repeatMatch(count: 0)` or `repeatMatch(count: 1)`
413+
return Regex(node: .quantification(.exactly(.init(faking: count)), .eager, component.regex.root))
414+
}
415+
416+
\(params.disfavored)\
417+
public func repeating<\(params.genericParams)>(
418+
count: Int,
419+
@RegexBuilder _ component: () -> Component
420+
) -> \(regexTypeName)<\(params.matchType)> \(params.whereClause) {
421+
assert(count > 0, "Must specify a positive count")
422+
// TODO: Emit a warning about `repeatMatch(count: 0)` or `repeatMatch(count: 1)`
423+
return Regex(node: .quantification(.exactly(.init(faking: count)), .eager, component().regex.root))
424+
}
425+
426+
\(params.disfavored)\
427+
public func repeating<\(params.genericParams), R: RangeExpression>(
428+
_ component: Component,
429+
_ expression: R,
430+
_ behavior: QuantificationBehavior = .eagerly
431+
) -> \(regexTypeName)<\(params.matchType)> \(params.repeatingWhereClause) {
432+
.init(node: .repeating(expression.relative(to: 0..<Int.max), behavior, component.regex.root))
433+
}
434+
435+
\(params.disfavored)\
436+
public func repeating<\(params.genericParams), R: RangeExpression>(
437+
_ expression: R,
438+
_ behavior: QuantificationBehavior = .eagerly,
439+
@RegexBuilder _ component: () -> Component
440+
) -> \(regexTypeName)<\(params.matchType)> \(params.repeatingWhereClause) {
441+
.init(node: .repeating(expression.relative(to: 0..<Int.max), behavior, component().regex.root))
442+
}
443+
444+
""")
445+
}
373446

374447
func emitAlternation(leftArity: Int, rightArity: Int) {
375448
let leftGenParams: String = {

Sources/_StringProcessing/RegexDSL/DSLTree.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,33 @@ extension DSLTree.Node {
307307
return .alternation([self, newNode])
308308
}
309309
}
310+
311+
extension DSLTree.Node {
312+
/// Generates a DSLTree node for a repeated range of the given DSLTree node.
313+
/// Individual public API functions are in the generated Variadics.swift file.
314+
static func repeating(
315+
_ range: Range<Int>,
316+
_ behavior: QuantificationBehavior,
317+
_ node: DSLTree.Node
318+
) -> DSLTree.Node {
319+
// TODO: Throw these as errors
320+
assert(range.lowerBound >= 0, "Cannot specify a negative lower bound")
321+
assert(!range.isEmpty, "Cannot specify an empty range")
322+
323+
switch (range.lowerBound, range.upperBound) {
324+
case (0, Int.max): // 0...
325+
return .quantification(.zeroOrMore, behavior.astKind, node)
326+
case (1, Int.max): // 1...
327+
return .quantification(.oneOrMore, behavior.astKind, node)
328+
case _ where range.count == 1: // ..<1 or ...0 or any range with count == 1
329+
// Note: `behavior` is ignored in this case
330+
return .quantification(.exactly(.init(faking: range.lowerBound)), .eager, node)
331+
case (0, _): // 0..<n or 0...n or ..<n or ...n
332+
return .quantification(.upToN(.init(faking: range.upperBound)), behavior.astKind, node)
333+
case (_, Int.max): // n...
334+
return .quantification(.nOrMore(.init(faking: range.lowerBound)), behavior.astKind, node)
335+
default: // any other range
336+
return .quantification(.range(.init(faking: range.lowerBound), .init(faking: range.upperBound)), behavior.astKind, node)
337+
}
338+
}
339+
}

0 commit comments

Comments
 (0)