1use 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
16const 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
49struct Options {
51 options: HashMap<String, (Ident, Expr)>,
52}
53
54impl Options {
55 fn validate(&self) -> Result<(), parse::Error> {
56 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 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 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 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 let sp: SelectProgram = stream.parse()?;
214 select_program = Some(sp);
215 let _trailing_comma: Option<Token![,]> = stream.parse().ok();
216 }
217 "options" => {
218 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 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 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 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 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 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#[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 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}