Skip to content

Commit b94c9d5

Browse files
authored
Merge pull request #121 from rxwei/try-capture
2 parents 8b288c3 + d6955f0 commit b94c9d5

File tree

9 files changed

+217
-37
lines changed

9 files changed

+217
-37
lines changed

Sources/Exercises/Participants/RegexParticipant.swift

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ struct RegexLiteralParticipant: Participant {
4343
}
4444
}
4545

46+
// MARK: - Regex literal
47+
4648
private func extractFromCaptures(
4749
_ match: Tuple4<Substring, Substring, Substring?, Substring>
4850
) -> GraphemeBreakEntry? {
@@ -63,28 +65,33 @@ private func graphemeBreakPropertyData<RP: RegexProtocol>(
6365
line.match(regex).map(\.match).flatMap(extractFromCaptures)
6466
}
6567

68+
private func graphemeBreakPropertyDataLiteral(
69+
forLine line: String
70+
) -> GraphemeBreakEntry? {
71+
return graphemeBreakPropertyData(
72+
forLine: line,
73+
using: r(#"([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+(\w+).*"#,
74+
matching: Tuple4<Substring, Substring, Substring?, Substring>.self))
75+
}
76+
77+
// MARK: - Builder DSL
78+
6679
private func graphemeBreakPropertyData(
6780
forLine line: String
6881
) -> GraphemeBreakEntry? {
69-
graphemeBreakPropertyData(forLine: line, using: Regex {
70-
OneOrMore(.hexDigit).capture()
82+
line.match {
83+
OneOrMore(.hexDigit).tryCapture(Unicode.Scalar.init(hex:))
7184
Optionally {
7285
".."
73-
OneOrMore(.hexDigit).capture()
86+
OneOrMore(.hexDigit).tryCapture(Unicode.Scalar.init(hex:))
7487
}
7588
OneOrMore(.whitespace)
7689
";"
7790
OneOrMore(.whitespace)
78-
OneOrMore(.word).capture()
91+
OneOrMore(.word).tryCapture(Unicode.GraphemeBreakProperty.init)
7992
Repeat(.any)
80-
})
81-
}
82-
83-
private func graphemeBreakPropertyDataLiteral(
84-
forLine line: String
85-
) -> GraphemeBreakEntry? {
86-
return graphemeBreakPropertyData(
87-
forLine: line,
88-
using: r(#"([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+(\w+).*"#,
89-
matching: Tuple4<Substring, Substring, Substring?, Substring>.self))
93+
}.map {
94+
let (_, lower, upper, property) = $0.match.tuple
95+
return GraphemeBreakEntry(lower...(upper ?? lower), property)
96+
}
9097
}

Sources/_MatchingEngine/Regex/AST/AST.swift

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,18 +210,61 @@ extension AST {
210210

211211
// FIXME: Get this out of here
212212
public struct CaptureTransform: Equatable, Hashable, CustomStringConvertible {
213+
public enum Closure {
214+
case nonfailable((Substring) -> Any)
215+
case failable((Substring) -> Any?)
216+
case throwing((Substring) throws -> Any)
217+
}
213218
public let resultType: Any.Type
214-
public let closure: (Substring) -> Any
219+
public let closure: Closure
215220

216-
public init(resultType: Any.Type, _ closure: @escaping (Substring) -> Any) {
221+
public init(resultType: Any.Type, closure: Closure) {
217222
self.resultType = resultType
218223
self.closure = closure
219224
}
220225

221-
public func callAsFunction(_ input: Substring) -> Any {
222-
let result = closure(input)
223-
assert(type(of: result) == resultType)
224-
return result
226+
public init(
227+
resultType: Any.Type,
228+
_ closure: @escaping (Substring) -> Any
229+
) {
230+
self.init(resultType: resultType, closure: .nonfailable(closure))
231+
}
232+
233+
public init(
234+
resultType: Any.Type,
235+
_ closure: @escaping (Substring) -> Any?
236+
) {
237+
self.init(resultType: resultType, closure: .failable(closure))
238+
}
239+
240+
public init(
241+
resultType: Any.Type,
242+
_ closure: @escaping (Substring) throws -> Any
243+
) {
244+
self.init(resultType: resultType, closure: .throwing(closure))
245+
}
246+
247+
public func callAsFunction(_ input: Substring) -> Any? {
248+
switch closure {
249+
case .nonfailable(let closure):
250+
let result = closure(input)
251+
assert(type(of: result) == resultType)
252+
return result
253+
case .failable(let closure):
254+
guard let result = closure(input) else {
255+
return nil
256+
}
257+
assert(type(of: result) == resultType)
258+
return result
259+
case .throwing(let closure):
260+
do {
261+
let result = try closure(input)
262+
assert(type(of: result) == resultType)
263+
return result
264+
} catch {
265+
return nil
266+
}
267+
}
225268
}
226269

227270
public static func == (lhs: CaptureTransform, rhs: CaptureTransform) -> Bool {

Sources/_StringProcessing/Legacy/HareVM.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ struct HareVM: VirtualMachine {
188188
bunny.hop()
189189

190190
case let .endCapture(transform):
191-
bunny.core.endCapture(bunny.sp, transform: transform)
191+
guard bunny.core.endCapture(bunny.sp, transform: transform) else {
192+
return nil
193+
}
192194
bunny.hop()
193195

194196
case .beginGroup:

Sources/_StringProcessing/Legacy/TortoiseVM.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,11 @@ extension TortoiseVM {
108108
hatchling.core.beginCapture(sp)
109109
hatchling.plod()
110110
case .endCapture(let transform):
111-
hatchling.core.endCapture(sp, transform: transform)
112-
hatchling.plod()
111+
if hatchling.core.endCapture(sp, transform: transform) {
112+
hatchling.plod()
113+
} else {
114+
hatchling.plod(to: code.endIndex)
115+
}
113116
case .beginGroup:
114117
hatchling.core.beginGroup()
115118
hatchling.plod()

Sources/_StringProcessing/Legacy/VirtualMachine.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,25 @@ extension RECode {
117117
captureState.start(at: index)
118118
}
119119

120-
mutating func endCapture(_ endIndex: String.Index, transform: CaptureTransform?) {
120+
/// Ends the current capture at the given index and applies the given
121+
/// transform if available. Returns true on success. Returns false if the
122+
/// trasnform failed.
123+
mutating func endCapture(
124+
_ endIndex: String.Index, transform: CaptureTransform?
125+
) -> Bool {
121126
let range = captureState.end(at: endIndex)
122127
let substring = input[range]
123-
let value = transform?(substring) ?? substring
128+
let value: Any
129+
if let transform = transform {
130+
guard let transformed = transform(substring) else {
131+
return false
132+
}
133+
value = transformed
134+
} else {
135+
value = substring
136+
}
124137
topLevelCaptures.append(.atom(value))
138+
return true
125139
}
126140

127141
mutating func beginGroup() {

Sources/_StringProcessing/RegexDSL/DSL.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,46 @@ public struct CapturingGroup<Match: MatchProtocol>: RegexProtocol {
172172
)
173173
}
174174

175+
init<Component: RegexProtocol>(
176+
_ component: Component,
177+
transform: CaptureTransform
178+
) {
179+
self.regex = .init(
180+
ast: .groupTransform(
181+
.init(.init(faking: .capture), component.regex.ast, .fake),
182+
transform: transform))
183+
}
184+
175185
init<NewCapture, Component: RegexProtocol>(
176186
_ component: Component,
177187
transform: @escaping (Substring) -> NewCapture
178188
) {
179-
self.regex = .init(ast:
180-
.groupTransform(
181-
.init(.init(faking: .capture), component.regex.ast, .fake),
182-
transform: CaptureTransform(resultType: NewCapture.self) {
183-
transform($0) as Any
184-
}))
189+
self.init(
190+
component,
191+
transform: CaptureTransform(resultType: NewCapture.self) {
192+
transform($0) as Any
193+
})
194+
}
195+
196+
init<NewCapture, Component: RegexProtocol>(
197+
_ component: Component,
198+
transform: @escaping (Substring) throws -> NewCapture
199+
) {
200+
self.init(
201+
component,
202+
transform: CaptureTransform(resultType: NewCapture.self) {
203+
try transform($0) as Any
204+
})
205+
}
206+
207+
init<NewCapture, Component: RegexProtocol>(
208+
_ component: Component,
209+
transform: @escaping (Substring) -> NewCapture?
210+
) {
211+
self.init(
212+
component,
213+
transform: CaptureTransform(resultType: NewCapture.self) {
214+
transform($0) as Any?
215+
})
185216
}
186217
}

Sources/_StringProcessing/RegexDSL/DSLCapture.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ extension RegexProtocol {
2020
.init(self, transform: transform)
2121
}
2222

23+
public func tryCapture<NewCapture>(
24+
_ transform: @escaping (Substring) throws -> NewCapture
25+
) -> CapturingGroup<Tuple2<Substring, NewCapture>> where Match.Capture: EmptyCaptureProtocol {
26+
.init(self, transform: transform)
27+
}
28+
29+
public func tryCapture<NewCapture>(
30+
_ transform: @escaping (Substring) -> NewCapture?
31+
) -> CapturingGroup<Tuple2<Substring, NewCapture>> where Match.Capture: EmptyCaptureProtocol {
32+
.init(self, transform: transform)
33+
}
34+
2335
public func capture<C0>() -> CapturingGroup<Tuple3<Substring, Substring, C0>>
2436
where Match.Capture == C0 {
2537
.init(self)
@@ -31,6 +43,18 @@ extension RegexProtocol {
3143
.init(self, transform: transform)
3244
}
3345

46+
public func tryCapture<NewCapture, C0>(
47+
_ transform: @escaping (Substring) throws -> NewCapture
48+
) -> CapturingGroup<Tuple3<Substring, NewCapture, C0>> where Match.Capture == C0 {
49+
.init(self, transform: transform)
50+
}
51+
52+
public func tryCapture<NewCapture, C0>(
53+
_ transform: @escaping (Substring) -> NewCapture?
54+
) -> CapturingGroup<Tuple3<Substring, NewCapture, C0>> where Match.Capture == C0 {
55+
.init(self, transform: transform)
56+
}
57+
3458
public func capture<C0, C1>() -> CapturingGroup<Tuple4<Substring, Substring, C0, C1>> where Match.Capture == Tuple2<C0, C1> {
3559
.init(self)
3660
}
@@ -41,6 +65,18 @@ extension RegexProtocol {
4165
.init(self, transform: transform)
4266
}
4367

68+
public func tryCapture<NewCapture, C0, C1>(
69+
_ transform: @escaping (Substring) throws -> NewCapture
70+
) -> CapturingGroup<Tuple4<Substring, NewCapture, C0, C1>> where Match.Capture == Tuple2<C0, C1> {
71+
.init(self, transform: transform)
72+
}
73+
74+
public func tryCapture<NewCapture, C0, C1>(
75+
_ transform: @escaping (Substring) -> NewCapture?
76+
) -> CapturingGroup<Tuple4<Substring, NewCapture, C0, C1>> where Match.Capture == Tuple2<C0, C1> {
77+
.init(self, transform: transform)
78+
}
79+
4480
public func capture<C0, C1, C2>() -> CapturingGroup<Tuple5<Substring, Substring, C0, C1, C2>>
4581
where Match.Capture == Tuple3<C0, C1, C2> {
4682
.init(self)
@@ -51,6 +87,18 @@ extension RegexProtocol {
5187
) -> CapturingGroup<Tuple5<Substring, NewCapture, C0, C1, C2>> where Match.Capture == Tuple3<C0, C1, C2> {
5288
.init(self, transform: transform)
5389
}
90+
91+
public func tryCapture<NewCapture, C0, C1, C2>(
92+
_ transform: @escaping (Substring) throws -> NewCapture
93+
) -> CapturingGroup<Tuple5<Substring, NewCapture, C0, C1, C2>> where Match.Capture == Tuple3<C0, C1, C2> {
94+
.init(self, transform: transform)
95+
}
96+
97+
public func tryCapture<NewCapture, C0, C1, C2>(
98+
_ transform: @escaping (Substring) -> NewCapture?
99+
) -> CapturingGroup<Tuple5<Substring, NewCapture, C0, C1, C2>> where Match.Capture == Tuple3<C0, C1, C2> {
100+
.init(self, transform: transform)
101+
}
54102
}
55103

56104
/* Or using parameterized extensions and variadic generics.

Tests/ExercisesTests/ExercisesTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ class ExercisesTests: XCTestCase {
3939
let result = f(line)
4040
guard ref == result else {
4141
pass = false
42-
XCTFail("Participant \(participant.name) failed")
42+
XCTFail("""
43+
Participant \(participant.name) failed
44+
- Input: \(line)
45+
- Expected: \(String(describing: ref))
46+
- Result: \(String(describing: result))
47+
""")
4348
break
4449
}
4550
}

Tests/RegexTests/RegexDSLTests.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class RegexDSLTests: XCTestCase {
1717
let regex = Regex {
1818
"a"
1919
Character("b").capture() // Character
20-
"1".capture { Int($0)! } // Int
20+
"1".tryCapture { Int($0) } // Int
2121
}
2222
// Assert the inferred capture type.
2323
let _: Tuple3<Substring, Substring, Int>.Type = type(of: regex).Match.self
@@ -150,7 +150,7 @@ class RegexDSLTests: XCTestCase {
150150
let regex2 = Regex {
151151
"a".+
152152
Regex {
153-
"b".capture { Int($0)! }.*
153+
"b".tryCapture { Int($0) }.*
154154
"e".?
155155
}.capture()
156156
}
@@ -159,8 +159,8 @@ class RegexDSLTests: XCTestCase {
159159
let regex3 = Regex {
160160
"a".+
161161
Regex {
162-
"b".capture { Int($0)! }
163-
"c".capture { Double($0)! }.*
162+
"b".tryCapture { Int($0) }
163+
"c".tryCapture { Double($0) }.*
164164
"e".?
165165
}.capture()
166166
}
@@ -256,6 +256,33 @@ class RegexDSLTests: XCTestCase {
256256
XCTAssertEqual(propertyString, "Extend")
257257
}
258258

259+
let regexWithTryCapture = Regex {
260+
OneOrMore(CharacterClass.hexDigit).tryCapture(Unicode.Scalar.init(hex:))
261+
Optionally {
262+
".."
263+
OneOrMore(CharacterClass.hexDigit).tryCapture(Unicode.Scalar.init(hex:))
264+
}
265+
OneOrMore(CharacterClass.whitespace)
266+
";"
267+
OneOrMore(CharacterClass.whitespace)
268+
OneOrMore(CharacterClass.word).capture()
269+
Repeat(CharacterClass.any)
270+
} // Regex<(Substring, Unicode.Scalar, Unicode.Scalar?, Substring)>
271+
do {
272+
// Assert the inferred capture type.
273+
typealias ExpectedMatch = Tuple4<
274+
Substring, Unicode.Scalar, Unicode.Scalar?, Substring
275+
>
276+
let _: ExpectedMatch.Type = type(of: regexWithTryCapture).Match.self
277+
let maybeMatchResult = line.match(regexWithTryCapture)
278+
let matchResult = try XCTUnwrap(maybeMatchResult)
279+
let (wholeMatch, lower, upper, propertyString) = matchResult.match.tuple
280+
XCTAssertEqual(wholeMatch, Substring(line))
281+
XCTAssertEqual(lower, Unicode.Scalar(0xA6F0))
282+
XCTAssertEqual(upper, Unicode.Scalar(0xA6F1))
283+
XCTAssertEqual(propertyString, "Extend")
284+
}
285+
259286
do {
260287
let regexLiteral = try MockRegexLiteral(
261288
#"([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+(\w+).*"#,
@@ -297,7 +324,7 @@ class RegexDSLTests: XCTestCase {
297324

298325
extension Unicode.Scalar {
299326
// Convert a hexadecimal string to a scalar
300-
public init?<S: StringProtocol>(hex: S) {
327+
init?<S: StringProtocol>(hex: S) {
301328
guard let val = UInt32(hex, radix: 16), let scalar = Self(val) else {
302329
return nil
303330
}

0 commit comments

Comments
 (0)