pio_proc/
lib.rs

1//! This crate is an implementation detail, you must not use it directly.
2//! Use the [`pio`](https://crates.io/crates/pio) crate instead.
3
4use lalrpop_util::ParseError;
5use proc_macro::TokenStream;
6use proc_macro2::Span;
7use proc_macro_error2::{abort, abort_call_site, proc_macro_error};
8use quote::quote;
9use std::collections::HashMap;
10use std::fmt::Write;
11use std::fs;
12use std::path::{Path, PathBuf};
13use syn::{
14    parenthesized, parse, parse_macro_input, Expr, ExprLit, Ident, Lit, LitInt, LitStr, Token,
15};
16
17/// Maximum program size supported by the macro.
18///
19/// As the program size is limited to 32 instructions on the currently available hardware as of 2021, 1024 instructions
20/// should be plenty for a while.
21const MAX_PROGRAM_SIZE: usize = 1024;
22
23struct OptionsArgs {
24    ident: Ident,
25    expr: Expr,
26}
27
28impl syn::parse::Parse for OptionsArgs {
29    fn parse(stream: syn::parse::ParseStream) -> syn::parse::Result<Self> {
30        let ident = stream.parse()?;
31        let _equals: Token![=] = stream.parse()?;
32        let expr = stream.parse()?;
33
34        Ok(Self { ident, expr })
35    }
36}
37
38// Options are on the form Ident = Literal
39struct Options {
40    options: HashMap<String, (Ident, Expr)>,
41}
42
43impl Options {
44    fn validate(&self) -> Result<(), parse::Error> {
45        // NOTE: Add more options here in the future
46        let valid_identifiers = ["max_program_size"];
47
48        for (name, (id, _)) in &self.options {
49            if !valid_identifiers.contains(&name.as_str()) {
50                abort!(
51                    id,
52                    "unknown identifier, expected one of {:?}",
53                    valid_identifiers
54                );
55            }
56        }
57
58        Ok(())
59    }
60
61    fn get_max_program_size_or_default(&self) -> Result<Expr, parse::Error> {
62        if let Some(mps) = self.options.get("max_program_size") {
63            Ok(mps.1.clone())
64        } else {
65            Ok(Expr::Lit(ExprLit {
66                attrs: vec![],
67                lit: Lit::Int(LitInt::new("32", Span::call_site())),
68            }))
69        }
70    }
71}
72
73impl syn::parse::Parse for Options {
74    fn parse(stream: syn::parse::ParseStream) -> parse::Result<Self> {
75        // Parse the optional 'options'
76        let content;
77        parenthesized!(content in stream);
78
79        if !content.is_empty() {
80            let mut options = HashMap::new();
81
82            while !content.is_empty() {
83                let opt: OptionsArgs = content.parse()?;
84                options.insert(opt.ident.to_string(), (opt.ident, opt.expr));
85                let _trailing_comma: Option<Token![,]> = content.parse().ok();
86            }
87
88            let _trailing_comma: Option<Token![,]> = stream.parse().ok();
89
90            let s = Self { options };
91
92            s.validate()?;
93
94            Ok(s)
95        } else {
96            Ok(Self {
97                options: HashMap::new(),
98            })
99        }
100    }
101}
102
103struct SelectProgram {
104    name: String,
105    ident: LitStr,
106}
107
108impl syn::parse::Parse for SelectProgram {
109    fn parse(stream: syn::parse::ParseStream) -> parse::Result<Self> {
110        // Parse the optional 'options'
111        let content;
112        parenthesized!(content in stream);
113
114        let name: LitStr = content.parse::<LitStr>()?;
115
116        Ok(Self {
117            name: name.value(),
118            ident: name,
119        })
120    }
121}
122
123struct PioFileMacroArgs {
124    krate: Ident,
125    max_program_size: Expr,
126    program: String,
127    program_name: Option<(String, LitStr)>,
128    file_path: PathBuf,
129}
130
131impl syn::parse::Parse for PioFileMacroArgs {
132    fn parse(stream: syn::parse::ParseStream) -> syn::parse::Result<Self> {
133        let krate: Ident = stream.parse()?;
134        let _comma: Option<Token![,]> = stream.parse()?;
135
136        let mut program = String::new();
137        let mut file_path = PathBuf::new();
138
139        // Parse the list of instructions
140        if let Ok(s) = stream.parse::<LitStr>() {
141            let path = s.value();
142            let path = Path::new(&path);
143
144            let pathbuf = {
145                let mut p = PathBuf::new();
146
147                if path.is_relative() {
148                    if let Some(crate_dir) = std::env::var_os("CARGO_MANIFEST_DIR") {
149                        p.push(crate_dir);
150                    } else {
151                        abort!(s, "Cannot find 'CARGO_MANIFEST_DIR' environment variable");
152                    }
153                }
154
155                p.push(path);
156
157                p
158            };
159
160            if !pathbuf.exists() {
161                abort!(s, "the file '{}' does not exist", pathbuf.display());
162            }
163
164            file_path = pathbuf.to_owned();
165
166            match fs::read(pathbuf) {
167                Ok(content) => match std::str::from_utf8(&content) {
168                    Ok(prog) => program = prog.to_string(),
169                    Err(e) => {
170                        abort!(s, "could parse file: '{}'", e);
171                    }
172                },
173                Err(e) => {
174                    abort!(s, "could not read file: '{}'", e);
175                }
176            }
177
178            let _trailing_comma: Option<Token![,]> = stream.parse().ok();
179        }
180
181        let mut select_program = None;
182        let mut options = Options {
183            options: HashMap::new(),
184        };
185
186        for _ in 0..2 {
187            if let Ok(ident) = stream.parse::<Ident>() {
188                match ident.to_string().as_str() {
189                    "select_program" => {
190                        // Parse the optional 'select_program'
191                        let sp: SelectProgram = stream.parse()?;
192                        select_program = Some(sp);
193                        let _trailing_comma: Option<Token![,]> = stream.parse().ok();
194                    }
195                    "options" => {
196                        // Parse the optional 'options'
197                        let opt: Options = stream.parse()?;
198                        options = opt;
199                        let _trailing_comma: Option<Token![,]> = stream.parse().ok();
200                    }
201                    _ => abort!(ident, "expected one of 'options' or 'select_program'"),
202                }
203            }
204        }
205
206        if !stream.is_empty() {
207            abort!(stream.span(), "expected end of input");
208        }
209
210        // Validate options
211        let max_program_size = options.get_max_program_size_or_default()?;
212
213        Ok(Self {
214            krate,
215            program_name: select_program.map(|v| (v.name, v.ident)),
216            max_program_size,
217            program,
218            file_path,
219        })
220    }
221}
222
223struct PioAsmMacroArgs {
224    krate: Ident,
225    max_program_size: Expr,
226    program: String,
227}
228
229impl syn::parse::Parse for PioAsmMacroArgs {
230    fn parse(stream: syn::parse::ParseStream) -> syn::parse::Result<Self> {
231        let krate: Ident = stream.parse()?;
232        let _comma: Option<Token![,]> = stream.parse()?;
233
234        let mut program = String::new();
235
236        // Parse the list of instructions
237        while let Ok(s) = stream.parse::<LitStr>() {
238            writeln!(&mut program, "{}", s.value()).unwrap();
239
240            let _trailing_comma: Option<Token![,]> = stream.parse().ok();
241        }
242
243        // Parse the optional 'options'
244
245        let mut options = Options {
246            options: HashMap::new(),
247        };
248
249        if let Ok(ident) = stream.parse::<Ident>() {
250            if ident == "options" {
251                let opt: Options = stream.parse()?;
252                options = opt;
253                let _trailing_comma: Option<Token![,]> = stream.parse().ok();
254            }
255        }
256
257        if !stream.is_empty() {
258            abort!(stream.span(), "expected end of input");
259        }
260
261        // Validate options
262        let max_program_size = options.get_max_program_size_or_default()?;
263
264        Ok(Self {
265            krate,
266            max_program_size,
267            program,
268        })
269    }
270}
271
272#[proc_macro]
273#[proc_macro_error]
274pub fn pio_file_inner(item: TokenStream) -> TokenStream {
275    let args = parse_macro_input!(item as PioFileMacroArgs);
276    let parsed_programs = pio_parser::Parser::<{ MAX_PROGRAM_SIZE }>::parse_file(&args.program);
277    let program = match &parsed_programs {
278        Ok(programs) => {
279            if let Some((program_name, ident)) = args.program_name {
280                if let Some(program) = programs.get(&program_name) {
281                    program
282                } else {
283                    abort! { ident, "program name not found in the provided file" }
284                }
285            } else {
286                // No name provided, check if there is only one in the map
287
288                match programs.len() {
289                    0 => abort_call_site! { "no programs in the provided file" },
290                    1 => programs.iter().next().unwrap().1,
291                    _ => {
292                        abort_call_site! { "more than 1 program in the provided file, select one using `select_program(\"my_program\")`" }
293                    }
294                }
295            }
296        }
297        Err(e) => return parse_error(e, &args.program).into(),
298    };
299
300    to_codegen(
301        args.krate,
302        program,
303        args.max_program_size,
304        Some(
305            args.file_path
306                .into_os_string()
307                .into_string()
308                .expect("file path must be valid UTF-8"),
309        ),
310    )
311    .into()
312}
313
314/// A macro which invokes the PIO assembler at compile time.
315#[proc_macro]
316#[proc_macro_error]
317pub fn pio_asm_inner(item: TokenStream) -> TokenStream {
318    let args = parse_macro_input!(item as PioAsmMacroArgs);
319
320    let parsed_program = pio_parser::Parser::<{ MAX_PROGRAM_SIZE }>::parse_program(&args.program);
321
322    let program = match &parsed_program {
323        Ok(program) => program,
324        Err(e) => return parse_error(e, &args.program).into(),
325    };
326
327    to_codegen(args.krate, program, args.max_program_size, None).into()
328}
329
330fn to_codegen(
331    krate: Ident,
332    program: &pio_core::ProgramWithDefines<HashMap<String, i32>, { MAX_PROGRAM_SIZE }>,
333    max_program_size: Expr,
334    file: Option<String>,
335) -> proc_macro2::TokenStream {
336    let pio_core::ProgramWithDefines {
337        program,
338        public_defines,
339    } = program;
340    if let Expr::Lit(ExprLit {
341        attrs: _,
342        lit: Lit::Int(i),
343    }) = &max_program_size
344    {
345        if let Ok(mps) = i.base10_parse::<usize>() {
346            if program.code.len() > mps {
347                abort_call_site!(
348                    "the resulting program is larger than the maximum allowed: max = {}, size = {}",
349                    mps,
350                    program.code.len()
351                );
352            }
353        }
354    }
355
356    let origin = if let Some(origin) = program.origin {
357        quote!(Some(#origin))
358    } else {
359        quote!(None)
360    };
361
362    let code = &program.code;
363    let code = quote!(
364        ::core::iter::IntoIterator::into_iter([#(#code),*]).collect()
365    );
366
367    let wrap_source = program.wrap.source;
368    let wrap_target = program.wrap.target;
369    let wrap = quote!(
370        #krate::Wrap {source: #wrap_source, target: #wrap_target}
371    );
372
373    let side_set_optional = program.side_set.optional();
374    let side_set_bits = program.side_set.bits();
375    let side_set_pindirs = program.side_set.pindirs();
376    let side_set = quote!(
377        #krate::SideSet::new_from_proc_macro(
378            #side_set_optional,
379            #side_set_bits,
380            #side_set_pindirs,
381        )
382    );
383
384    let version = Ident::new(&format!("{:?}", program.version), Span::call_site());
385    let version = quote!(#krate::PioVersion::#version);
386
387    let defines_fields = public_defines
388        .keys()
389        .map(|k| Ident::new(k, Span::call_site()))
390        .collect::<Vec<_>>();
391    let defines_values = public_defines.values();
392    let defines_struct = quote!(
393        struct ExpandedDefines {
394            #(#defines_fields: i32,)*
395        }
396    );
397    let defines_init = quote!(
398        ExpandedDefines {
399            #(#defines_fields: #defines_values,)*
400        }
401    );
402
403    let program_size = max_program_size;
404
405    // This makes sure the file is added to the list
406    // of tracked files, so a change of that file triggers
407    // a recompile. Should be replaced by
408    // `proc_macro::tracked_path::path` when it is stable.
409    let dummy_include = match file {
410        Some(file_path) => quote! {let _ = include_bytes!( #file_path );},
411        None => quote!(),
412    };
413    quote! {
414        {
415            #defines_struct
416            {
417                #dummy_include;
418                #krate::ProgramWithDefines {
419                    program: #krate::Program::<{ #program_size }> {
420                        code: #code,
421                        origin: #origin,
422                        wrap: #wrap,
423                        side_set: #side_set,
424                        version: #version,
425                    },
426                    public_defines: #defines_init,
427                }
428            }
429        }
430    }
431}
432
433fn parse_error(error: &pio_parser::ParseError, program_source: &str) -> proc_macro2::TokenStream {
434    let e = error;
435    let files = codespan_reporting::files::SimpleFile::new("source", program_source);
436
437    let (loc, messages) = match e {
438        ParseError::InvalidToken { location } => {
439            (*location..*location, vec!["invalid token".to_string()])
440        }
441        ParseError::UnrecognizedEof { location, expected } => (
442            *location..*location,
443            vec![
444                "unrecognized eof".to_string(),
445                format!("expected one of {}", expected.join(", ")),
446            ],
447        ),
448        ParseError::UnrecognizedToken { token, expected } => (
449            token.0..token.2,
450            vec![
451                format!("unexpected token: {:?}", format!("{}", token.1)),
452                format!("expected one of {}", expected.join(", ")),
453            ],
454        ),
455        ParseError::ExtraToken { token } => {
456            (token.0..token.2, vec![format!("extra token: {}", token.1)])
457        }
458        ParseError::User { error } => (0..0, vec![error.to_string()]),
459    };
460
461    let diagnostic = codespan_reporting::diagnostic::Diagnostic::error()
462        .with_message(messages[0].clone())
463        .with_labels(
464            messages
465                .iter()
466                .enumerate()
467                .map(|(i, m)| {
468                    codespan_reporting::diagnostic::Label::new(
469                        if i == 0 {
470                            codespan_reporting::diagnostic::LabelStyle::Primary
471                        } else {
472                            codespan_reporting::diagnostic::LabelStyle::Secondary
473                        },
474                        (),
475                        loc.clone(),
476                    )
477                    .with_message(m)
478                })
479                .collect(),
480        );
481
482    let mut writer = codespan_reporting::term::termcolor::Buffer::ansi();
483    let config = codespan_reporting::term::Config::default();
484    codespan_reporting::term::emit(&mut writer, &config, &files, &diagnostic).unwrap();
485    let data = writer.into_inner();
486    let data = std::str::from_utf8(&data).unwrap();
487
488    quote! {
489        compile_error!(#data)
490    }
491}