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 <hawkinsw@obs.cr>
This commit is contained in:
Will Hawkins
2026-05-04 22:24:28 -04:00
parent 783aac26c7
commit 7a36ca32dd
8 changed files with 227 additions and 17 deletions
+3 -1
View File
@@ -22,7 +22,9 @@ import Darwin
@main @main
struct Cli: ParsableCommand { struct Cli: ParsableCommand {
public func run() throws { public func run() throws {
let formatter = FormatterPlain()
let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "Testing") 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))
} }
} }
+16 -9
View File
@@ -16,8 +16,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
public struct Error: Errorable, Equatable, CustomStringConvertible { public struct Error: Errorable, Equatable, CustomStringConvertible {
public func format() -> String { public func format(_ formatter: any Formattable) -> String {
return self.description return self._msg
} }
public func msg() -> String { public func msg() -> String {
@@ -40,11 +40,10 @@ public struct Error: Errorable, Equatable, CustomStringConvertible {
} }
public struct ErrorWithLocation: Errorable, Equatable, CustomStringConvertible { public struct ErrorWithLocation: Errorable, Equatable, CustomStringConvertible {
let startFormat: String = "\u{1B}[31;1;4m" public func format(_ formatter: any Formattable) -> String {
let endFormat: String = "\u{1B}[0m" let bold_red = Style(StyleColor.Red, [StyleFormat.Bold])
let formatted_location = formatter.formatWithStyle(self.location.description, bold_red)
public func format() -> String { return formatted_location + ": " + self._msg
return startFormat + "\(self.location)" + endFormat + ": \(self._msg)"
} }
public func msg() -> String { public func msg() -> String {
@@ -70,8 +69,10 @@ public struct ErrorWithLocation: Errorable, Equatable, CustomStringConvertible {
} }
public struct Errors: Errorable, CustomStringConvertible { public struct Errors: Errorable, CustomStringConvertible {
public func format() -> String { public func format(_ formatter: any Formattable) -> String {
return self.description self.errors.map() { error in
error.format(formatter)
}.joined(separator: "\n")
} }
public func msg() -> String { public func msg() -> String {
@@ -103,6 +104,12 @@ public struct ErrorWithLabel: Errorable {
let label: String let label: String
let error: any Errorable 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) { public init(_ label: String, _ error: any Errorable) {
self.label = label self.label = label
self.error = error self.error = error
+116
View File
@@ -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 <https://www.gnu.org/licenses/>.
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
}
}
+5 -1
View File
@@ -71,7 +71,7 @@ public protocol ProgramExecutionEvaluator {
} }
public protocol Errorable: CustomStringConvertible { public protocol Errorable: CustomStringConvertible {
func format() -> String func format(_ formatter: Formattable) -> String
func msg() -> String func msg() -> String
func append(error: any Errorable) -> any Errorable func append(error: any Errorable) -> any Errorable
func eq(_ rhs: any Errorable) -> Bool func eq(_ rhs: any Errorable) -> Bool
@@ -83,6 +83,10 @@ extension Errorable {
} }
} }
public protocol Formattable {
func formatWithStyle(_ value: String, _ style: Style) -> String
}
extension ProgramExecutionEvaluator { extension ProgramExecutionEvaluator {
public func ExecuteStatements( public func ExecuteStatements(
_ statements: [EvaluatableStatement], inExecution execution: ProgramExecution _ statements: [EvaluatableStatement], inExecution execution: ProgramExecution
+1 -1
View File
@@ -62,7 +62,7 @@ extension Result: CustomStringConvertible where OKT: CustomStringConvertible {
public var description: String { public var description: String {
switch self { switch self {
case Result.Error(let e): case Result.Error(let e):
return e.format() return e.msg()
case Result.Ok(let o): case Result.Ok(let o):
return "Ok: \(o)" return "Ok: \(o)"
} }
+27 -3
View File
@@ -18,8 +18,8 @@
import Common import Common
import Foundation import Foundation
import Macros import Macros
import P4Runtime
import P4Lang import P4Lang
import P4Runtime
import SwiftTreeSitter import SwiftTreeSitter
import Testing import Testing
import TreeSitter import TreeSitter
@@ -28,7 +28,31 @@ import TreeSitterP4
@testable import P4Compiler @testable import P4Compiler
@Test func test_error_with_location_formatting() async throws { @Test func test_error_with_location_formatting() async throws {
let formatter = FormatterAnsi()
let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "There was an error") let e = ErrorWithLocation(sourceLocation: SourceLocation(1, 5), withError: "There was an error")
let formatted = e.format(formatter)
print(e.format()) #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"
)
} }
+57
View File
@@ -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 <https://www.gnu.org/licenses/>.
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]))
}
+1 -1
View File
@@ -42,7 +42,7 @@ import TreeSitterP4
guard case Result.Error(let e) = err else { guard case Result.Error(let e) = err else {
assert(false, "Expected an error, but had success") 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)"))
} }
} }