From e53c32f8029c4fd502d3b4304902eb45bcc54fa3 Mon Sep 17 00:00:00 2001 From: Will Hawkins Date: Fri, 22 May 2026 02:37:33 -0400 Subject: [PATCH] compiler, cli: Support Nice Compilation Error Messages Signed-off-by: Will Hawkins --- Sources/Cli/main.swift | 3 +- Sources/Common/Error.swift | 22 +++++++++++++ Sources/Common/Protocols.swift | 4 +++ Sources/Common/SourceCode.swift | 27 +++++++++++++++ Sources/P4Compiler/Feedback.swift | 33 +++++++++++++++++++ .../Sources/action-parameters-wrong-order.p4 | 13 ++++++++ TestData/simple_cli_compilation_error.golden | 3 ++ Tests/p4rseTests/CliTests/Cli.swift | 5 +++ Tests/p4rseTests/ControlTests/Compile.swift | 9 +++-- 9 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 Sources/P4Compiler/Feedback.swift create mode 100644 TestData/Sources/action-parameters-wrong-order.p4 create mode 100644 TestData/simple_cli_compilation_error.golden diff --git a/Sources/Cli/main.swift b/Sources/Cli/main.swift index 2e8509d..ddd100e 100644 --- a/Sources/Cli/main.swift +++ b/Sources/Cli/main.swift @@ -96,7 +96,8 @@ extension Cli { let maybe_program = Program.Compile(source.getSource()) guard case .Ok(_) = maybe_program else { - print(ErrorWithLabel("Compiler Error", maybe_source.error()!).format(formatter)) + let feedback = CompilationFeedback(source, [maybe_program.error()!], formatter) + print(feedback.feedback) return } diff --git a/Sources/Common/Error.swift b/Sources/Common/Error.swift index ab17712..92c23c8 100644 --- a/Sources/Common/Error.swift +++ b/Sources/Common/Error.swift @@ -39,7 +39,29 @@ public struct Error: Errorable, Equatable, CustomStringConvertible { } } +extension String { + public func trimmingPostfix(_ in_: String.Element) -> String { + return String(self.reversed().drop { $0 == in_ }.reversed()) + } +} + public struct ErrorWithLocation: Errorable, Equatable, CustomStringConvertible { + + public func format(_ formatter: any Formattable, _ sc: SourceCode) -> String { + guard let (fp, source, prior, after) = sc.getSourceSnippet(location: self.location, context: 5) + else { + return self.format(formatter) + } + + let prior_snipped = prior.trimmingPrefix(["\n"]) + let after_snipped = after.prefix { $0 != "\n" } + return formatter.formatWithStyle("Error: ", Style(StyleColor.Red)) + + "In \(fp), there was an error: \n..." + prior_snipped + + formatter.formatWithStyle(source, Style(.none, [StyleFormat.Underline])) + after_snipped + + "...\n" + + 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) diff --git a/Sources/Common/Protocols.swift b/Sources/Common/Protocols.swift index c6a3db1..9c25bf6 100644 --- a/Sources/Common/Protocols.swift +++ b/Sources/Common/Protocols.swift @@ -72,6 +72,7 @@ public protocol ProgramExecutionEvaluator { public protocol Errorable: CustomStringConvertible { func format(_ formatter: Formattable) -> String + func format(_ formatter: Formattable, _ sc: SourceCode) -> String func msg() -> String func append(error: any Errorable) -> any Errorable func eq(_ rhs: any Errorable) -> Bool @@ -81,6 +82,9 @@ extension Errorable { public func eq(_ rhs: any Errorable) -> Bool { return self.msg() == rhs.msg() } + public func format(_ formatter: Formattable, _ sc: SourceCode) -> String { + return self.format(formatter) + } } public protocol Formattable { diff --git a/Sources/Common/SourceCode.swift b/Sources/Common/SourceCode.swift index 5a79f97..ee46294 100644 --- a/Sources/Common/SourceCode.swift +++ b/Sources/Common/SourceCode.swift @@ -210,6 +210,33 @@ public struct SourceCode { return self.code } + public func getSourceSnippet( + location: SourceLocation, context: Int = 0 + ) -> (FilePath, String, String, String)? { + guard let path = self.pathForLocation(location.range.lowerBound) else { + return .none + } + let lower = String.UTF8View.Index(utf16Offset: location.range.lowerBound, in: self.code) + let upper = String.UTF8View.Index(utf16Offset: location.range.upperBound, in: self.code) + let prior_start = + if location.range.lowerBound - context >= 0 { + String.UTF8View.Index(utf16Offset: location.range.lowerBound - context, in: self.code) + } else { + String.UTF8View.Index(utf16Offset: location.range.lowerBound, in: self.code) + } + let after_end = + if location.range.upperBound + context < self.code.count { + String.UTF8View.Index(utf16Offset: location.range.upperBound + context, in: self.code) + } else { + String.UTF8View.Index(utf16Offset: location.range.upperBound, in: self.code) + } + + let result = String(self.code.utf16[lower.. FileSourceLocation { return self.locations } diff --git a/Sources/P4Compiler/Feedback.swift b/Sources/P4Compiler/Feedback.swift new file mode 100644 index 0000000..7d245e3 --- /dev/null +++ b/Sources/P4Compiler/Feedback.swift @@ -0,0 +1,33 @@ +// 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 + +public struct CompilationFeedback { + public let feedback: String + + static func markup( + _ source: SourceCode, _ errors: [any Errorable], _ formatter: any Formattable + ) -> String { + errors.map { $0.format(formatter, source) }.joined(separator: "\n") + } + + public init(_ source: SourceCode, _ errors: [any Errorable], _ formatter: any Formattable) { + self.feedback = Self.markup(source, errors, formatter) + } + +} diff --git a/TestData/Sources/action-parameters-wrong-order.p4 b/TestData/Sources/action-parameters-wrong-order.p4 new file mode 100644 index 0000000..48931bb --- /dev/null +++ b/TestData/Sources/action-parameters-wrong-order.p4 @@ -0,0 +1,13 @@ + control simple(int x, int y) { + action a(inout bool aa, int ax, inout bool ay) { + aa = false; + } + table t { + key = { + x: exact; + y: exact; + } + } + apply { + } + }; diff --git a/TestData/simple_cli_compilation_error.golden b/TestData/simple_cli_compilation_error.golden new file mode 100644 index 0000000..c847013 --- /dev/null +++ b/TestData/simple_cli_compilation_error.golden @@ -0,0 +1,3 @@ +Error: In /Users/hawkinsw/code/p4ce/TestData/Sources/action-parameters-wrong-order.p4, there was an error: +...ion a(inout bool aa, int ax, inout bool ay) {... +All parameters with direction must precede directionless parameters diff --git a/Tests/p4rseTests/CliTests/Cli.swift b/Tests/p4rseTests/CliTests/Cli.swift index 1c57fbc..fdc878c 100644 --- a/Tests/p4rseTests/CliTests/Cli.swift +++ b/Tests/p4rseTests/CliTests/Cli.swift @@ -38,4 +38,9 @@ func simple_cli_preprocessor_test() -> [String] { @CliTest() func simple_cli_preprocessor_test_file_not_found() -> [String] { return ["p4ce", "--plain", "preprocess", "simple.p", "-I", "TestData/Sources/"] +} + +@CliTest() +func simple_cli_compilation_error() -> [String] { + return ["p4ce", "--plain", "compile", "action-parameters-wrong-order.p4", "-I", "TestData/Sources/"] } \ No newline at end of file diff --git a/Tests/p4rseTests/ControlTests/Compile.swift b/Tests/p4rseTests/ControlTests/Compile.swift index 3c29f03..cfd206a 100644 --- a/Tests/p4rseTests/ControlTests/Compile.swift +++ b/Tests/p4rseTests/ControlTests/Compile.swift @@ -136,7 +136,8 @@ import TreeSitterP4 #expect( #RequireErrorResult( Error( - withMessage: "{54, 63}: Error(s) parsing property list: {91, 26}: Error(s) parsing table actions: Cannot find b in lexical scope." + withMessage: + "{54, 63}: Error(s) parsing property list: {91, 26}: Error(s) parsing table actions: Cannot find b in lexical scope." ), Program.Compile(simple_parser_declaration)) ) @@ -163,7 +164,8 @@ import TreeSitterP4 #expect( #RequireErrorResult( Error( - withMessage: "{54, 72}: Error(s) parsing property list: {91, 35}: Error(s) parsing table actions: Cannot find b in lexical scope." + withMessage: + "{54, 72}: Error(s) parsing property list: {91, 35}: Error(s) parsing table actions: Cannot find b in lexical scope." ), Program.Compile(simple_parser_declaration)) ) @@ -190,7 +192,8 @@ import TreeSitterP4 #expect( #RequireErrorResult( Error( - withMessage: "{64, 63}: Error(s) parsing property list: {101, 26}: Error(s) parsing table actions: {101, 26}: a does not name an action" + withMessage: + "{64, 63}: Error(s) parsing property list: {101, 26}: Error(s) parsing table actions: {101, 26}: a does not name an action" ), Program.Compile(simple_parser_declaration)) )