From ed976c7855814f14c5fc3755c6ea1b04fa1c51b9 Mon Sep 17 00:00:00 2001 From: Will Hawkins Date: Thu, 30 Apr 2026 18:39:52 -0400 Subject: [PATCH] compiler, runtime: Initial Support For Calling Controls Support calling (invoking) a Control. There is still plenty more to do here, but we are off to a good start. Signed-off-by: Will Hawkins --- Sources/P4Compiler/Declarations.swift | 21 +- Sources/P4Lang/Control.swift | 47 ++- Sources/P4Runtime/Control.swift | 100 +++++ .../Compile.swift} | 0 Tests/p4rseTests/ControlTests/Runtime.swift | 384 ++++++++++++++++++ 5 files changed, 524 insertions(+), 28 deletions(-) create mode 100644 Sources/P4Runtime/Control.swift rename Tests/p4rseTests/{ControlCompilerTests.swift => ControlTests/Compile.swift} (100%) create mode 100644 Tests/p4rseTests/ControlTests/Runtime.swift diff --git a/Sources/P4Compiler/Declarations.swift b/Sources/P4Compiler/Declarations.swift index 9540ba4..d5286e8 100644 --- a/Sources/P4Compiler/Declarations.swift +++ b/Sources/P4Compiler/Declarations.swift @@ -691,7 +691,6 @@ extension TableKeys: Compilable { walker.next() // 3 var current_node: Node? = .none - var current_context = context #MustOr( result: current_node, thing: walker.getNext(), @@ -700,28 +699,22 @@ extension TableKeys: Compilable { node: node, withError: "Missing table keys declaration component in control declaration")) ) - var entries: [TableKeyEntry] = Array() - var errors: [Error] = Array() - - current_node!.enumerateNamedChildren { entry in - switch TableKeyEntry.Compile(node: current_node!, withContext: current_context) { - case .Ok((let keyset_expression, let updated_context)): - entries.append(keyset_expression) - current_context = updated_context - case .Error(let e): errors.append(e) + let (keys, errors) = walker.try_map(n: node.childCount - 1, onlyNamed: true) { current_node in + return switch TableKeyEntry.Compile(node: current_node, withContext: context) { + case .Ok((let keyset_expression, _)): .Ok(keyset_expression) + case .Error(let e): .Error(e) } } if !errors.isEmpty { return .Error( - Error( - withMessage: "Error(s) parsing table key: " + ErrorOnNode(node: node, withError: "Error(s) parsing table key: " + (errors.map { error in return "\(error.msg)" }.joined(separator: ";")))) } - return .Ok((TableKeys(withEntries: entries), current_context)) + return .Ok((TableKeys(withEntries: keys), context)) } } @@ -828,7 +821,7 @@ extension TablePropertyList: Compilable { ErrorOnNode(node: node, withError: "More than one key set in table property list")) } - // There should be only one table keys! + // There should be only one table actions! if actions.count > 1 { // Todo: Make this error message better. return .Error( diff --git a/Sources/P4Lang/Control.swift b/Sources/P4Lang/Control.swift index 9870ceb..dcbcd5b 100644 --- a/Sources/P4Lang/Control.swift +++ b/Sources/P4Lang/Control.swift @@ -89,7 +89,7 @@ public struct Action: CustomStringConvertible, P4DataType, P4DataValue { } public struct Actions: CustomStringConvertible { - let actions: [Action] + public let actions: [Action] public init(withActions actions: [Action]) { self.actions = actions } @@ -107,8 +107,8 @@ public enum TableKeyMatchType { } public struct TableKeyEntry: CustomStringConvertible { - let key: KeysetExpression - let match_type: TableKeyMatchType + public let key: KeysetExpression + public let match_type: TableKeyMatchType public init(_ key: KeysetExpression, _ match: TableKeyMatchType) { self.key = key @@ -121,19 +121,19 @@ public struct TableKeyEntry: CustomStringConvertible { } public struct TableKeys: CustomStringConvertible { - let entries: [TableKeyEntry] + public let keys: [TableKeyEntry] public init(withEntries entries: [TableKeyEntry]) { - self.entries = entries + self.keys = entries } public init() { - self.entries = [] + self.keys = [] } public var description: String { return "Table Keys: " - + self.entries.map { entry in - return "\(entry)" + + self.keys.map { key in + return "\(key)" }.joined(separator: ";") } } @@ -153,8 +153,8 @@ public struct TableActionsProperty: CustomStringConvertible { } public struct TablePropertyList: CustomStringConvertible { - let actions: TableActionsProperty - let keys: TableKeys + public let actions: TableActionsProperty + public let keys: TableKeys public init(withActions actions: TableActionsProperty, withKeys keys: TableKeys) { self.actions = actions self.keys = keys @@ -166,17 +166,30 @@ public struct TablePropertyList: CustomStringConvertible { } public struct Table: CustomStringConvertible { - let properties: TablePropertyList + public let properties: TablePropertyList let name: Identifier + public let entries: [(P4Value, TypedIdentifier)] - public init(withName name: Identifier, withPropertyList property_list: TablePropertyList) { + public init( + withName name: Identifier, withPropertyList property_list: TablePropertyList, + withEntries entries: [(P4Value, TypedIdentifier)] = [] + ) { self.name = name self.properties = property_list + self.entries = entries } public var description: String { return "Table named: \(self.name) \(self.properties)" } + + /// When the control is evaluated, the value of the x in the table is + /// compared to the entries and the match is assocated with an action + /// that is invoked when the match occurs! + + public func update(addEntry entry: (P4Value, TypedIdentifier)) -> Table{ + return Table(withName: self.name, withPropertyList: self.properties, withEntries: self.entries + [entry]) + } } public struct Control: P4DataType, P4DataValue, Equatable, CustomStringConvertible { @@ -236,8 +249,8 @@ public struct Control: P4DataType, P4DataValue, Equatable, CustomStringConvertib return "Control named \(self._name) \(self.parameters) \(self.actions) \(self.table)" } - let actions: Actions - let table: Table + public let actions: Actions + public let table: Table let _parameters: ParameterList let _name: Identifier let apply: ApplyStatement @@ -261,6 +274,12 @@ public struct Control: P4DataType, P4DataValue, Equatable, CustomStringConvertib self.apply = apply } + public func updateTable(addEntry entry: (P4Value, TypedIdentifier)) -> Control { + let table = self.table.update(addEntry: entry) + + return Control(named: self.name, withParameters: self.parameters, withTable: table, withActions: self.actions, withApply: self.apply) + } + public func def() -> any P4DataValue { return Control( named: Identifier(name: ""), diff --git a/Sources/P4Runtime/Control.swift b/Sources/P4Runtime/Control.swift new file mode 100644 index 0000000..b1b2006 --- /dev/null +++ b/Sources/P4Runtime/Control.swift @@ -0,0 +1,100 @@ +// 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 P4Lang + +extension Control: LibraryCallable { + public typealias T = P4TableHitMissValue + public func call( + execution: Common.ProgramExecution, arguments: ArgumentList + ) -> (P4TableHitMissValue, Common.ProgramExecution) { + + var control_execution = execution.enter_scope() + + // Add initial values to the global scope + for (name, value) in execution.getGlobalValues() { + control_execution = control_execution.declare(identifier: name, withValue: value) + } + + let call_body: (ProgramExecution) -> (Result, ProgramExecution) = { + execution in + var control_execution = execution + + for action in self.actions.actions { + control_execution = control_execution.declare( + identifier: action.name, withValue: P4Value(action)) + } + + for key in self.table.properties.keys.keys { + // Every evaluation of the key starts from an unchanged execution context. + let (key_eval, updated_execution) = key.key.evaluate(execution: control_execution) + + guard case .Ok(let key_val) = key_eval else { + return (.Error(key_eval.error()!), updated_execution) + } + + /// ASSUME: The first matching entry is the one to do. + /// TODO: Check whether this matches architecture. + for (val, action) in self.table.entries { + + // Skip those with mismatching types. + + if !val.type().eq(key_val.type()) { + continue; + } + + /// ASSUME: All matches are exact. + if val.eq(key_val) { + // Lookup action! + + let maybe_action = updated_execution.scopes.lookup(identifier: action) + guard case .Ok(let action) = maybe_action else { + return (.Error(maybe_action.error()!), updated_execution) + } + + let aaction = (action.dataValue() as! Action) + + return switch aaction.evaluate(execution: updated_execution) { + case (ControlFlow.Error, let updated_execution): + (.Error(updated_execution.getError()!), updated_execution) + case (_, let updated_execution): (.Ok(P4TableHitMissValue.Hit), updated_execution) + } + } + } + } + return (.Ok(P4TableHitMissValue.Miss), control_execution) + } + + switch Call(body: call_body, withArguments: arguments, withParameters: self.parameters, inExecution: control_execution) { + case (.Ok(let r), let updated_execution): return (r, updated_execution) + case (.Error(let e), let updated_execution): return (P4TableHitMissValue.Miss, updated_execution.setError(error: e)) + } + } +} + +extension Action: EvaluatableStatement { + public func evaluate( + execution: Common.ProgramExecution + ) -> (Common.ControlFlow, Common.ProgramExecution) { + if let body = self.body { + return body.evaluate(execution: execution) + } + + return (ControlFlow.Next, execution) + } +} diff --git a/Tests/p4rseTests/ControlCompilerTests.swift b/Tests/p4rseTests/ControlTests/Compile.swift similarity index 100% rename from Tests/p4rseTests/ControlCompilerTests.swift rename to Tests/p4rseTests/ControlTests/Compile.swift diff --git a/Tests/p4rseTests/ControlTests/Runtime.swift b/Tests/p4rseTests/ControlTests/Runtime.swift new file mode 100644 index 0000000..d861005 --- /dev/null +++ b/Tests/p4rseTests/ControlTests/Runtime.swift @@ -0,0 +1,384 @@ +// 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 P4Lang +import P4Runtime +import SwiftTreeSitter +import Testing +import TreeSitter +import TreeSitterP4 + +@testable import P4Compiler + +@Test func test_control_single_key() async throws { + let simple_parser_declaration = """ + control simple(inout int result, bool x) { + action a() { + result = 5; + } + action b() { + result = 7; + } + table t { + key = { + x: 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: P4Type) -> Bool in + switch tipe.dataType() { + case let c as Control: c.name == "simple" + default: false + } + } + var control = ((controls[0].dataType() as P4DataType) as! Control) + + // Add entries to the table. + control = control.updateTable( + addEntry: ( + P4Value(P4BooleanValue(withValue: true)), + TypedIdentifier(name: "a", withType: P4Type(Action())) + ) + ).updateTable( + addEntry: ( + P4Value(P4BooleanValue(withValue: false)), + TypedIdentifier(name: "b", withType: P4Type(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), + P4Type(P4Int()))) + + let runtime = try #UseOkResult( + P4Runtime.Runtime.create(control: control, withGlobalValues: global_values)) + + let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( + withArguments: ArgumentList([ + Argument(TypedIdentifier(name: "result_arg", withType: P4Type(P4Int())), atIndex: 0), + Argument(P4Value(P4BooleanValue(withValue: true)), atIndex: 1), + ]))) + + // We expect there to be a hit. + #expect(hit_miss == P4TableHitMissValue.Hit) + + // 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: 5)))) +} + +@Test func test_control_single_key_false() async throws { + let simple_parser_declaration = """ + control simple(inout int result, bool x) { + action a() { + result = 5; + } + action b() { + result = 7; + } + table t { + key = { + x: 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: P4Type) -> Bool in + switch tipe.dataType() { + case let c as Control: c.name == "simple" + default: false + } + } + var control = ((controls[0].dataType() as P4DataType) as! Control) + + // Add entries to the table. + control = control.updateTable( + addEntry: ( + P4Value(P4BooleanValue(withValue: true)), + TypedIdentifier(name: "a", withType: P4Type(Action())) + ) + ).updateTable( + addEntry: ( + P4Value(P4BooleanValue(withValue: false)), + TypedIdentifier(name: "b", withType: P4Type(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), + P4Type(P4Int()))) + + let runtime = try #UseOkResult( + P4Runtime.Runtime.create(control: control, withGlobalValues: global_values)) + + let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( + withArguments: ArgumentList([ + Argument(TypedIdentifier(name: "result_arg", withType: P4Type(P4Int())), atIndex: 0), + Argument(P4Value(P4BooleanValue(withValue: false)), atIndex: 1), + ]))) + + // We expect there to be a hit. + #expect(hit_miss == P4TableHitMissValue.Hit) + + // 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: 7)))) +} + +@Test func test_control_single_integer_key_hit() async throws { + let simple_parser_declaration = """ + control simple(inout int result, int x) { + action a() { + result = 5; + } + action b() { + result = 7; + } + table t { + key = { + x: 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: P4Type) -> Bool in + switch tipe.dataType() { + case let c as Control: c.name == "simple" + default: false + } + } + var control = ((controls[0].dataType() as P4DataType) as! Control) + + // Add entries to the table. + control = control.updateTable( + addEntry: ( + P4Value(P4IntValue(withValue: 5)), + TypedIdentifier(name: "a", withType: P4Type(Action())) + ) + ).updateTable( + addEntry: ( + P4Value(P4IntValue(withValue: 2)), + TypedIdentifier(name: "b", withType: P4Type(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), + P4Type(P4Int()))) + + let runtime = try #UseOkResult( + P4Runtime.Runtime.create(control: control, withGlobalValues: global_values)) + + let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( + withArguments: ArgumentList([ + Argument(TypedIdentifier(name: "result_arg", withType: P4Type(P4Int())), atIndex: 0), + Argument(P4Value(P4IntValue(withValue: 5)), atIndex: 1), + ]))) + + // We expect there to be a hit. + #expect(hit_miss == P4TableHitMissValue.Hit) + + let result_arg = try #UseOkResult(updated_execution.scopes.lookup(identifier: Identifier(name: "result_arg"))) + #expect(result_arg.eq(P4Value(P4IntValue(withValue: 5)))) +} + +@Test func test_control_single_integer_key_miss() async throws { + let simple_parser_declaration = """ + control simple(inout int result, int x) { + action a() { + result = 5; + } + action b() { + result = 7; + } + table t { + key = { + x: 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: P4Type) -> Bool in + switch tipe.dataType() { + case let c as Control: c.name == "simple" + default: false + } + } + var control = ((controls[0].dataType() as P4DataType) as! Control) + + // Add entries to the table. + control = control.updateTable( + addEntry: ( + P4Value(P4IntValue(withValue: 1)), + TypedIdentifier(name: "a", withType: P4Type(Action())) + ) + ).updateTable( + addEntry: ( + P4Value(P4IntValue(withValue: 2)), + TypedIdentifier(name: "b", withType: P4Type(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), + P4Type(P4Int()))) + + let runtime = try #UseOkResult( + P4Runtime.Runtime.create(control: control, withGlobalValues: global_values)) + + let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( + withArguments: ArgumentList([ + Argument(TypedIdentifier(name: "result_arg", withType: P4Type(P4Int())), atIndex: 0), + Argument(P4Value(P4IntValue(withValue: 3)), atIndex: 1), + ]))) + + // We expect there to be a hit. + #expect(hit_miss == P4TableHitMissValue.Miss) + + let result_arg = try #UseOkResult(updated_execution.scopes.lookup(identifier: Identifier(name: "result_arg"))) + #expect(result_arg.eq(P4Value(P4IntValue(withValue: 0)))) +} + +@Test func test_control_multiple_keys() async throws { + let simple_parser_declaration = """ + control simple(inout int result, bool x, int f) { + action a() { + result = 5; + } + action b() { + result = 7; + } + table t { + key = { + x: exact; + f: 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: P4Type) -> Bool in + switch tipe.dataType() { + case let c as Control: c.name == "simple" + default: false + } + } + var control = ((controls[0].dataType() as P4DataType) as! Control) + + // Add entries to the table. + control = control.updateTable( + addEntry: ( + P4Value(P4BooleanValue(withValue: true)), + TypedIdentifier(name: "a", withType: P4Type(Action())) + ) + ).updateTable( + addEntry: ( + P4Value(P4IntValue(withValue: 5)), + TypedIdentifier(name: "b", withType: P4Type(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), + P4Type(P4Int()))) + + let runtime = try #UseOkResult( + P4Runtime.Runtime.create(control: control, withGlobalValues: global_values)) + + let (hit_miss, updated_execution) = try #UseOkResult(runtime.run( + withArguments: ArgumentList([ + Argument(TypedIdentifier(name: "result_arg", withType: P4Type(P4Int())), atIndex: 0), + Argument(P4Value(P4BooleanValue(withValue: false)), atIndex: 1), + Argument(P4Value(P4IntValue(withValue: 5)), atIndex: 2), + ]))) + + // We expect there to be a hit. + #expect(hit_miss == P4TableHitMissValue.Hit) + + // 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: 7)))) + +}