From 7a36ca32dd322426b1378591fdbf220671e56176 Mon Sep 17 00:00:00 2001 From: Will Hawkins Date: Mon, 4 May 2026 22:24:28 -0400 Subject: [PATCH] compiler, runtime: Support Formatted Error Messages Make it possible to output formatted error messages using a flexible API that includes an ability to specify styles and formatters. Signed-off-by: Will Hawkins --- Sources/Cli/main.swift | 4 +- Sources/Common/Error.swift | 25 ++-- Sources/Common/Formatter.swift | 116 +++++++++++++++++++ Sources/Common/Protocols.swift | 6 +- Sources/Common/Result.swift | 2 +- Tests/p4rseTests/ErrorTests/Formatting.swift | 32 ++++- Tests/p4rseTests/StyleTests/Style.swift | 57 +++++++++ Tests/p4rseTests/ValueTypeParserTests.swift | 2 +- 8 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 Sources/Common/Formatter.swift create mode 100644 Tests/p4rseTests/StyleTests/Style.swift diff --git a/Sources/Cli/main.swift b/Sources/Cli/main.swift index e74bdc7..60c6cf1 100644 --- a/Sources/Cli/main.swift +++ b/Sources/Cli/main.swift @@ -22,7 +22,9 @@ import Darwin @main struct Cli: ParsableCommand { public func run() throws { + let formatter = FormatterPlain() let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "Testing") - print(e.format()) + let e1 = ErrorWithLocation(sourceLocation: SourceLocation(10, 5), withError: "Oh no") + print(e.append(error: e1).format(formatter)) } } diff --git a/Sources/Common/Error.swift b/Sources/Common/Error.swift index 00bed18..db5fe0f 100644 --- a/Sources/Common/Error.swift +++ b/Sources/Common/Error.swift @@ -16,8 +16,8 @@ // along with this program. If not, see . public struct Error: Errorable, Equatable, CustomStringConvertible { - public func format() -> String { - return self.description + public func format(_ formatter: any Formattable) -> String { + return self._msg } public func msg() -> String { @@ -40,11 +40,10 @@ public struct Error: Errorable, Equatable, CustomStringConvertible { } public struct ErrorWithLocation: Errorable, Equatable, CustomStringConvertible { - let startFormat: String = "\u{1B}[31;1;4m" - let endFormat: String = "\u{1B}[0m" - - public func format() -> String { - return startFormat + "\(self.location)" + endFormat + ": \(self._msg)" + public func format(_ formatter: any Formattable) -> String { + let bold_red = Style(StyleColor.Red, [StyleFormat.Bold]) + let formatted_location = formatter.formatWithStyle(self.location.description, bold_red) + return formatted_location + ": " + self._msg } public func msg() -> String { @@ -70,8 +69,10 @@ public struct ErrorWithLocation: Errorable, Equatable, CustomStringConvertible { } public struct Errors: Errorable, CustomStringConvertible { - public func format() -> String { - return self.description + public func format(_ formatter: any Formattable) -> String { + self.errors.map() { error in + error.format(formatter) + }.joined(separator: "\n") } public func msg() -> String { @@ -103,6 +104,12 @@ public struct ErrorWithLabel: Errorable { let label: String let error: any Errorable + public func format(_ formatter: any Formattable) -> String { + let green = Style(StyleColor.Green) + let formatted_label = formatter.formatWithStyle(self.label, green) + return formatted_label + self.error.format(formatter) + } + public init(_ label: String, _ error: any Errorable) { self.label = label self.error = error diff --git a/Sources/Common/Formatter.swift b/Sources/Common/Formatter.swift new file mode 100644 index 0000000..ba3fecf --- /dev/null +++ b/Sources/Common/Formatter.swift @@ -0,0 +1,116 @@ +// p4rse, Copyright 2026, Will Hawkins +// +// This file is part of p4rse. +// +// This file is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +public enum StyleColor { + case Red + case Blue + case Green +} + +public enum StyleFormat { + case Underline + case Bold +} + +public struct Style: Equatable { + let color: StyleColor? + let format: [StyleFormat] + + public init(_ color: StyleColor?, _ format: [StyleFormat] = []) { + self.color = color + self.format = format + } + + public func update(setColor color: StyleColor) -> Style { + return Style(color, self.format) + } + + public func removeColor() -> Style { + return Style(nil, self.format) + } + + public func update(addFormat format: StyleFormat) -> Style { + return if self.format.contains(format) { + Style(self.color, self.format) + } else { + Style(self.color, self.format + [format]) + } + } + + public func update(removeFormat format: StyleFormat) -> Style { + let new_format = self.format.filter { existing_format in + existing_format != format + } + return Style(self.color, new_format) + } + + public func getColor() -> StyleColor? { + return self.color + } + + public func getFormat() -> [StyleFormat] { + return self.format + } +} + +public struct FormatterPlain: Formattable { + public init() {} + public func formatWithStyle(_ value: String, _ style: Style) -> String { + return value + } + +} + +public struct FormatterAnsi: Formattable { + + public init() {} + + let startFormat: String = "\u{1B}[" + let resetFormat: String = "\u{1B}[0m" + + let colorMap = [ + StyleColor.Red: "31", + StyleColor.Green: "32", + StyleColor.Blue: "34", + ] + + let styleMap = [ + StyleFormat.Underline: "4", + StyleFormat.Bold: "1", + ] + + public func formatWithStyle(_ value: String, _ style: Style) -> String { + let color = + if let color = style.getColor() { + self.colorMap[color]! + } else { + "" + } + + let style = style.getFormat().map { format in + String(self.styleMap[format]!) + }.joined(separator: ";") + + if color.isEmpty && style.isEmpty { + return value + } + + let code = startFormat + color + ((!color.isEmpty && !style.isEmpty) ? ";" : "") + style + "m" + + return code + value + resetFormat + } +} diff --git a/Sources/Common/Protocols.swift b/Sources/Common/Protocols.swift index e3fdbef..c6a3db1 100644 --- a/Sources/Common/Protocols.swift +++ b/Sources/Common/Protocols.swift @@ -71,7 +71,7 @@ public protocol ProgramExecutionEvaluator { } public protocol Errorable: CustomStringConvertible { - func format() -> String + func format(_ formatter: Formattable) -> String func msg() -> String func append(error: any Errorable) -> any Errorable func eq(_ rhs: any Errorable) -> Bool @@ -83,6 +83,10 @@ extension Errorable { } } +public protocol Formattable { + func formatWithStyle(_ value: String, _ style: Style) -> String +} + extension ProgramExecutionEvaluator { public func ExecuteStatements( _ statements: [EvaluatableStatement], inExecution execution: ProgramExecution diff --git a/Sources/Common/Result.swift b/Sources/Common/Result.swift index 72e4307..a2f85db 100644 --- a/Sources/Common/Result.swift +++ b/Sources/Common/Result.swift @@ -62,7 +62,7 @@ extension Result: CustomStringConvertible where OKT: CustomStringConvertible { public var description: String { switch self { case Result.Error(let e): - return e.format() + return e.msg() case Result.Ok(let o): return "Ok: \(o)" } diff --git a/Tests/p4rseTests/ErrorTests/Formatting.swift b/Tests/p4rseTests/ErrorTests/Formatting.swift index d6c5aff..000fde1 100644 --- a/Tests/p4rseTests/ErrorTests/Formatting.swift +++ b/Tests/p4rseTests/ErrorTests/Formatting.swift @@ -18,8 +18,8 @@ import Common import Foundation import Macros -import P4Runtime import P4Lang +import P4Runtime import SwiftTreeSitter import Testing import TreeSitter @@ -28,7 +28,31 @@ import TreeSitterP4 @testable import P4Compiler @Test func test_error_with_location_formatting() async throws { - let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "There was an error") - - print(e.format()) + let formatter = FormatterAnsi() + let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "There was an error") + let formatted = e.format(formatter) + #expect(formatted == "\u{1B}[31;1m{1, 5}\u{1B}[0m: There was an error") +} + +@Test func test_errors_with_location_no_formatting() async throws { + let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "There was an error") + let e1 = ErrorWithLocation( + sourceLocation: SourceLocation(10, 5), withError: "There was another error") + + let formatted = e.append(error: e1).format(FormatterPlain()) + + #expect(formatted == "{1, 5}: There was an error\n{10, 5}: There was another error") +} + +@Test func test_errors_with_location_ansi_formatting() async throws { + let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "There was an error") + let e1 = ErrorWithLocation( + sourceLocation: SourceLocation(10, 5), withError: "There was another error") + + let formatted = e.append(error: e1).format(FormatterAnsi()) + + #expect( + formatted + == "\u{1B}[31;1m{1, 5}\u{1B}[0m: There was an error\n\u{1B}[31;1m{10, 5}\u{1B}[0m: There was another error" + ) } diff --git a/Tests/p4rseTests/StyleTests/Style.swift b/Tests/p4rseTests/StyleTests/Style.swift new file mode 100644 index 0000000..3afeead --- /dev/null +++ b/Tests/p4rseTests/StyleTests/Style.swift @@ -0,0 +1,57 @@ +// p4rse, Copyright 2026, Will Hawkins +// +// This file is part of p4rse. +// +// This file is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import Common +import Foundation +import Macros +import P4Runtime +import P4Lang +import SwiftTreeSitter +import Testing +import TreeSitter +import TreeSitterP4 + +@testable import P4Compiler + +@Test func test_style_add_format() async throws { + let red = Style(StyleColor.Red) + let bold_red = red.update(addFormat: StyleFormat.Bold) + + #expect(bold_red == Style(StyleColor.Red, [StyleFormat.Bold])) +} + +@Test func test_style_add_format2() async throws { + let bold_red = Style(StyleColor.Red, [StyleFormat.Bold]) + let bold_underline_red = bold_red.update(addFormat: StyleFormat.Underline) + + #expect(bold_underline_red == Style(StyleColor.Red, [StyleFormat.Bold, StyleFormat.Underline])) +} + + +@Test func test_style_remove_format() async throws { + let bold_red = Style(StyleColor.Red, [StyleFormat.Bold]) + let red = bold_red.update(removeFormat: StyleFormat.Bold) + + #expect(red == Style(StyleColor.Red)) +} + +@Test func test_style_remove_format2() async throws { + let bold_underline_red = Style(StyleColor.Red, [StyleFormat.Bold, StyleFormat.Underline]) + let underline_red = bold_underline_red.update(removeFormat: StyleFormat.Bold) + + #expect(underline_red == Style(StyleColor.Red, [StyleFormat.Underline])) +} diff --git a/Tests/p4rseTests/ValueTypeParserTests.swift b/Tests/p4rseTests/ValueTypeParserTests.swift index b0b892e..8f5ee8a 100644 --- a/Tests/p4rseTests/ValueTypeParserTests.swift +++ b/Tests/p4rseTests/ValueTypeParserTests.swift @@ -42,7 +42,7 @@ import TreeSitterP4 guard case Result.Error(let e) = err else { assert(false, "Expected an error, but had success") } - #expect(e.format().contains("Failed to parse a statement element: Could not parse a P4 type from \(invalid_type_name)")) + #expect(e.msg().contains("Failed to parse a statement element: Could not parse a P4 type from \(invalid_type_name)")) } }