Skip to content

Commit 6f4de11

Browse files
committed
Introduce Diagnostics
1 parent c5e4a97 commit 6f4de11

File tree

9 files changed

+147
-20
lines changed

9 files changed

+147
-20
lines changed

Sources/_RegexParser/Regex/AST/AST.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,31 @@
1515
public struct AST: Hashable {
1616
public var root: AST.Node
1717
public var globalOptions: GlobalMatchingOptionSequence?
18+
public var diags: Diagnostics
1819

19-
public init(_ root: AST.Node, globalOptions: GlobalMatchingOptionSequence?) {
20+
public init(
21+
_ root: AST.Node, globalOptions: GlobalMatchingOptionSequence?,
22+
diags: Diagnostics
23+
) {
2024
self.root = root
2125
self.globalOptions = globalOptions
26+
self.diags = diags
2227
}
2328
}
2429

2530
extension AST {
2631
/// Whether this AST tree contains at least one capture nested inside of it.
2732
public var hasCapture: Bool { root.hasCapture }
33+
34+
/// Whether this AST tree is either syntactically or semantically invalid.
35+
public var isInvalid: Bool { diags.hasAnyError }
36+
37+
/// If the AST is invalid, throws an error. Otherwise, returns self.
38+
@discardableResult
39+
public func ensureValid() throws -> AST {
40+
try diags.throwAnyError()
41+
return self
42+
}
2843
}
2944

3045
extension AST {

Sources/_RegexParser/Regex/Parse/Diagnostics.swift

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,128 @@ extension ParseError: CustomStringConvertible {
229229
}
230230
}
231231

232-
// TODO: Fixits, notes, etc.
232+
/// A fatal error that indicates broken logic in the parser.
233+
enum FatalParseError: Hashable, Error {
234+
case unreachable(String)
235+
}
236+
237+
extension FatalParseError: CustomStringConvertible {
238+
var description: String {
239+
switch self {
240+
case .unreachable(let str):
241+
return "UNREACHABLE: \(str)"
242+
}
243+
}
244+
}
245+
246+
// MARK: Diagnostic handling
247+
248+
/// A diagnostic to emit.
249+
public struct Diagnostic: Hashable {
250+
public let behavior: Behavior
251+
public let message: String
252+
public let location: SourceLocation
253+
254+
// TODO: Fixits, notes, etc.
255+
256+
// The underlying ParseError if applicable. This is used for testing.
257+
internal let underlyingParseError: ParseError?
258+
259+
init(_ behavior: Behavior, _ message: String, at loc: SourceLocation,
260+
underlyingParseError: ParseError? = nil) {
261+
self.behavior = behavior
262+
self.message = message
263+
self.location = loc
264+
self.underlyingParseError = underlyingParseError
265+
}
266+
267+
public var isAnyError: Bool { behavior.isAnyError }
268+
}
269+
270+
extension Diagnostic {
271+
public enum Behavior: Hashable {
272+
case fatalError, error, warning
273+
274+
public var isAnyError: Bool {
275+
switch self {
276+
case .fatalError, .error:
277+
return true
278+
case .warning:
279+
return false
280+
}
281+
}
282+
}
283+
}
233284

234-
// TODO: Diagnostics engine, recorder, logger, or similar.
285+
/// A collection of diagnostics to emit.
286+
public struct Diagnostics: Hashable {
287+
public private(set) var diags = [Diagnostic]()
235288

289+
public init() {}
290+
public init(_ diags: [Diagnostic]) {
291+
self.diags = diags
292+
}
236293

294+
/// Add a new diagnostic to emit.
295+
public mutating func append(_ diag: Diagnostic) {
296+
diags.append(diag)
297+
}
237298

299+
/// Add all the diagnostics of another diagnostic collection.
300+
public mutating func append(contentsOf other: Diagnostics) {
301+
diags.append(contentsOf: other.diags)
302+
}
303+
304+
/// Add all the new fatal error diagnostics of another diagnostic collection.
305+
/// This assumes that `other` was the same as `self`, but may have additional
306+
/// diagnostics added to it.
307+
public mutating func appendNewFatalErrors(from other: Diagnostics) {
308+
let newDiags = other.diags.dropFirst(diags.count)
309+
for diag in newDiags where diag.behavior == .fatalError {
310+
append(diag)
311+
}
312+
}
313+
314+
/// Whether any error is present. This includes fatal errors.
315+
public var hasAnyError: Bool {
316+
diags.contains(where: { $0.isAnyError })
317+
}
318+
319+
/// Whether any fatal error is present.
320+
public var hasFatalError: Bool {
321+
diags.contains(where: { $0.behavior == .fatalError })
322+
}
323+
324+
/// If any error diagnostic has been added, throw it as an Error.
325+
func throwAnyError() throws {
326+
for diag in diags where diag.isAnyError {
327+
struct ErrorDiagnostic: Error, CustomStringConvertible {
328+
var diag: Diagnostic
329+
var description: String { diag.message }
330+
}
331+
throw ErrorDiagnostic(diag: diag)
332+
}
333+
}
334+
}
335+
336+
// MARK: Diagnostic construction
337+
338+
extension Diagnostic {
339+
init(_ err: ParseError, at loc: SourceLocation) {
340+
self.init(.error, "\(err)", at: loc, underlyingParseError: err)
341+
}
342+
343+
init(_ err: FatalParseError, at loc: SourceLocation) {
344+
self.init(.fatalError, "\(err)", at: loc)
345+
}
346+
}
347+
348+
extension Diagnostics {
349+
mutating func error(_ err: ParseError, at loc: SourceLocation) {
350+
append(Diagnostic(err, at: loc))
351+
}
352+
353+
mutating func fatal(_ err: FatalParseError, at loc: SourceLocation) {
354+
append(Diagnostic(err, at: loc))
355+
}
356+
}

Sources/_RegexParser/Regex/Parse/Parse.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ extension Parser {
165165
}
166166
fatalError("Unhandled termination condition")
167167
}
168-
return .init(ast, globalOptions: opts)
168+
// TODO: Record and store diagnostics on the AST.
169+
return .init(ast, globalOptions: opts, diags: Diagnostics())
169170
}
170171

171172
/// Parse a regular expression node. This should be used instead of `parse()`

Sources/_RegexParser/Regex/Printing/PrintAsCanonical.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension AST.Node {
3232
showDelimiters delimiters: Bool = false,
3333
terminateLine: Bool = false
3434
) -> String {
35-
AST(self, globalOptions: nil).renderAsCanonical(
35+
AST(self, globalOptions: nil, diags: Diagnostics()).renderAsCanonical(
3636
showDelimiters: delimiters, terminateLine: terminateLine)
3737
}
3838
}

Sources/_StringProcessing/PrintAsPattern.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extension PrettyPrinter {
5656
mutating func printBackoff(_ node: DSLTree.Node) {
5757
precondition(node.astNode != nil, "unconverted node")
5858
printAsCanonical(
59-
.init(node.astNode!, globalOptions: nil),
59+
.init(node.astNode!, globalOptions: nil, diags: Diagnostics()),
6060
delimiters: true)
6161
}
6262

Sources/_StringProcessing/Regex/Core.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public struct Regex<Output>: RegexComponent {
3737
self.program = Program(ast: ast)
3838
}
3939
init(ast: AST.Node) {
40-
self.program = Program(ast: .init(ast, globalOptions: nil))
40+
self.program = Program(ast:
41+
.init(ast, globalOptions: nil, diags: Diagnostics()))
4142
}
4243

4344
// Compiler interface. Do not change independently.

Sources/_StringProcessing/Regex/DSLTree.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -306,16 +306,6 @@ extension DSLTree {
306306
}
307307
}
308308

309-
extension DSLTree {
310-
var ast: AST? {
311-
guard let root = root.astNode else {
312-
return nil
313-
}
314-
// TODO: Options mapping
315-
return AST(root, globalOptions: nil)
316-
}
317-
}
318-
319309
extension DSLTree {
320310
var hasCapture: Bool {
321311
root.hasCapture

Sources/_StringProcessing/Utility/ASTBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func empty() -> AST.Node {
4848
}
4949

5050
func ast(_ root: AST.Node, opts: [AST.GlobalMatchingOption.Kind]) -> AST {
51-
.init(root, globalOptions: .init(opts.map { .init($0, .fake) }))
51+
.init(root, globalOptions: .init(opts.map { .init($0, .fake) }), diags: Diagnostics())
5252
}
5353

5454
func ast(_ root: AST.Node, opts: AST.GlobalMatchingOption.Kind...) -> AST {

Tests/RegexTests/ParseTests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ func parseTest(
4848
line: UInt = #line
4949
) {
5050
parseTest(
51-
input, .init(expectedAST, globalOptions: nil), throwsError: errorKind,
52-
syntax: syntax, captures: expectedCaptures, file: file, line: line
51+
input, .init(expectedAST, globalOptions: nil, diags: Diagnostics()),
52+
throwsError: errorKind, syntax: syntax, captures: expectedCaptures,
53+
file: file, line: line
5354
)
5455
}
5556

0 commit comments

Comments
 (0)