diff --git a/README.md b/README.md index 353fc3f..e04954f 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ where `` is one of: 5. `documentation`: For any documentation updates. 6. `testing`: For Swift-based tests. 7. `cli`: For Cli components. +7. `codegen`: For code generation components. where `` can be more free-form and `` is a pithy description of the changes in the commit. @@ -143,4 +144,4 @@ To check the format: ```console $ swift-format --recursive -i Sources/ -``` \ No newline at end of file +``` diff --git a/Sources/Cli/main.swift b/Sources/Cli/main.swift index 8ef0210..662a29d 100644 --- a/Sources/Cli/main.swift +++ b/Sources/Cli/main.swift @@ -18,6 +18,7 @@ import ArgumentParser import Common import P4Compiler +import P4Runtime import SystemPackage @main @@ -26,7 +27,7 @@ struct Cli: ParsableCommand { static let configuration = CommandConfiguration( abstract: "P4CE compiler, interpreter and debugger.", - subcommands: [Compile.self]) + subcommands: [Compile.self, CodeGen.self]) } struct CliOptions: ParsableArguments { @@ -50,30 +51,72 @@ extension Cli { let prep = SourceCodePreprocessor(sm) let file = FilePath(options.path) - let maybe_source = prep.preprocess(file) - guard case .Ok(let source) = maybe_source else { - let formatter = FormatterAnsi() - print(ErrorWithLabel("Preprocessor Error", maybe_source.error()!).format(formatter)) - return - } - - let maybe_program = Program.Compile(source.getSource()) - guard case .Ok(_) = maybe_program else { - let formatter = FormatterAnsi() - print(ErrorWithLabel("Compiler Error", maybe_source.error()!).format(formatter)) - return - } - - let success_formatter: any Formattable = + let formatter: any Formattable = if parent.plain != 0 { FormatterPlain() } else { FormatterAnsi() } + let maybe_source = prep.preprocess(file) + guard case .Ok(let source) = maybe_source else { + print(ErrorWithLabel("Preprocessor Error", maybe_source.error()!).format(formatter)) + return + } + + let maybe_program = Program.Compile(source.getSource()) + guard case .Ok(_) = maybe_program else { + print(ErrorWithLabel("Compiler Error", maybe_source.error()!).format(formatter)) + return + } + print( - success_formatter.formatWithStyle( + formatter.formatWithStyle( "Success", Style(StyleColor.Green, [StyleFormat.Underline]))) } } } + +extension Cli { + struct CodeGen: ParsableCommand { + static let configuration = CommandConfiguration(abstract: "Generate P4CE code.") + + @ParentCommand var parent: Cli + + @OptionGroup var options: CliOptions + + mutating func run() { + let sm = SourceManager(options.search.map { FilePath($0) }) + let prep = SourceCodePreprocessor(sm) + let file = FilePath(options.path) + + let formatter: any Formattable = + if parent.plain != 0 { + FormatterPlain() + } else { + FormatterAnsi() + } + + let maybe_source = prep.preprocess(file) + guard case .Ok(let source) = maybe_source else { + print(ErrorWithLabel("Preprocessor Error", maybe_source.error()!).format(formatter)) + return + } + + let maybe_program = Program.Compile(source.getSource()) + guard case .Ok(let program) = maybe_program else { + print(ErrorWithLabel("Compiler Error", maybe_source.error()!).format(formatter)) + return + } + + let maybe_codegen = P4Runtime.CodeGenerator().codeGen(program) + guard case .Ok(let codegen) = maybe_codegen else { + let formatter = FormatterAnsi() + print(ErrorWithLabel("Code Generation Error", maybe_codegen.error()!).format(formatter)) + return + } + + print("\(codegen.getGeneratedCode())") + } + } +} diff --git a/Sources/Common/Support.swift b/Sources/Common/Support.swift index 84d5846..b1e2be5 100644 --- a/Sources/Common/Support.swift +++ b/Sources/Common/Support.swift @@ -73,6 +73,14 @@ public func Map(input: T, block: (T) -> U) -> U { return block(input) } +public func Fold(input: [T], initial: A, block: (T, A) -> A) -> A { + var result = initial + for i in input { + result = block(i, result) + } + return result +} + @freestanding(expression) public macro RequireOkResult(_: Result) -> Bool = #externalMacro(module: "Macros", type: "RequireResult") @freestanding(expression) public macro RequireErrorResult( diff --git a/Sources/P4Lang/Common.swift b/Sources/P4Lang/Common.swift index e69de29..98bbbde 100644 --- a/Sources/P4Lang/Common.swift +++ b/Sources/P4Lang/Common.swift @@ -0,0 +1,95 @@ +// 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 protocol Visitable { + +} + +public protocol LanguageVisitor { + associatedtype Context + + // Program + func visit(_ program: Program, _ c: VisitorContext) -> Result> + + // Parser + func visit(_ parser: Parser, _ c: VisitorContext) -> Result> + func visit( + _ parser_state: InstantiatedParserState, _ c: VisitorContext + ) -> Result> + + // Statements + func visit( + _ variable_declaration: VariableDeclarationStatement, _ c: VisitorContext + ) -> Result> + func visit( + _ conditional: ConditionalStatement, _ c: VisitorContext + ) -> Result> + func visit( + _ block: BlockStatement, _ c: VisitorContext + ) -> Result> + func visit( + _ rtn: ReturnStatement, _ c: VisitorContext + ) -> Result> + func visit( + _ apply: ApplyStatement, _ c: VisitorContext + ) -> Result> + + // Expressions + func visit( + _ keyset: KeysetExpression, _ c: VisitorContext + ) -> Result> + func visit( + _ select_case: SelectCaseExpression, _ c: VisitorContext + ) -> Result> + func visit( + _ select: SelectExpression, _ c: VisitorContext + ) -> Result> + func visit( + _ array_access: ArrayAccessExpression, _ c: VisitorContext + ) -> Result> + func visit( + _ field_access: FieldAccessExpression, _ c: VisitorContext + ) -> Result> + func visit( + _ function_call: FunctionCall, _ c: VisitorContext + ) -> Result> + func visit( + _ binary_operator: BinaryOperatorExpression, _ c: VisitorContext + ) -> Result> + + // Declarations + func visit(_ decl: Declaration, _ c: VisitorContext) -> Result> + func visit( + _ extern_decl: ExternDeclaration, _ c: VisitorContext + ) -> Result> + func visit( + _ func_decl: FunctionDeclaration, _ c: VisitorContext + ) -> Result> + + // Control + func visit(_ action: Action, _ c: VisitorContext) -> Result> + func visit( + _ table_key_entry: TableKeyEntry, _ c: VisitorContext + ) -> Result> + func visit( + _ table_property_list: TablePropertyList, _ c: VisitorContext + ) -> Result> + func visit(_ table: Table, _ c: VisitorContext) -> Result> + func visit(_ control: Control, _ c: VisitorContext) -> Result> +} diff --git a/Sources/P4Lang/Visitor.swift b/Sources/P4Lang/Visitor.swift new file mode 100644 index 0000000..a3a9117 --- /dev/null +++ b/Sources/P4Lang/Visitor.swift @@ -0,0 +1,130 @@ +// 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 + +// Witness the parts of a P4 Program that are visitable. +extension Program: Visitable {} +extension Parser: Visitable {} +extension ParserState: Visitable {} +extension VariableDeclarationStatement: Visitable {} +extension ConditionalStatement: Visitable {} +extension BlockStatement: Visitable {} +extension ReturnStatement: Visitable {} +extension ApplyStatement: Visitable {} +extension KeysetExpression: Visitable {} +extension SelectCaseExpression: Visitable {} +extension SelectExpression: Visitable {} +extension ArrayAccessExpression: Visitable {} +extension FieldAccessExpression: Visitable {} +extension FunctionCall: Visitable {} +extension BinaryOperatorExpression: Visitable {} +extension Declaration: Visitable {} +extension ExternDeclaration: Visitable {} +extension FunctionDeclaration: Visitable {} +extension Action: Visitable {} +extension TableKeyEntry: Visitable {} +extension TablePropertyList: Visitable {} +extension Table: Visitable {} +extension Control: Visitable {} + +/// Context for the visiting process. +public struct VisitorContext { + let visitor: VisitorDriver + let uc: UserContext + public init(_ v: VisitorDriver, _ uc: UserContext) { + self.visitor = v + self.uc = uc + } + + public func getUserContext() -> UserContext { + return self.uc + } + + public func getVisitorDriver() -> VisitorDriver { + return self.visitor + } + + public func next(uc: UserContext) -> VisitorContext { + return VisitorContext(self.visitor, uc) + } +} + +/// A driver for visiting the components of a parsed P4 program. +public struct VisitorDriver { + let visitor: any LanguageVisitor + + public init(_ visitor: any LanguageVisitor) { + self.visitor = visitor + } + + public func generateContext(uc: UserContext) -> VisitorContext { + return VisitorContext(self, uc) + } + + /// Visit a `P4Type`. + public func visit( + _ t: P4Type, context: VisitorContext + ) -> Result> { + return switch t { + case let vv as Control: visitor.visit(vv, context) + case let vv as Parser: visitor.visit(vv, context) + default: .Error(Error(withMessage: "Could not visit type \(t)")) + } + } + + /// Visit a part of a P4 program. + public func visit( + _ v: Visitable, context: VisitorContext + ) -> Result> { + return switch v { + case let vv as Program: visitor.visit(vv, context) + case let vv as Parser: visitor.visit(vv, context) + case let vv as InstantiatedParserState: visitor.visit(vv, context) + case let vv as VariableDeclarationStatement: visitor.visit(vv, context) + case let vv as ConditionalStatement: visitor.visit(vv, context) + case let vv as BlockStatement: visitor.visit(vv, context) + case let vv as ReturnStatement: visitor.visit(vv, context) + case let vv as ApplyStatement: visitor.visit(vv, context) + case let vv as KeysetExpression: visitor.visit(vv, context) + case let vv as SelectCaseExpression: visitor.visit(vv, context) + case let vv as SelectExpression: visitor.visit(vv, context) + case let vv as ArrayAccessExpression: visitor.visit(vv, context) + case let vv as FieldAccessExpression: visitor.visit(vv, context) + case let vv as FunctionCall: visitor.visit(vv, context) + case let vv as BinaryOperatorExpression: visitor.visit(vv, context) + case let vv as Declaration: visitor.visit(vv, context) + case let vv as ExternDeclaration: visitor.visit(vv, context) + case let vv as FunctionDeclaration: visitor.visit(vv, context) + case let vv as Action: visitor.visit(vv, context) + case let vv as TableKeyEntry: visitor.visit(vv, context) + case let vv as TablePropertyList: visitor.visit(vv, context) + case let vv as Table: visitor.visit(vv, context) + case let vv as Control: visitor.visit(vv, context) + default: .Error(Error(withMessage: "Could not visit \(v)")) + } + } + + /// Start the process of visiting a P4 program. + public func start(_ v: Visitable, context: UserContext) -> Result { + let visit_result = self.visit(v, context: VisitorContext(self, context)) + return switch visit_result { + case .Ok(let vc): .Ok(vc.getUserContext()) + case .Error(let e): .Error(e) + } + } +} diff --git a/Sources/P4Runtime/CodeGen.swift b/Sources/P4Runtime/CodeGen.swift new file mode 100644 index 0000000..a908a5e --- /dev/null +++ b/Sources/P4Runtime/CodeGen.swift @@ -0,0 +1,285 @@ +// 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 + +public struct Generated { + let gen: String + public init(_ base: String = "") { + self.gen = base + } + + public func getGeneratedCode() -> String { + return self.gen + } + + public func append(_ a: String) -> Generated { + return Generated(self.gen + a) + } +} + +/// Generate code for a P4 program. +/// +/// See the [P4 behavioral model](https://github.com/p4lang/behavioral-model) for +/// the format. +public struct CodeGenerator: LanguageVisitor { + + public init() {} + + /// Generate code. + public func codeGen(_ node: any Visitable) -> Result { + let visitor = VisitorDriver(self) + let generated = Generated() + return visitor.start(node, context: generated) + } + + /// TODO: Can we generate these implementations somehow? + + public typealias Context = Generated + + public func visit( + _ v: Program, _ c: VisitorContext + ) -> Result> { + var result: Result> = Fold( + input: v.types, initial: .Ok(c.next(uc: c.getUserContext().append("["))) + ) { (current, acc) in + return switch acc { + case .Ok(let acc): acc.getVisitorDriver().visit(current, context: acc) + case .Error(let e): .Error(e) + } + } + + result = Fold( + input: v.externs, initial: result + ) { (current, acc) in + return switch acc { + case .Ok(let acc): acc.getVisitorDriver().visit(current, context: acc) + case .Error(let e): .Error(e) + } + } + + result = Fold( + input: v.instances, initial: result + ) { (current, acc) in + return switch acc { + case .Ok(let acc): acc.getVisitorDriver().visit(current.baseType(), context: acc) + case .Error(let e): .Error(e) + } + } + + result = result.map { + .Ok($0.next(uc: $0.getUserContext().append("]"))) + } + + return result + } + + public func visit( + _ v: Parser, _ c: VisitorContext + ) -> Result> { + var initial = "{" + initial += "name: \"\(v.name)\"," + initial += "init_state: \"start\"," + initial += "parse_states: [" + + let result: Result> = Fold( + input: v.states.states, initial: .Ok(c.next(uc: c.getUserContext().append(initial))) + ) { (current, acc) in + return switch acc { + case .Ok(let acc): + switch self.visit(current, acc) { + case .Ok(let gend): .Ok(gend.next(uc: gend.getUserContext().append(","))) + case .Error(let e): .Error(e) + } + case .Error(let e): .Error(e) + } + } + + return result.map { + .Ok($0.next(uc: $0.getUserContext().append("]}"))) + } + } + + public func visit( + _ v: InstantiatedParserState, _ c: VisitorContext + ) -> Result> { + let direct_transition_codegen = { + ( + _: ParserStateDirectTransition, c: VisitorContext + ) -> Result> in + return .Ok(c.next(uc: c.getUserContext().append("[]"))) + } + + let no_transition_codegen = { + ( + _: ParserStateNoTransition, c: VisitorContext + ) -> Result> in + return .Ok(c.next(uc: c.getUserContext().append("[]"))) + } + + let select_transition_codegen = { + ( + state: ParserStateSelectTransition, c: VisitorContext + ) -> Result> in + return switch self.visit(state.selectExpression, c.next(uc: c.getUserContext().append("["))) { + case .Ok(let res): .Ok(res.next(uc: res.getUserContext().append("]"))) + case .Error(let e): .Error(e) + } + } + + var initial = "{" + initial += "name: \"\(v.state)\"," + initial += "transitions: " + + let result: Result> = + switch v { + case let s as ParserStateSelectTransition: + select_transition_codegen(s, c.next(uc: c.getUserContext().append(initial))) + case let s as ParserStateDirectTransition: + direct_transition_codegen(s, c.next(uc: c.getUserContext().append(initial))) + case let s as ParserStateNoTransition: + no_transition_codegen(s, c.next(uc: c.getUserContext().append(initial))) + default: + .Error(Error(withMessage: "Could not code gen \(self)")) + } + + return result.map { + .Ok($0.next(uc: $0.getUserContext().append("}"))) + } + } + + public func visit( + _ v: VariableDeclarationStatement, _ c: VisitorContext + ) -> Result> { + + return .Ok(c) + } + + public func visit( + _ v: ConditionalStatement, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: BlockStatement, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: ReturnStatement, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: ApplyStatement, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: KeysetExpression, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: SelectCaseExpression, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: SelectExpression, _ c: VisitorContext + ) -> Result> { + return .Ok(c.next(uc: c.getUserContext().append("Select"))) + } + + public func visit( + _ v: ArrayAccessExpression, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: FieldAccessExpression, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: FunctionCall, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: BinaryOperatorExpression, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: Declaration, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: ExternDeclaration, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: FunctionDeclaration, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: Action, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: TableKeyEntry, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit( + _ v: TablePropertyList, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } + + public func visit(_ v: Table, _ c: VisitorContext) -> Result> + { + return .Ok(c) + } + + public func visit( + _ v: Control, _ c: VisitorContext + ) -> Result> { + return .Ok(c) + } +} diff --git a/Tests/p4rseTests/CodeGenTests/CodeGen.swift b/Tests/p4rseTests/CodeGenTests/CodeGen.swift new file mode 100644 index 0000000..134fb82 --- /dev/null +++ b/Tests/p4rseTests/CodeGenTests/CodeGen.swift @@ -0,0 +1,98 @@ +// 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 P4Compiler +import P4Lang +import P4Runtime +import SwiftTreeSitter +import Testing +import TreeSitter +import TreeSitterP4 + +@testable import P4Runtime + +func shrink(_ from: String) -> String { + return from.replacing(Regex(#/\n[\s]+/#), with: "").replacing("\n", with: "") +} + +@Test func test_codegen_parser_state_direct_transition() async throws { + let simple_parser_declaration = """ + parser main_parser() { + state start { + true; + transition start; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let cg = try #UseOkResult(CodeGenerator().codeGen(program)) + + let expected = shrink( + """ + [ + { + name: "main_parser", + init_state: "start", + parse_states: [ + { + name: "start", + transitions: [] + }, + ] + } + ] + """) + + #expect(expected == cg.getGeneratedCode()) +} + +@Test func test_codegen_parser_state_select_transition() async throws { + let simple_parser_declaration = """ + parser main_parser() { + state start { + transition select (false) { + true: reject; + false: reject; + }; + } + }; + """ + let program = try #UseOkResult(Program.Compile(simple_parser_declaration)) + let cg = try #UseOkResult(CodeGenerator().codeGen(program)) + + /// TODO: Fix this test. + let expected = shrink( + """ + [ + { + name: "main_parser", + init_state: "start", + parse_states: [ + { + name: "start", + transitions: [Select] + }, + ] + } + ] + """) + + #expect(expected == cg.getGeneratedCode()) +}