p4_macro/
lib.rs

1// Copyright 2026 Oxide Computer Company
2
3//! The [`use_p4!`] macro allows for P4 programs to be directly integrated into
4//! Rust programs.
5//!
6//! ```ignore
7//! p4_macro::use_p4!("path/to/p4/program.p4");
8//! ```
9//!
10//! This will generate a `main_pipeline` struct that implements the
11//! [Pipeline](../p4rs/trait.Pipeline.html) trait. The [`use_p4!`] macro expands
12//! directly in to `x4c` compiled code. This includes all data structures,
13//! parsers and control blocks.
14//!
15//! To customize the name of the generated pipeline use the `pipeline_name`
16//! parameter.
17//!
18//! ```ignore
19//! p4_macro::use_p4!(p4 = "path/to/p4/program.p4", pipeline_name = "muffin");
20//! ```
21//! This will result in a `muffin_pipeline` struct being being generated.
22//!
23//! For documentation on using [Pipeline](../p4rs/trait.Pipeline.html) trait, see the
24//! [p4rs](../p4rs/index.html) docs.
25
26use std::collections::HashMap;
27use std::fs;
28use std::path::Path;
29use std::sync::Arc;
30
31use p4::check::Diagnostics;
32use p4::{
33    ast::AST, check, error, error::SemanticError, lexer, parser, preprocessor,
34};
35use proc_macro::TokenStream;
36use serde::Deserialize;
37use serde_tokenstream::ParseWrapper;
38use syn::{parse, LitStr};
39
40#[derive(Deserialize)]
41struct MacroSettings {
42    p4: ParseWrapper<LitStr>,
43    pipeline_name: ParseWrapper<LitStr>,
44}
45
46struct GenerationSettings {
47    pipeline_name: String,
48}
49
50impl Default for GenerationSettings {
51    fn default() -> Self {
52        Self {
53            pipeline_name: "main".to_owned(),
54        }
55    }
56}
57
58/// The `use_p4!` macro uses the `x4c` compiler to generate Rust code from a P4
59/// program. The macro itself expands into the generated code. The macro can be
60/// called with only the path to the P4 program as an argument or, it can be
61/// called with the path to the P4 program plus the name to use for the
62/// generated pipeline object.
63///
64/// For usage examples, see the [p4-macro](index.html) module documentation.
65#[proc_macro]
66pub fn use_p4(item: TokenStream) -> TokenStream {
67    match do_use_p4(item) {
68        Err(err) => err.to_compile_error().into(),
69        Ok(out) => out,
70    }
71}
72
73fn do_use_p4(item: TokenStream) -> Result<TokenStream, syn::Error> {
74    let (filename, settings) =
75        if let Ok(filename) = parse::<LitStr>(item.clone()) {
76            (filename.value(), GenerationSettings::default())
77        } else {
78            let MacroSettings { p4, pipeline_name } =
79                serde_tokenstream::from_tokenstream(&item.into())?;
80            (
81                p4.into_inner().value(),
82                GenerationSettings {
83                    pipeline_name: pipeline_name.into_inner().value(),
84                },
85            )
86        };
87
88    generate_rs(filename, settings).map(Into::into)
89}
90
91fn generate_rs(
92    filename: String,
93    settings: GenerationSettings,
94) -> Result<proc_macro2::TokenStream, syn::Error> {
95    //TODO gracefull error handling
96
97    let mut ast = AST::default();
98    let mut sources = HashMap::new();
99    process_file(Arc::new(filename), &mut ast, &mut sources)?;
100    p4_rust::sanitize(&mut ast);
101
102    let (hlir, diags) = check::all(&ast);
103    check(&sources, &diags)?;
104
105    let tokens = p4_rust::emit_tokens(
106        &ast,
107        &hlir,
108        p4_rust::Settings {
109            pipeline_name: settings.pipeline_name.clone(),
110        },
111    );
112
113    Ok(tokens)
114}
115
116fn process_file(
117    filename: Arc<String>,
118    ast: &mut AST,
119    sources: &mut HashMap<Arc<String>, Vec<String>>,
120) -> Result<(), syn::Error> {
121    let contents = match fs::read_to_string(&*filename) {
122        Ok(c) => c,
123        Err(e) => panic!("failed to read file {}: {}", filename, e),
124    };
125    let ppr = preprocessor::run(&contents, filename.clone()).unwrap();
126    for included in &ppr.elements.includes {
127        let path = Path::new(included);
128        if !path.is_absolute() {
129            let parent = Path::new(&*filename).parent().unwrap();
130            let joined = parent.join(included);
131            process_file(
132                Arc::new(joined.to_str().unwrap().to_string()),
133                ast,
134                sources,
135            )?
136        } else {
137            process_file(Arc::new(included.clone()), ast, sources)?;
138        }
139    }
140
141    let lines: Vec<&str> = ppr.lines.iter().map(|x| x.as_str()).collect();
142    let lxr = lexer::Lexer::new(lines, filename.clone());
143    let mut psr = parser::Parser::new(lxr);
144    psr.run(ast).unwrap();
145    sources.insert(filename, ppr.lines);
146    Ok(())
147}
148
149// TODO copy pasta from x4c
150fn check(
151    sources: &HashMap<Arc<String>, Vec<String>>,
152    diagnostics: &Diagnostics,
153) -> Result<(), syn::Error> {
154    let errors = diagnostics.errors();
155    if !errors.is_empty() {
156        let mut err = Vec::new();
157        for e in errors {
158            err.push(SemanticError {
159                at: e.token.clone(),
160                message: e.message.clone(),
161                source: sources
162                    .get(&e.token.file)
163                    .and_then(|lines| lines.get(e.token.line))
164                    .cloned()
165                    .unwrap_or_default(),
166            });
167        }
168        return Err(syn::Error::new(
169            proc_macro2::Span::call_site(),
170            error::Error::Semantic(err).to_string(),
171        ));
172    }
173    Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn generate(path: &Path) -> Result<proc_macro2::TokenStream, syn::Error> {
181        let settings = GenerationSettings::default();
182        generate_rs(path.to_str().unwrap().into(), settings)
183    }
184
185    #[test]
186    fn macro_rejects_width_in_final_file() {
187        let dir = tempfile::tempdir().unwrap();
188        let path = dir.path().join("wide.p4");
189        fs::write(&path, "header wide_t {\n    bit<129> field;\n}\n").unwrap();
190
191        let error = generate(&path).expect_err("bit<129> accepted").to_string();
192
193        assert!(error.contains("Width 129 exceeds the 128-bit compiler limit"));
194        assert!(error.contains("bit<129> field;"));
195    }
196
197    #[test]
198    fn macro_diagnostic_uses_included_source() {
199        let dir = tempfile::tempdir().unwrap();
200        let path = dir.path().join("main.p4");
201        let included = dir.path().join("wide.p4");
202        fs::write(&path, "#include <wide.p4>\n").unwrap();
203        fs::write(
204            &included,
205            "\n\n\nheader wide_t {\n    bit<129> included_field;\n}\n",
206        )
207        .unwrap();
208
209        let error = generate(&path).expect_err("bit<129> accepted").to_string();
210
211        assert!(error.contains("Width 129 exceeds the 128-bit compiler limit"));
212        assert!(error.contains("bit<129> included_field;"));
213        assert!(error.contains(included.to_str().unwrap()));
214    }
215
216    #[test]
217    fn macro_checks_complete_program_after_includes() {
218        let dir = tempfile::tempdir().unwrap();
219        let path = dir.path().join("main.p4");
220        fs::write(
221            &path,
222            "#include <structs.p4>\nheader header_t {\n    bit<8> field;\n}\n",
223        )
224        .unwrap();
225        fs::write(
226            dir.path().join("structs.p4"),
227            "struct headers_t {\n    header_t hdr;\n}\n",
228        )
229        .unwrap();
230
231        generate(&path).expect("complete program rejected");
232    }
233}