Compare commits

...

4 Commits

Author SHA1 Message Date
Will Hawkins 0c8b9e88cf testing: Debug Format CI Test Failure
Continuous Integration / Grammar Tests (push) Successful in 37s
Continuous Integration / Library Format Tests (push) Failing after 1m37s
Continuous Integration / Library Tests (push) Failing after 4m15s
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-22 02:39:12 -04:00
Will Hawkins e53c32f802 compiler, cli: Support Nice Compilation Error Messages
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-22 02:38:47 -04:00
Will Hawkins 5845cb75cc testing: Test For Controls Using Keys From Structs
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-22 02:35:34 -04:00
Will Hawkins 24b0f0284a language: Check For Incorrect Order For Action Parameters
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-22 02:34:51 -04:00
12 changed files with 346 additions and 17 deletions
+2 -1
View File
@@ -96,7 +96,8 @@ extension Cli {
let maybe_program = Program.Compile(source.getSource()) let maybe_program = Program.Compile(source.getSource())
guard case .Ok(_) = maybe_program else { 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 return
} }
+22
View File
@@ -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 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 { public func format(_ formatter: any Formattable) -> String {
let bold_red = Style(StyleColor.Red, [StyleFormat.Bold]) let bold_red = Style(StyleColor.Red, [StyleFormat.Bold])
let formatted_location = formatter.formatWithStyle(self.location.description, bold_red) let formatted_location = formatter.formatWithStyle(self.location.description, bold_red)
+4
View File
@@ -72,6 +72,7 @@ public protocol ProgramExecutionEvaluator {
public protocol Errorable: CustomStringConvertible { public protocol Errorable: CustomStringConvertible {
func format(_ formatter: Formattable) -> String func format(_ formatter: Formattable) -> String
func format(_ formatter: Formattable, _ sc: SourceCode) -> 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
@@ -81,6 +82,9 @@ extension Errorable {
public func eq(_ rhs: any Errorable) -> Bool { public func eq(_ rhs: any Errorable) -> Bool {
return self.msg() == rhs.msg() return self.msg() == rhs.msg()
} }
public func format(_ formatter: Formattable, _ sc: SourceCode) -> String {
return self.format(formatter)
}
} }
public protocol Formattable { public protocol Formattable {
+27
View File
@@ -210,6 +210,33 @@ public struct SourceCode {
return self.code 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..<upper])!
let prior = String(self.code.utf16[prior_start..<lower])!
let after = String(self.code.utf16[upper...after_end])!
return (path, result, prior, after)
}
public func getLocations() -> FileSourceLocation { public func getLocations() -> FileSourceLocation {
return self.locations return self.locations
} }
+11
View File
@@ -614,6 +614,17 @@ extension Action: Compilable {
} }
current_context = updated_context current_context = updated_context
// Check whether the parameters are in the proper order.
let remaining_parameters = action_parameters.parameters.drop(while: {
$0.type.direction() != .none
})
if remaining_parameters.contains(where: { $0.type.direction() != .none }) {
return .Error(
ErrorWithLocation(
sourceLocation: current_node!.toSourceLocation(),
withError: "All parameters with direction must precede directionless parameters"))
}
walker.next() walker.next()
#MustOr( #MustOr(
result: current_node, thing: walker.getNext(), result: current_node, thing: walker.getNext(),
+33
View File
@@ -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 <https://www.gnu.org/licenses/>.
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)
}
}
@@ -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 {
}
};
@@ -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
+5
View File
@@ -39,3 +39,8 @@ func simple_cli_preprocessor_test() -> [String] {
func simple_cli_preprocessor_test_file_not_found() -> [String] { func simple_cli_preprocessor_test_file_not_found() -> [String] {
return ["p4ce", "--plain", "preprocess", "simple.p", "-I", "TestData/Sources/"] 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/"]
}
+84 -4
View File
@@ -18,12 +18,12 @@
import Common import Common
import Foundation import Foundation
import Macros import Macros
import P4Lang
import Runtime import Runtime
import SwiftTreeSitter import SwiftTreeSitter
import Testing import Testing
import TreeSitter import TreeSitter
import TreeSitterP4 import TreeSitterP4
import P4Lang
@testable import P4Compiler @testable import P4Compiler
@@ -136,7 +136,8 @@ import P4Lang
#expect( #expect(
#RequireErrorResult( #RequireErrorResult(
Error( 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)) Program.Compile(simple_parser_declaration))
) )
@@ -163,7 +164,8 @@ import P4Lang
#expect( #expect(
#RequireErrorResult( #RequireErrorResult(
Error( 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)) Program.Compile(simple_parser_declaration))
) )
@@ -190,7 +192,8 @@ import P4Lang
#expect( #expect(
#RequireErrorResult( #RequireErrorResult(
Error( 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)) Program.Compile(simple_parser_declaration))
) )
@@ -320,3 +323,80 @@ import P4Lang
Program.Compile(simple_parser_declaration)) Program.Compile(simple_parser_declaration))
) )
} }
@Test func test_simple_control_declaration_with_action_with_params_right_order() async throws {
let simple_parser_declaration = """
control simple(int x, int y) {
action a(inout int ax, bool ay) {
ax = 5;
}
table t {
key = {
x: exact;
y: exact;
}
}
apply {
}
};
"""
let x = { (tipe: P4QualifiedType) -> Bool in
switch tipe.baseType() {
case let c as Control: c.name == "simple"
default: false
}
}
let program = try! #UseOkResult(Program.Compile(simple_parser_declaration))
#expect(program.InstancesWithTypes(x).count == 1)
}
@Test func test_simple_control_declaration_with_action_with_params_wrong_order() async throws {
let simple_parser_declaration = """
control simple(int x, int y) {
action a(int ax, inout bool ay) {
ay = false;
}
table t {
key = {
x: exact;
y: exact;
}
}
apply {
}
};
"""
#expect(
#RequireErrorResult(
ErrorWithLocation(
sourceLocation: SourceLocation(41, 23),
withError: "All parameters with direction must precede directionless parameters"),
Program.Compile(simple_parser_declaration))
)
}
@Test func test_simple_control_declaration_with_action_with_params_wrong_order_interspersed() async throws {
let simple_parser_declaration = """
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 {
}
};
"""
#expect(
#RequireErrorResult(
ErrorWithLocation(
sourceLocation: SourceLocation(41, 38),
withError: "All parameters with direction must precede directionless parameters"),
Program.Compile(simple_parser_declaration))
)
}
+139 -10
View File
@@ -370,11 +370,11 @@ import TreeSitterP4
let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( let (hit_miss, updated_execution) = try #UseOkResult(runtime.run(
withArguments: ArgumentList([ withArguments: ArgumentList([
Argument(TypedIdentifier(name: "result_arg", withType: P4QualifiedType(P4Int())), atIndex: 0), Argument(TypedIdentifier(name: "result_arg", withType: P4QualifiedType(P4Int())), atIndex: 0),
Argument(P4Value(P4BooleanValue(withValue: false)), atIndex: 1), Argument(P4Value(P4BooleanValue(withValue: false)), atIndex: 1), // false will make the x key miss.
Argument(P4Value(P4IntValue(withValue: 5)), atIndex: 2), Argument(P4Value(P4IntValue(withValue: 5)), atIndex: 2),
]))) ])))
// We expect there to be a hit. // We expect there to be a hit -- but from the second key!
#expect(hit_miss == P4TableHitMissValue.Hit) #expect(hit_miss == P4TableHitMissValue.Hit)
// And that the proper action was invoked. // And that the proper action was invoked.
@@ -384,7 +384,11 @@ import TreeSitterP4
@Test func test_control_key_from_struct() async throws { @Test func test_control_key_from_struct() async throws {
let simple_parser_declaration = """ let simple_parser_declaration = """
control simple(inout int result, bool x, int f) { struct K {
int i;
int j;
};
control simple(inout int result, K k) {
action a() { action a() {
result = 5; result = 5;
} }
@@ -393,8 +397,7 @@ import TreeSitterP4
} }
table t { table t {
key = { key = {
x: exact; k.i: exact;
f: exact;
} }
actions = { actions = {
a; a;
@@ -417,15 +420,26 @@ import TreeSitterP4
} }
var control = ((controls[0].baseType() as P4Type) as! Control) var control = ((controls[0].baseType() as P4Type) as! Control)
let k_fields = P4StructFields([
P4StructFieldIdentifier(name: "i", withType: P4QualifiedType(P4Int())),
P4StructFieldIdentifier(name: "j", withType: P4QualifiedType(P4Int())),
])
let k_type = P4Struct(withName: Identifier(name: "K"), andFields: k_fields)
let k_instance = P4StructValue(withType: k_type, andInitializers: [
P4Value(P4IntValue(withValue: 5)),
P4Value(P4IntValue(withValue: 1)),
])
// Add entries to the table. // Add entries to the table.
control = control.updateTable( control = control.updateTable(
addEntry: ( addEntry: (
P4Value(P4BooleanValue(withValue: true)), P4Value(P4IntValue(withValue: 5)),
TypedIdentifier(name: "a", withType: P4QualifiedType(Action())) TypedIdentifier(name: "a", withType: P4QualifiedType(Action()))
) )
).updateTable( ).updateTable(
addEntry: ( addEntry: (
P4Value(P4IntValue(withValue: 5)), P4Value(P4IntValue(withValue: 4)),
TypedIdentifier(name: "b", withType: P4QualifiedType(Action())) TypedIdentifier(name: "b", withType: P4QualifiedType(Action()))
)) ))
@@ -443,8 +457,7 @@ import TreeSitterP4
let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( let (hit_miss, updated_execution) = try #UseOkResult(runtime.run(
withArguments: ArgumentList([ withArguments: ArgumentList([
Argument(TypedIdentifier(name: "result_arg", withType: P4QualifiedType(P4Int())), atIndex: 0), Argument(TypedIdentifier(name: "result_arg", withType: P4QualifiedType(P4Int())), atIndex: 0),
Argument(P4Value(P4BooleanValue(withValue: false)), atIndex: 1), Argument(P4Value(k_instance), atIndex: 1),
Argument(P4Value(P4IntValue(withValue: 5)), atIndex: 2),
]))) ])))
// We expect there to be a hit. // We expect there to be a hit.
@@ -452,6 +465,122 @@ import TreeSitterP4
// And that the proper action was invoked. // And that the proper action was invoked.
let result_arg = try #UseOkResult(updated_execution.scopes.lookup(identifier: Identifier(name: "result_arg"))) let result_arg = try #UseOkResult(updated_execution.scopes.lookup(identifier: Identifier(name: "result_arg")))
#expect(result_arg.eq(P4Value(P4IntValue(withValue: 7)))) #expect(result_arg.eq(P4Value(P4IntValue(withValue: 5))))
// Now, check whether the b action can be invoked.
let k_instance2 = P4StructValue(withType: k_type, andInitializers: [
P4Value(P4IntValue(withValue: 4)),
P4Value(P4IntValue(withValue: 1)),
])
// Set a variable in the global scope for the inout first parameter.
var next_global_values = VarValueScopes().enter()
next_global_values = global_values.declare(
identifier: Identifier(name: "result_arg"),
withValue: P4Value(
P4IntValue(withValue: 0),
P4QualifiedType(P4Int())))
let runtime2 = try #UseOkResult(
P4Runtime.Runtime<P4TableHitMissValue, Control>.create(control: control, withGlobalValues: next_global_values))
let (hit_miss2, updated_execution2) = try #UseOkResult(runtime2.run(
withArguments: ArgumentList([
Argument(TypedIdentifier(name: "result_arg", withType: P4QualifiedType(P4Int())), atIndex: 0),
Argument(P4Value(k_instance2), atIndex: 1),
])))
// We expect there to be a hit.
#expect(hit_miss2 == P4TableHitMissValue.Hit)
// And that the proper action was invoked.
let result_arg2 = try #UseOkResult(updated_execution2.scopes.lookup(identifier: Identifier(name: "result_arg")))
#expect(result_arg2.eq(P4Value(P4IntValue(withValue: 7))))
} }
@Test func test_control_key_from_struct_miss() async throws {
let simple_parser_declaration = """
struct K {
int i;
int j;
};
control simple(inout int result, K k) {
action a() {
result = 5;
}
action b() {
result = 7;
}
table t {
key = {
k.i: exact;
}
actions = {
a;
b;
}
}
apply {
}
};
"""
let program = try! #UseOkResult(Program.Compile(simple_parser_declaration))
// Pull the control out of the compiled program.
let controls = program.InstancesWithTypes() { (tipe: P4QualifiedType) -> Bool in
switch tipe.baseType() {
case let c as Control: c.name == "simple"
default: false
}
}
var control = ((controls[0].baseType() as P4Type) as! Control)
let k_fields = P4StructFields([
P4StructFieldIdentifier(name: "i", withType: P4QualifiedType(P4Int())),
P4StructFieldIdentifier(name: "j", withType: P4QualifiedType(P4Int())),
])
let k_type = P4Struct(withName: Identifier(name: "K"), andFields: k_fields)
let k_instance = P4StructValue(withType: k_type, andInitializers: [
P4Value(P4IntValue(withValue: 8)),
P4Value(P4IntValue(withValue: 1)),
])
// Add entries to the table.
control = control.updateTable(
addEntry: (
P4Value(P4IntValue(withValue: 5)),
TypedIdentifier(name: "a", withType: P4QualifiedType(Action()))
)
).updateTable(
addEntry: (
P4Value(P4IntValue(withValue: 4)),
TypedIdentifier(name: "b", withType: P4QualifiedType(Action()))
))
// Set a variable in the global scope for the inout first parameter.
var global_values = VarValueScopes().enter()
global_values = global_values.declare(
identifier: Identifier(name: "result_arg"),
withValue: P4Value(
P4IntValue(withValue: 0),
P4QualifiedType(P4Int())))
let runtime = try #UseOkResult(
P4Runtime.Runtime<P4TableHitMissValue, Control>.create(control: control, withGlobalValues: global_values))
let (hit_miss, updated_execution) = try #UseOkResult(runtime.run(
withArguments: ArgumentList([
Argument(TypedIdentifier(name: "result_arg", withType: P4QualifiedType(P4Int())), atIndex: 0),
Argument(P4Value(k_instance), atIndex: 1),
])))
// We expect there to be a hit.
#expect(hit_miss == P4TableHitMissValue.Miss)
// And that the proper action was invoked.
let result_arg = try #UseOkResult(updated_execution.scopes.lookup(identifier: Identifier(name: "result_arg")))
#expect(result_arg.eq(P4Value(P4IntValue(withValue: 0))))
}
+1
View File
@@ -7,6 +7,7 @@ swift format --recursive -i Sources/ 2>&1 >/dev/null
mc=`git status . | grep modified | wc -l | sed 's/ //g'` mc=`git status . | grep modified | wc -l | sed 's/ //g'`
if [ ${mc} -ne 0 ]; then if [ ${mc} -ne 0 ]; then
git status .
exit 1 exit 1
fi fi