Skip to content

Commit 217b29c

Browse files
committed
Add documentation, some small renaming.
1 parent a6c7748 commit 217b29c

File tree

9 files changed

+113
-39
lines changed

9 files changed

+113
-39
lines changed

Sources/Patterns/Decoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ extension Parser.Match where Input == String {
135135
var codingPath: [CodingKey] = []
136136
@usableFromInline
137137
var allKeys: [Key] {
138-
matchDecoder.match.names.compactMap(Key.init(stringValue:))
138+
matchDecoder.match.captureNames.compactMap(Key.init(stringValue:))
139139
}
140140

141141
@usableFromInline

Sources/Patterns/Grammar.swift

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,33 @@
55
// Created by Kåre Morstøl on 27/05/2020.
66
//
77

8+
/// Allows for recursive patterns, also indirectly.
9+
///
10+
/// Define subpatterns using `<-`, like this arithmetic pattern:
11+
/// ```
12+
/// let g = Grammar { g in
13+
/// g.all <- g.expr • !any
14+
/// g.expr <- g.sum
15+
/// g.sum <- g.product • (("+" / "-") • g.product)*
16+
/// g.product <- g.power • (("*" / "/") • g.power)*
17+
/// g.power <- g.value • ("^" • g.power)¿
18+
/// g.value <- digit+ / "(" • g.expr • ")"
19+
/// }
20+
/// ```
21+
/// This recognises e.g. "1+2-3*(4+3)"
22+
///
23+
/// - warning: Does not support left recursion:
24+
/// ```
25+
/// g.a <- g.a • g.b
26+
/// ```
27+
/// will lead to infinite recursion.
828
@dynamicMemberLookup
929
public class Grammar: Pattern {
30+
/// Calls another subpattern in a grammar.
1031
public struct CallPattern: Pattern {
32+
/// The grammar that contains the subpattern being called.
1133
public let grammar: Grammar
34+
/// The name of the subpattern being called.
1235
public let name: String
1336
public var description: String { "<\(name)>" }
1437

@@ -26,8 +49,10 @@ public class Grammar: Pattern {
2649

2750
public var description: String { "Grammar" } // TODO:
2851

52+
/// All the subpatterns and their names.
2953
public internal(set) var patterns: [(name: String, pattern: AnyPattern)] = []
3054

55+
/// The main subpattern, which will be called when this Grammar is being used.
3156
public var firstPattern: String? { patterns.first?.name }
3257

3358
@inlinable
@@ -40,35 +65,41 @@ public class Grammar: Pattern {
4065
}
4166

4267
@inlinable
68+
/// Allows the use of e.g. `g.a` to refer to subpatterns.
4369
public subscript(dynamicMember name: String) -> CallPattern {
4470
CallPattern(grammar: self, name: name)
4571
}
4672

4773
@inlinable
48-
public func createInstructions(_ finalInstructions: inout Instructions) throws {
49-
var instructions = finalInstructions
74+
public func createInstructions(_ instructions: inout Instructions) throws {
75+
// We begin with a call to the first subpattern, followed by a jump to the end.
76+
// This enables this grammar to be used inside other patterns (including other grammars).
77+
5078
let startIndex = instructions.endIndex
5179
instructions.append(
52-
.openCall(name: try firstPattern ?? Parser<Input>.InitError.message("Grammar is empty.")))
80+
.openCall(name: try firstPattern ?? Parser<Input>.PatternError.message("Grammar is empty.")))
5381
instructions.append(.jump(offset: .max)) // replaced later
5482
var callTable = [String: Range<Instructions.Index>]()
83+
84+
// Create instructions for all subpatterns. Store their positions in `callTable`.
5585
for (name, pattern) in patterns {
5686
let startIndex = instructions.endIndex
5787
try pattern.createInstructions(&instructions)
5888
instructions.append(.return)
5989
guard (startIndex ..< instructions.endIndex).count > 1 else {
60-
throw Parser<Input>.InitError.message("Pattern '\(name) <- \(pattern)' was empty.")
90+
throw Parser<Input>.PatternError.message("Pattern '\(name) <- \(pattern)' was empty.")
6191
}
6292
callTable[name] = startIndex ..< instructions.endIndex
6393
}
6494

95+
// Replace all `.openCall` with `.call(offset)` and the correct offsets.
6596
for i in instructions.indices[startIndex...] {
6697
if case let .openCall(name) = instructions[i] {
6798
guard let subpatternRange = callTable[name] else {
68-
throw Parser<Input>.InitError.message("Pattern '\(name)' was never defined with ´<-´ operator.")
99+
throw Parser<Input>.PatternError.message("Pattern '\(name)' was never defined with ´<-´ operator.")
69100
}
70-
// If the last non-dummy (i.e. .choiceEnd) instruction in a subpattern is a call to itself we perform
71-
// a tail call optimisation by jumping directly instead.
101+
// If the last non-dummy (i.e. not .choiceEnd) instruction in a subpattern is a call to itself we
102+
// perform a tail call optimisation by jumping directly instead.
72103
// The very last instruction is a .return, so skip that.
73104
if subpatternRange.upperBound - 2 == i
74105
|| (subpatternRange.upperBound - 3 == i && instructions[i + 1].doesNotDoAnything) {
@@ -78,18 +109,19 @@ public class Grammar: Pattern {
78109
}
79110
}
80111
}
81-
instructions[startIndex + 1] = .jump(offset: instructions.endIndex - startIndex - 1)
82112

83-
finalInstructions = instructions
113+
instructions[startIndex + 1] = .jump(offset: instructions.endIndex - startIndex - 1)
84114
}
85115
}
86116

87117
infix operator <-: AssignmentPrecedence
88118

119+
/// Used by grammars to define subpatterns with `g.a <- ...`.
89120
public func <- <P: Pattern>(call: Grammar.CallPattern, pattern: P) {
90121
call.grammar.patterns.append((call.name, AnyPattern(pattern)))
91122
}
92123

124+
/// In case of `g.name <- Capture(...)`, names the nameless Capture "name".
93125
public func <- <P: Pattern>(call: Grammar.CallPattern, capture: Capture<P>) {
94126
let newPattern = capture.name == nil
95127
? Capture(name: call.name, capture.wrapped)

Sources/Patterns/Operations on Patterns/Skip.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
// Created by Kåre Morstøl on 25/05/2020.
66
//
77

8+
/// Skips 0 or more elements until a match for the next patterns are found.
9+
///
10+
/// If this is at the end of a pattern, it skips to the end of input.
811
public struct Skip: Pattern {
912
public var description: String { "Skip()" }
1013

@@ -109,7 +112,7 @@ extension MutableCollection where Self: RandomAccessCollection, Self: RangeRepla
109112
}
110113
}
111114

112-
/// Inserts new instructions at `location`. Adjusts the offsets of other instructions accordingly.
115+
/// Inserts new instructions at `location`. Adjusts the offsets of the other instructions accordingly.
113116
@usableFromInline
114117
mutating func insertInstructions<Input>(_ newInstructions: Element..., at location: Index)
115118
where Element == Instruction<Input> {

Sources/Patterns/Optimise Instructions.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ private extension Instruction {
2727
import SE0270_RangeSet
2828

2929
extension MutableCollection where Self: RandomAccessCollection, Index == Int {
30+
/// Moves any `.checkIndex, .captureStart, .captureEnd` past any `.elementEquals, .checkElement`.
31+
///
32+
/// Improves performance noticeably.
3033
@usableFromInline
3134
mutating func moveMovablesForward<Input>() where Element == Instruction<Input> {
32-
var movables = ContiguousArray<Index>()[...]
35+
var movables = ContiguousArray<Index>()
3336
for i in indices {
3437
if self[i].isMovable {
3538
movables.append(i)
@@ -52,7 +55,7 @@ extension MutableCollection where Self: RandomAccessCollection, Index == Int {
5255
}
5356
movables.removeAll()
5457

55-
// All `.checkIndex` should be first.
58+
// All `.checkIndex` should be first. If they fail there is no point in capturing anything.
5659
moveSubranges(checkIndexIndices, to: moved.lowerBound)
5760
}
5861
}

Sources/Patterns/Parser.swift

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,47 @@
55
// Created by Kåre Morstøl on 23/10/2018.
66
//
77

8+
/// Takes a pattern, optimises it and tries to match it over an input.
89
public struct Parser<Input: BidirectionalCollection> where Input.Element: Hashable {
9-
public enum InitError: Error, CustomStringConvertible {
10-
case invalid([Pattern])
10+
/// Indicates a problem with a malformed pattern.
11+
public enum PatternError: Error, CustomStringConvertible {
12+
/// The error message from the parser.
1113
case message(String)
1214

1315
public var description: String {
1416
switch self {
15-
case let .invalid(patterns):
16-
return "Invalid series of patterns: \(patterns)"
1717
case let .message(string):
1818
return string
1919
}
2020
}
2121
}
2222

2323
@usableFromInline
24-
let matcher: VMBacktrackEngine<Input>
24+
let matcher: VMEngine<Input>
2525

26+
/// A parser which matches `pattern` _at_ a given position.
2627
@inlinable
2728
public init<P: Pattern>(_ pattern: P) throws where P.Input == Input {
28-
self.matcher = try VMBacktrackEngine(pattern)
29+
self.matcher = try VMEngine(pattern)
2930
}
3031

32+
/// A parser which searches for `pattern` _from_ a given position.
33+
///
34+
/// Is the same as `Parser(Skip() • pattern)`.
3135
@inlinable
3236
public init<P: Pattern>(search pattern: P) throws where P.Input == Input {
3337
try self.init(Skip() pattern)
3438
}
3539

36-
@inlinable
37-
public func ranges(in input: Input, from startindex: Input.Index? = nil)
38-
-> AnySequence<Range<Input.Index>> {
39-
AnySequence(matches(in: input, from: startindex).lazy.map { $0.range })
40-
}
41-
40+
/// Contains information about a patterns successfully completed match.
4241
public struct Match: Equatable {
42+
/// The position in the input when the pattern completed.
43+
///
44+
/// - note: If the last part of the pattern is a `!` or `&&`,
45+
/// `endIndex` is the position when that last part _started_.
4346
public let endIndex: Input.Index
47+
48+
/// The names and ranges of all captures.
4449
public let captures: [(name: String?, range: Range<Input.Index>)]
4550

4651
@inlinable
@@ -57,6 +62,8 @@ public struct Parser<Input: BidirectionalCollection> where Input.Element: Hashab
5762
})
5863
}
5964

65+
/// The range from the beginning of the first capture to the end of the last one.
66+
/// If there are no captures, the empty range at the `endIndex` of this Match.
6067
@inlinable
6168
public var range: Range<Input.Index> {
6269
// TODO: Is `captures.last!.range.upperBound` always the highest captured index?
@@ -74,25 +81,40 @@ public struct Parser<Input: BidirectionalCollection> where Input.Element: Hashab
7481
"""
7582
}
7683

84+
/// Returns the first capture named `name`.
7785
@inlinable
7886
public subscript(one name: String) -> Range<Input.Index>? {
7987
captures.first(where: { $0.name == name })?.range
8088
}
8189

90+
/// Returns all captures named `name`.
8291
@inlinable
8392
public subscript(multiple name: String) -> [Range<Input.Index>] {
8493
captures.filter { $0.name == name }.map { $0.range }
8594
}
8695

96+
/// The names of all the captures.
8797
@inlinable
88-
public var names: Set<String> { Set(captures.compactMap { $0.name }) }
98+
public var captureNames: Set<String> { Set(captures.compactMap { $0.name }) }
8999
}
90100

101+
/// Tries to match the pattern in `input` at `index`.
102+
/// - Parameters:
103+
/// - index: The position to match at, if not provided the beginning of input will be used.
91104
@inlinable
92-
public func match(in input: Input, at startIndex: Input.Index? = nil) -> Match? {
93-
matcher.match(in: input, from: startIndex ?? input.startIndex)
105+
public func match(in input: Input, at index: Input.Index? = nil) -> Match? {
106+
matcher.match(in: input, at: index ?? input.startIndex)
94107
}
95108

109+
/// A lazily generated sequence of consecutive matches of the pattern in `input`.
110+
///
111+
/// Each match attempt starts at the `.range.upperBound` of the previous match,
112+
/// so the matches can be overlapping.
113+
///
114+
/// You can dictate where the next match should start by where you place the last capture.
115+
///
116+
/// - Parameters:
117+
/// - startindex: The position to match from, if not provided the beginning of input will be used.
96118
@inlinable
97119
public func matches(in input: Input, from startindex: Input.Index? = nil)
98120
-> UnfoldSequence<Match, Input.Index> {
@@ -107,15 +129,15 @@ public struct Parser<Input: BidirectionalCollection> where Input.Element: Hashab
107129
match = newMatch
108130
}
109131
lastMatch = match
110-
let range = match.range
111-
if range.upperBound == index {
112-
guard range.upperBound != input.endIndex else {
132+
let matchEnd = match.range.upperBound
133+
if matchEnd == index {
134+
guard matchEnd != input.endIndex else {
113135
stop = true
114136
return match
115137
}
116138
input.formIndex(after: &index)
117139
} else {
118-
index = range.upperBound
140+
index = matchEnd
119141
}
120142
return match
121143
})

Sources/Patterns/Pattern And Instruction.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,15 @@ public enum Instruction<Input: BidirectionalCollection> where Input.Element: Has
7070

7171
/// Will be replaced by .call in preprocessing. Is never executed.
7272
case openCall(name: String)
73-
/// Goes to the sub-expression at `offset` relative to this instruction.
73+
/// Goes to the subpattern at `offset` relative to this instruction.
74+
/// When the subpattern finishes we move on to the instruction after this.
7475
case call(offset: Distance)
75-
/// Returns from this subexpression to where it was called from.
76+
/// Returns from this subpattern to the instruction after where this was called from.
7677
case `return`
7778

7879
/// Signals a failure.
80+
///
81+
/// The snapshot from the previous `.choice` is restored, if there aren't any left we stop matching altogether.
7982
case fail
8083
/// A match has been successfully completed!
8184
///

Sources/Patterns/Regex.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77

88
import Foundation
99

10-
public protocol RegexConvertible {
10+
/// A pattern that can be converted to regex.
11+
public protocol RegexConvertible: Pattern {
12+
/// The equivalent regex for this pattern.
1113
var regex: String { get }
1214
}
1315

16+
// For `OneOf` to be convertible the regex has to be provided manually when it is created.
17+
1418
extension Literal: RegexConvertible {
1519
public var regex: String { NSRegularExpression.escapedPattern(for: String(elements)) }
1620
}

Sources/Patterns/VMBacktrack.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//
77

88
@usableFromInline
9-
struct VMBacktrackEngine<Input: BidirectionalCollection> where Input.Element: Hashable {
9+
struct VMEngine<Input: BidirectionalCollection> where Input.Element: Hashable {
1010
@usableFromInline
1111
typealias Instructions = ContiguousArray<Instruction<Input>>
1212
let instructions: Instructions
@@ -24,14 +24,14 @@ struct VMBacktrackEngine<Input: BidirectionalCollection> where Input.Element: Ha
2424
}
2525

2626
@usableFromInline
27-
func match(in input: Input, from startIndex: Input.Index) -> Parser<Input>.Match? {
27+
func match(in input: Input, at startIndex: Input.Index) -> Parser<Input>.Match? {
2828
launch(input: input, startIndex: startIndex)
2929
}
3030
}
3131

3232
extension Parser.Match {
3333
@usableFromInline
34-
init(_ thread: VMBacktrackEngine<Input>.Thread, instructions: VMBacktrackEngine<Input>.Instructions) {
34+
init(_ thread: VMEngine<Input>.Thread, instructions: VMEngine<Input>.Instructions) {
3535
var captures = [(name: String?, range: Range<Input.Index>)]()
3636
captures.reserveCapacity(thread.captures.count / 2)
3737
var captureBeginnings = [(name: String?, start: Input.Index)]()
@@ -53,7 +53,7 @@ extension Parser.Match {
5353
}
5454
}
5555

56-
extension VMBacktrackEngine {
56+
extension VMEngine {
5757
@usableFromInline
5858
struct Thread {
5959
var instructionIndex: Instructions.Index

Tests/PatternsTests/TestHelpers.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ extension Array where Element: Hashable {
1717
}
1818
}
1919

20+
extension Parser {
21+
func ranges(in input: Input, from startindex: Input.Index? = nil)
22+
-> AnySequence<Range<Input.Index>> {
23+
AnySequence(matches(in: input, from: startindex).lazy.map { $0.range })
24+
}
25+
}
26+
2027
extension XCTestCase {
2128
func assertParseAll(_ parser: Parser<String>, input: String, result: [String],
2229
file: StaticString = #file, line: UInt = #line) {

0 commit comments

Comments
 (0)