Skip to content

Commit 874c302

Browse files
committed
Throwing matches and update to CustomMatchingRegexComponent
- Update the name `CustomRegexComponent` to `CustomMatchingRegexComponent` per pitch - Adopt `throws` for `CustomMatchingRegexComponent` as added in swiftlang#261. Errors thrown by `CustomMatchingRegexComponent`'s conformers will be bubbled up to the engine and surfaced at client-side.
1 parent ce458eb commit 874c302

File tree

5 files changed

+178
-17
lines changed

5 files changed

+178
-17
lines changed

Documentation/Evolution/StringProcessingAlgorithms.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public protocol CustomMatchingRegexComponent : RegexComponent {
187187
_ input: String,
188188
startingAt index: String.Index,
189189
in bounds: Range<String.Index>
190-
) -> (upperBound: String.Index, match: Match)?
190+
) throws -> (upperBound: String.Index, match: Match)?
191191
}
192192
```
193193

Sources/_StringProcessing/ByteCodeGen.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,15 @@ extension Compiler.ByteCodeGen {
293293
mutating func emitMatcher(
294294
_ matcher: @escaping _MatcherInterface,
295295
into capture: CaptureRegister? = nil
296-
) {
296+
) throws {
297297

298298
// TODO: Consider emitting consumer interface if
299299
// not captured. This may mean we should store
300300
// an existential instead of a closure...
301301

302-
let matcher = builder.makeMatcherFunction(matcher)
302+
let matcher = builder.makeMatcherFunction { input, start, range in
303+
try matcher(input, start, range)
304+
}
303305

304306
let valReg = builder.makeValueRegister()
305307
builder.buildMatcher(matcher, into: valReg)
@@ -576,7 +578,7 @@ extension Compiler.ByteCodeGen {
576578
let cap = builder.makeCapture(id: refId)
577579
switch child {
578580
case let .matcher(_, m):
579-
emitMatcher(m, into: cap)
581+
try emitMatcher(m, into: cap)
580582
case let .transform(t, child):
581583
try emitTransform(t, child, into: cap)
582584
default:
@@ -639,7 +641,7 @@ extension Compiler.ByteCodeGen {
639641
throw Unsupported("consumer")
640642

641643
case let .matcher(_, f):
642-
emitMatcher(f)
644+
try emitMatcher(f)
643645

644646
case .transform:
645647
throw Unreachable(

Sources/_StringProcessing/Regex/DSLConsumers.swift

+8-6
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
public protocol CustomRegexComponent: RegexComponent {
12+
public protocol CustomMatchingRegexComponent: RegexComponent {
1313
func match(
1414
_ input: String,
1515
startingAt index: String.Index,
1616
in bounds: Range<String.Index>
17-
) -> (upperBound: String.Index, output: Output)?
17+
) throws -> (upperBound: String.Index, output: Output)?
1818
}
1919

20-
extension CustomRegexComponent {
20+
extension CustomMatchingRegexComponent {
2121
public var regex: Regex<Output> {
22-
Regex(node: .matcher(.init(Output.self), { input, index, bounds in
23-
match(input, startingAt: index, in: bounds)
24-
}))
22+
23+
let node: DSLTree.Node = .matcher(.init(Output.self), { input, index, bounds in
24+
try match(input, startingAt: index, in: bounds)
25+
})
26+
return Regex(node: node)
2527
}
2628
}

Tests/RegexBuilderTests/CustomTests.swift

+159-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import _StringProcessing
1414
@testable import RegexBuilder
1515

1616
// A nibbler processes a single character from a string
17-
private protocol Nibbler: CustomRegexComponent {
17+
private protocol Nibbler: CustomMatchingRegexComponent {
1818
func nibble(_: Character) -> Output?
1919
}
2020

@@ -24,7 +24,7 @@ extension Nibbler {
2424
_ input: String,
2525
startingAt index: String.Index,
2626
in bounds: Range<String.Index>
27-
) -> (upperBound: String.Index, output: Output)? {
27+
) throws -> (upperBound: String.Index, output: Output)? {
2828
guard index != bounds.upperBound, let res = nibble(input[index]) else {
2929
return nil
3030
}
@@ -49,6 +49,68 @@ private struct Asciibbler: Nibbler {
4949
}
5050
}
5151

52+
private struct IntParser: CustomMatchingRegexComponent {
53+
struct ParseError: Error, Hashable {}
54+
typealias Output = Int
55+
func match(_ input: String,
56+
startingAt index: String.Index,
57+
in bounds: Range<String.Index>
58+
) throws -> (upperBound: String.Index, output: Int)? {
59+
let r = Regex {
60+
Capture(OneOrMore(.digit)) { Int($0) }
61+
}
62+
guard let match = input[index..<bounds.upperBound].wholeMatch(of: r),
63+
let output = match.output.1 else {
64+
throw ParseError()
65+
}
66+
67+
return (match.range.upperBound, output)
68+
}
69+
}
70+
71+
private struct CurrencyParser: CustomMatchingRegexComponent {
72+
enum Currency: String {
73+
case usd = "USD"
74+
case ntd = "NTD"
75+
case dem = "DEM"
76+
}
77+
78+
enum ParseError: Error, Hashable {
79+
case unrecognized
80+
case deprecated
81+
}
82+
83+
typealias Output = Currency
84+
func match(_ input: String,
85+
startingAt index: String.Index,
86+
in bounds: Range<String.Index>
87+
) throws -> (upperBound: String.Index, output: Currency)? {
88+
89+
guard index != bounds.upperBound else {
90+
return nil
91+
}
92+
93+
let substr = input[index..<bounds.upperBound]
94+
95+
let currencies: [Currency] = [ .usd, .ntd ]
96+
let deprecated: [Currency] = [ .dem ]
97+
98+
for currency in currencies {
99+
if let range = substr.range(of: currency.rawValue) {
100+
return (range.upperBound, currency)
101+
}
102+
}
103+
104+
for dep in deprecated {
105+
if let _ = substr.range(of: dep.rawValue) {
106+
throw ParseError.deprecated
107+
}
108+
}
109+
110+
throw ParseError.unrecognized
111+
}
112+
}
113+
52114
enum MatchCall {
53115
case match
54116
case firstMatch
@@ -223,4 +285,99 @@ class CustomRegexComponentTests: XCTestCase {
223285

224286

225287
}
288+
289+
func testCustomRegexThrows() {
290+
291+
func customTest<Match: Equatable, E: Error & Equatable>(
292+
_ regex: Regex<Match>,
293+
_ tests: (input: String, match: Match?, expectError: E?)...,
294+
file: StaticString = #file,
295+
line: UInt = #line
296+
) {
297+
for (input, match, expectError) in tests {
298+
do {
299+
let result = try regex.wholeMatch(in: input)?.output
300+
XCTAssertEqual(result, match)
301+
} catch let e as E {
302+
XCTAssertEqual(e, expectError)
303+
} catch {
304+
XCTFail()
305+
}
306+
}
307+
}
308+
309+
customTest(
310+
Regex {
311+
IntParser()
312+
},
313+
("zzz", nil, IntParser.ParseError()),
314+
("x10x", nil, IntParser.ParseError()),
315+
("30", 30, nil)
316+
)
317+
318+
customTest(
319+
Regex {
320+
CurrencyParser()
321+
},
322+
("USD", .usd, nil),
323+
("NTD", .ntd, nil),
324+
("NTD USD", .usd, nil),
325+
("DEM", nil, CurrencyParser.ParseError.deprecated),
326+
("XXX", nil, CurrencyParser.ParseError.unrecognized)
327+
)
328+
329+
customTest(
330+
Regex {
331+
CurrencyParser()
332+
IntParser()
333+
},
334+
("USD100", "USD100", nil),
335+
("USD100.000", "USD100", IntParser.ParseError())
336+
)
337+
338+
customTest(
339+
Regex {
340+
CurrencyParser()
341+
IntParser()
342+
},
343+
("XXX100", nil, CurrencyParser.ParseError.unrecognized),
344+
("XXX100.00", nil, CurrencyParser.ParseError.unrecognized),
345+
("DEM100", nil, CurrencyParser.ParseError.deprecated)
346+
)
347+
348+
func currencyTest(
349+
_ regex: Regex<(Substring, CurrencyParser.Currency, Int)>,
350+
_ tests: (input: String, match: (Substring, CurrencyParser.Currency, Int)?, expectError1: CurrencyParser.ParseError?, expectError2: IntParser.ParseError?)...,
351+
file: StaticString = #file,
352+
line: UInt = #line
353+
) {
354+
for (input, match, expectError1, expectError2) in tests {
355+
do {
356+
let result = try regex.wholeMatch(in: input)?.output
357+
XCTAssertEqual(result?.0, match?.0)
358+
XCTAssertEqual(result?.1, match?.1)
359+
XCTAssertEqual(result?.2, match?.2)
360+
} catch let e as CurrencyParser.ParseError {
361+
XCTAssertEqual(e, expectError1)
362+
} catch let e as IntParser.ParseError {
363+
XCTAssertEqual(e, expectError2)
364+
} catch {
365+
XCTFail("caught error: \(error.localizedDescription)")
366+
}
367+
}
368+
}
369+
370+
currencyTest(
371+
Regex {
372+
Capture(CurrencyParser())
373+
Capture(IntParser())
374+
},
375+
("USD100", ("USD100", .usd, 100), nil, nil),
376+
("NTD500", ("NTD500", .ntd, 500), nil, nil),
377+
("XXX20", nil, .unrecognized, IntParser.ParseError()),
378+
("DEM500", nil, .deprecated, nil),
379+
("DEM500.345", nil, .deprecated, IntParser.ParseError()),
380+
("NTD100.345", nil, nil, IntParser.ParseError())
381+
)
382+
}
226383
}

Tests/RegexBuilderTests/RegexDSLTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -743,13 +743,13 @@ class RegexDSLTests: XCTestCase {
743743
var patch: Int
744744
var dev: String?
745745
}
746-
struct SemanticVersionParser: CustomRegexComponent {
746+
struct SemanticVersionParser: CustomMatchingRegexComponent {
747747
typealias Output = SemanticVersion
748748
func match(
749749
_ input: String,
750750
startingAt index: String.Index,
751751
in bounds: Range<String.Index>
752-
) -> (upperBound: String.Index, output: SemanticVersion)? {
752+
) throws -> (upperBound: String.Index, output: SemanticVersion)? {
753753
let regex = Regex {
754754
TryCapture(OneOrMore(.digit)) { Int($0) }
755755
"."
@@ -776,13 +776,13 @@ class RegexDSLTests: XCTestCase {
776776
return (match.range.upperBound, result)
777777
}
778778
}
779-
779+
780780
let versions = [
781781
("1.0", SemanticVersion(major: 1, minor: 0, patch: 0)),
782782
("1.0.1", SemanticVersion(major: 1, minor: 0, patch: 1)),
783783
("12.100.5-dev", SemanticVersion(major: 12, minor: 100, patch: 5, dev: "dev")),
784784
]
785-
785+
786786
let parser = SemanticVersionParser()
787787
for (str, version) in versions {
788788
XCTAssertEqual(str.wholeMatch(of: parser)?.output, version)

0 commit comments

Comments
 (0)