Skip to content

Commit fcbd10c

Browse files
committed
Introduce Diagnostics
1 parent cc3d228 commit fcbd10c

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
@@ -239,9 +239,128 @@ extension ParseError: CustomStringConvertible {
239239
}
240240
}
241241

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

244-
// TODO: Diagnostics engine, recorder, logger, or similar.
295+
/// A collection of diagnostics to emit.
296+
public struct Diagnostics: Hashable {
297+
public private(set) var diags = [Diagnostic]()
245298

299+
public init() {}
300+
public init(_ diags: [Diagnostic]) {
301+
self.diags = diags
302+
}
246303

304+
/// Add a new diagnostic to emit.
305+
public mutating func append(_ diag: Diagnostic) {
306+
diags.append(diag)
307+
}
247308

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

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)