From d971aab1fe9fcc1ef55dbeedb2ff7d356402962d Mon Sep 17 00:00:00 2001 From: Will Hawkins Date: Thu, 2 Apr 2026 01:28:43 -0400 Subject: [PATCH] Support Calling Parsers With Parameters Signed-off-by: Will Hawkins --- Sources/Common/Support.swift | 6 + Sources/P4Compiler/Declarations.swift | 171 ++++++++++-------- Sources/P4Lang/Declarations.swift | 4 + Sources/P4Lang/Expressions.swift | 31 ++++ Sources/P4Lang/Parser.swift | 4 +- Sources/P4Runtime/Parser.swift | 33 +++- Sources/P4Runtime/Protocols.swift | 5 +- Sources/P4Runtime/Runtime.swift | 10 +- .../ExpressionTests/SelectExpression.swift | 42 +++++ Tests/p4rseTests/ParserCompilerTests.swift | 13 ++ Tests/p4rseTests/RuntimeTests.swift | 91 ++++++++++ Tests/p4rseTests/ValueTypeParserTests.swift | 17 ++ 12 files changed, 345 insertions(+), 82 deletions(-) diff --git a/Sources/Common/Support.swift b/Sources/Common/Support.swift index fb86088..1ef4ae4 100644 --- a/Sources/Common/Support.swift +++ b/Sources/Common/Support.swift @@ -96,6 +96,12 @@ public enum Result: Equatable { } } + public func ok() -> Bool { + switch self { + case .Ok(_): true + case .Error(_): false + } + } public func error() -> Error? { if case Result.Error(let e) = self { return e diff --git a/Sources/P4Compiler/Declarations.swift b/Sources/P4Compiler/Declarations.swift index 33b1821..84ac2f0 100644 --- a/Sources/P4Compiler/Declarations.swift +++ b/Sources/P4Compiler/Declarations.swift @@ -171,7 +171,7 @@ extension P4Lang.Parser: CompilableDeclaration { var parser_name: Common.Identifier? = .none // Assume that the parameter list is empty! - var parameter_list: ParameterList = ParameterList([]) + var parameter_list = ParameterList() do { // Parse the parser type (type_node) @@ -207,31 +207,28 @@ extension P4Lang.Parser: CompilableDeclaration { return .Error(e) } - // Now, see if there are any parameters currentChildIdx += 1 currentChildIdxSafe += 1 - if currentChildIdxSafe < type_node!.childCount { - - // There is something that _should_ be parameters! - - // skip the '(' - currentChildIdx += 1 - currentChildIdxSafe += 1 - if type_node!.childCount < currentChildIdxSafe { - return .Error( - ErrorOnNode(node: currentChild!, withError: "Missing ( before parameter list")) - } - - currentChild = type_node?.child(at: currentChildIdx) - - switch ParameterList.Compile(node: currentChild!, withContext: current_context) { - case .Ok(let (parsed_parameter_list, updated_context)): - parameter_list = parsed_parameter_list - current_context = updated_context - case .Error(let e): - return .Error(e) - } + if type_node!.childCount < currentChildIdxSafe { + return .Error( + ErrorOnNode(node: type_node!, withError: "Missing parser parameters")) } + + currentChild = type_node?.child(at: currentChildIdx) + switch ParameterList.Compile(node: currentChild!, withContext: current_context) { + case .Ok(let (parsed_parameter_list, updated_context)): + parameter_list = parsed_parameter_list + current_context = updated_context + case .Error(let e): + return .Error(e) + } + } + + // Now, let's put the parameters into scope. + for parameter in parameter_list.parameters { + current_context = current_context.update( + newInstances: current_context.instances.declare( + identifier: parameter.name, withValue: parameter.type)) } currentChildIdx += 1 @@ -281,73 +278,101 @@ extension P4Lang.Parser: CompilableDeclaration { } } +func parameter_list_compiler( + node: SwiftTreeSitter.Node, withContext context: CompilerContext +) -> Common.Result<(ParameterList, CompilerContext)> { + + var currentChildIdx = 0 + var currentChildIdxSafe = 1 + var currentChild: Node? = .none + + if node.text == ")" { + // There are no parameters! + return Result.Ok((ParameterList([]), context)) + } + + #RequireNodeType( + node: node, type: "parameter_list", nice_type_name: "Parameter List") + + var parameters: ParameterList = ParameterList([]) + + if node.childCount < currentChildIdxSafe { + return Result.Error( + ErrorOnNode(node: node, withError: "Missing parameter list component")) + } + + currentChild = node.child(at: currentChildIdx) + if currentChild?.nodeType == "parameter_list" { + switch parameter_list_compiler(node: currentChild!, withContext: context) { + case .Ok(let (ps, _)): + parameters = ps + case .Error(let e): return Result.Error(e) + } + + currentChildIdx += 1 + currentChildIdxSafe += 1 + } + + // We may have moved nodes, check/reset currentChild. + if node.childCount < currentChildIdxSafe { + return Result.Error( + ErrorOnNode(node: node, withError: "Missing parameter list component")) + } + currentChild = node.child(at: currentChildIdx) + + // If this is a ')', we are done. + if currentChild?.text == ")" { + return Result.Ok((parameters, context)) + } + + // If this is a comma, we skip it! + if currentChild?.text == "," { + currentChildIdx += 1 + currentChildIdxSafe += 1 + } + + if node.childCount < currentChildIdxSafe { + return Result.Error( + ErrorOnNode(node: node, withError: "Missing parameter list component")) + } + currentChild = node.child(at: currentChildIdx) + + // Otherwise, there should be one parameter left! + switch Parameter.Compile(node: currentChild!, withContext: context) { + case .Ok(let (vds, updated_context)): + return Result.Ok((parameters.addParameter(vds), updated_context)) + case .Error(let e): return Result.Error(e) + } +} + extension ParameterList: Compilable { public typealias T = ParameterList public static func Compile( node: SwiftTreeSitter.Node, withContext context: CompilerContext ) -> Common.Result<(ParameterList, CompilerContext)> { - if node.text == ")" { - // There are no parameters! - return .Ok((ParameterList([]), context)) - } - + let parameter_node = node #RequireNodeType( - node: node, type: "parameter_list", nice_type_name: "Parameter List") + node: parameter_node, type: "parameters", nice_type_name: "Parameters") var currentChildIdx = 0 var currentChildIdxSafe = 1 - var currentChild: Node? = .none - var parameters: ParameterList = ParameterList([]) - if node.childCount < currentChildIdxSafe { + // Let's eat the '(' before we start ... + if parameter_node.childCount < currentChildIdxSafe { return .Error( - ErrorOnNode(node: node, withError: "Missing parameter list component")) + ErrorOnNode(node: parameter_node, withError: "Missing '(' in parameter list component")) } - currentChild = node.child(at: currentChildIdx) - if currentChild?.nodeType == "parameter_list" { - switch ParameterList.Compile(node: currentChild!, withContext: context) { - case .Ok(let (ps, _)): - parameters = ps - case .Error(let e): return .Error(e) - } - - print("Back here!") - currentChildIdx += 1 - currentChildIdxSafe += 1 - } - - // We may have moved nodes, check/reset currentChild. - if node.childCount < currentChildIdxSafe { + currentChildIdx += 1 + currentChildIdxSafe += 1 + if parameter_node.childCount < currentChildIdxSafe { return .Error( - ErrorOnNode(node: node, withError: "Missing parameter list component")) + ErrorOnNode(node: parameter_node, withError: "Missing parameter list component")) } - currentChild = node.child(at: currentChildIdx) + let currentChild = parameter_node.child(at: currentChildIdx) - // If this is a ')', we are done. - if currentChild?.text == ")" { - return .Ok((parameters, context)) - } - - // If this is a comma, we skip it! - if currentChild?.text == "," { - currentChildIdx += 1 - currentChildIdxSafe += 1 - } - - if node.childCount < currentChildIdxSafe { - return .Error( - ErrorOnNode(node: node, withError: "Missing parameter list component")) - } - currentChild = node.child(at: currentChildIdx) - - // Otherwise, there should be one parameter left! - switch Parameter.Compile(node: currentChild!, withContext: context) { - case .Ok(let (vds, updated_context)): - return .Ok((parameters.addParameter(vds), updated_context)) - case .Error(let e): return .Error(e) - } + return parameter_list_compiler(node: currentChild!, withContext: context) } } diff --git a/Sources/P4Lang/Declarations.swift b/Sources/P4Lang/Declarations.swift index 013db12..ed29b58 100644 --- a/Sources/P4Lang/Declarations.swift +++ b/Sources/P4Lang/Declarations.swift @@ -38,6 +38,10 @@ public struct Parameter: CustomStringConvertible { public struct ParameterList: CustomStringConvertible { public var parameters: [Parameter] + public init() { + self.parameters = Array() + } + public init(_ parameters: [Parameter]) { self.parameters = parameters } diff --git a/Sources/P4Lang/Expressions.swift b/Sources/P4Lang/Expressions.swift index ea9454c..51c7f98 100644 --- a/Sources/P4Lang/Expressions.swift +++ b/Sources/P4Lang/Expressions.swift @@ -200,3 +200,34 @@ public struct FieldAccessExpression { self.field = field } } + +public struct ArgumentList { + public let arguments: [(Int, EvaluatableExpression)] + public init(_ arguments: [EvaluatableExpression]) { + self.arguments = zip(1..., arguments).map { (idx, argument) in + (idx, argument) + } + } + + public func compatible(_ parameters: ParameterList) -> Result<()> { + if self.arguments.count != parameters.parameters.count { + return .Error( + Error( + withMessage: + "\(self.arguments.count) arguments found but \(parameters.parameters.count) required")) + } + + for (arg, param) in zip(self.arguments, parameters.parameters) { + let arg_index = arg.0 + let arg_type = arg.1.type() + if !arg_type.eq(rhs: param.type) { + return .Error( + Error( + withMessage: + "Argument \(arg_index)'s type (\(arg_type)) is incompatible with the parameter type (\(param.type))" + )) + } + } + return .Ok(()) + } +} diff --git a/Sources/P4Lang/Parser.swift b/Sources/P4Lang/Parser.swift index 4de2987..e60478c 100644 --- a/Sources/P4Lang/Parser.swift +++ b/Sources/P4Lang/Parser.swift @@ -319,10 +319,11 @@ public struct Parser: P4Type, P4Value { public var states: ParserStates public var name: Identifier - public var parameters: ParameterList? + public var parameters: ParameterList public init(withName name: Identifier) { self.states = ParserStates() + self.parameters = ParameterList() self.name = name } @@ -349,7 +350,6 @@ public struct Parser: P4Type, P4Value { } public var description: String { - let parameters = self.parameters?.description ?? "N/A" return "Parser \(self.name) with parameters: \(parameters) and states: \(self.states)" } diff --git a/Sources/P4Runtime/Parser.swift b/Sources/P4Runtime/Parser.swift index 03f4ca6..148d255 100644 --- a/Sources/P4Runtime/Parser.swift +++ b/Sources/P4Runtime/Parser.swift @@ -117,8 +117,11 @@ extension ParserStateSelectTransition: EvaluatableParserState { } } -extension Parser: ParserExecution { - public func execute(execution: ProgramExecution) -> (InstantiatedParserState, ProgramExecution) { +extension Parser: CallableExecution { + public typealias T = InstantiatedParserState + public func call( + execution: Common.ProgramExecution, arguments: P4Lang.ArgumentList + ) -> (P4Lang.InstantiatedParserState, Common.ProgramExecution) { var execution = execution.enter_scope() execution = execution.declare( @@ -146,10 +149,34 @@ extension Parser: ParserExecution { ) } + // Now that we are assured that there is a start state, + // let's set the arguments. + + if case .Error(let e) = arguments.compatible(self.parameters) { + return ( + reject, execution.setError(error: Error(withMessage: "Cannot call parser: \(e)")) + ) + } + + for (parameter, argument) in zip(self.parameters.parameters, arguments.arguments) { + let arg_idx = argument.0 + let arg_value = argument.1 + let maybe_argument_value = arg_value.evaluate(execution: execution) + guard case .Ok(let argument_value) = maybe_argument_value else { + return ( + reject, + execution.setError( + error: Error(withMessage: "Cannot evaluate argument \(arg_idx): \(argument)")) + ) + } + execution = execution.declare(identifier: parameter.name, withValue: argument_value) + } + // Evaluate until the state is either accept or reject. while !current_state.done() && !execution.hasError() { (current_state, execution) = current_state.execute(program: execution) } - return (AsInstantiatedParserState(current_state.state()), execution) + + return (AsInstantiatedParserState(current_state.state()), execution.exit_scope()) } } diff --git a/Sources/P4Runtime/Protocols.swift b/Sources/P4Runtime/Protocols.swift index e383a20..01a6bc3 100644 --- a/Sources/P4Runtime/Protocols.swift +++ b/Sources/P4Runtime/Protocols.swift @@ -34,6 +34,7 @@ public protocol EvaluatableParserState: P4Value { func state() -> ParserState } -public protocol ParserExecution { - func execute(execution: ProgramExecution) -> (InstantiatedParserState, ProgramExecution) +public protocol CallableExecution { + associatedtype T + func call(execution: ProgramExecution, arguments: ArgumentList) -> (T, ProgramExecution) } diff --git a/Sources/P4Runtime/Runtime.swift b/Sources/P4Runtime/Runtime.swift index a4abbf9..e2f254e 100644 --- a/Sources/P4Runtime/Runtime.swift +++ b/Sources/P4Runtime/Runtime.swift @@ -49,8 +49,14 @@ public struct ParserRuntime: CustomStringConvertible { } } - /// Run the P4 parser on a given packet + /// Run a P4 parser with no arguments public func run() -> Result<(ParserState, ProgramExecution)> { + return self.run(withArguments: ArgumentList([])) + } + + /// Run the P4 parser on a given packet + public func run(withArguments arguments: ArgumentList) -> Result<(ParserState, ProgramExecution)> + { let pe = if let initial = initialValues { @@ -59,7 +65,7 @@ public struct ParserRuntime: CustomStringConvertible { ProgramExecution() } - let (end_state, execution) = parser.execute(execution: pe) + let (end_state, execution) = parser.call(execution: pe, arguments: arguments) if let error = execution.getError() { return .Error(error) } diff --git a/Tests/p4rseTests/ExpressionTests/SelectExpression.swift b/Tests/p4rseTests/ExpressionTests/SelectExpression.swift index 455228a..95bb15c 100644 --- a/Tests/p4rseTests/ExpressionTests/SelectExpression.swift +++ b/Tests/p4rseTests/ExpressionTests/SelectExpression.swift @@ -160,3 +160,45 @@ import TreeSitterP4 #expect(AsInstantiatedParserState(state_result) == P4Lang.reject) } + +@Test func test_select_expression_from_parser_parameters() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + transition select (pmtr) { + true: accept; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let runtime = try #UseOkResult(P4Runtime.ParserRuntime.create(program: program)) + + let args = ArgumentList([ + P4BooleanValue(withValue: false), P4StringValue(withValue: "Testing"), P4IntValue(withValue: 5), + ]) + let (state_result, _) = try! #UseOkResult(runtime.run(withArguments: args)) + #expect(AsInstantiatedParserState(state_result) == P4Lang.reject) +} + +@Test func test_select_expression_from_parser_parameters2() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + transition select (imtr == 5) { + true: accept; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let runtime = try #UseOkResult(P4Runtime.ParserRuntime.create(program: program)) + + let args = ArgumentList([ + P4BooleanValue(withValue: false), P4StringValue(withValue: "Testing"), P4IntValue(withValue: 5), + ]) + let (state_result, _) = try! #UseOkResult(runtime.run(withArguments: args)) + #expect(AsInstantiatedParserState(state_result) == P4Lang.accept) +} \ No newline at end of file diff --git a/Tests/p4rseTests/ParserCompilerTests.swift b/Tests/p4rseTests/ParserCompilerTests.swift index 6300ce2..b71d605 100644 --- a/Tests/p4rseTests/ParserCompilerTests.swift +++ b/Tests/p4rseTests/ParserCompilerTests.swift @@ -169,3 +169,16 @@ import P4Lang #expect(parameters.parameters[2].name == Identifier(name: "imtr")) #expect(parameters.parameters[2].type.eq(rhs: P4Int())) } + +@Test func test_simple_compiler_parser_use_parameters() async throws { + let simple = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + pmtr = true; + transition accept; + } + }; + """ + + #expect(#RequireOkResult(Program.Compile(simple))) +} diff --git a/Tests/p4rseTests/RuntimeTests.swift b/Tests/p4rseTests/RuntimeTests.swift index 1be72c8..5ef46d3 100644 --- a/Tests/p4rseTests/RuntimeTests.swift +++ b/Tests/p4rseTests/RuntimeTests.swift @@ -80,3 +80,94 @@ import TreeSitterP4 Error(withMessage: "Could not find the start state"), runtime.run())) } + +@Test func test_simple_runtime_parser_with_parameters() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + transition select (pmtr) { + true: accept; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let runtime = try #UseOkResult(P4Runtime.ParserRuntime.create(program: program)) + + let args = ArgumentList([ + P4BooleanValue(withValue: true), P4StringValue(withValue: "Testing"), P4IntValue(withValue: 5), + ]) + let (state_result, _) = try! #UseOkResult(runtime.run(withArguments: args)) + // We should be in the accept state. + #expect(AsInstantiatedParserState(state_result) == P4Lang.accept) +} + +@Test func test_simple_runtime_parser_with_mismatched_parameter_types() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + transition select (pmtr) { + true: accept; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let runtime = try #UseOkResult(P4Runtime.ParserRuntime.create(program: program)) + + let args = ArgumentList([ + P4BooleanValue(withValue: true), P4BooleanValue(withValue: false), P4IntValue(withValue: 5), + ]) + + #expect( + #RequireErrorResult<(ParserState, ProgramExecution)>( + Error(withMessage: "Cannot call parser: Argument 2's type (Boolean) is incompatible with the parameter type (String)"), + runtime.run(withArguments: args))) +} + +@Test func test_simple_runtime_parser_with_mismatched_parameter_types2() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + transition select (pmtr) { + true: accept; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let runtime = try #UseOkResult(P4Runtime.ParserRuntime.create(program: program)) + + let args = ArgumentList([ + P4IntValue(withValue: 5), P4StringValue(withValue: "Testing"), P4IntValue(withValue: 5), + ]) + + #expect( + #RequireErrorResult<(ParserState, ProgramExecution)>( + Error(withMessage: "Cannot call parser: Argument 1's type (Int) is incompatible with the parameter type (Boolean)"), + runtime.run(withArguments: args))) +} + +@Test func test_simple_runtime_parser_with_mismatched_parameter_counts() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + transition select (pmtr) { + true: accept; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let runtime = try #UseOkResult(P4Runtime.ParserRuntime.create(program: program)) + let args = ArgumentList([P4BooleanValue(withValue: true)]) + + #expect( + #RequireErrorResult<(ParserState, ProgramExecution)>( + Error(withMessage: "Cannot call parser: 1 arguments found but 3 required"), + runtime.run(withArguments: args))) +} diff --git a/Tests/p4rseTests/ValueTypeParserTests.swift b/Tests/p4rseTests/ValueTypeParserTests.swift index 58eef72..de6155a 100644 --- a/Tests/p4rseTests/ValueTypeParserTests.swift +++ b/Tests/p4rseTests/ValueTypeParserTests.swift @@ -262,3 +262,20 @@ import TreeSitterP4 ), Program.Compile(simple_parser_declaration, withGlobalInstances: test_types))) } + +@Test func test_simple_compiler_parser_parameters_invalid_types() async throws { + let simple_parser_declaration = """ + parser main_parser(bool pmtr, string smtr, int imtr) { + state start { + pmtr = 1; + transition accept; + } + }; + """ + #expect( + #RequireErrorResult( + Error( + withMessage: "{85, 9}: Failed to parse a statement element: {85, 4}: Cannot assign value with type Int to identifier pmtr with type Boolean" + ), + Program.Compile(simple_parser_declaration))) +}