Skip to main content

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