Skip to content

Commit 9c27da8

Browse files
committed
Introduce Diagnostics
1 parent 6997fcb commit 9c27da8

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
@@ -242,9 +242,128 @@ extension ParseError: CustomStringConvertible {
242242
}
243243
}
244244

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

247-
// TODO: Diagnostics engine, recorder, logger, or similar.
298+
/// A collection of diagnostics to emit.
299+
public struct Diagnostics: Hashable {
300+
public private(set) var diags = [Diagnostic]()
248301

302+
public init() {}
303+
public init(_ diags: [Diagnostic]) {
304+
self.diags = diags
305+
}
249306

307+
/// Add a new diagnostic to emit.
308+
public mutating func append(_ diag: Diagnostic) {
309+
diags.append(diag)
310+
}
250311

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

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)