diff --git a/esp-hal-procmacros/Cargo.toml b/esp-hal-procmacros/Cargo.toml index c093fd56dc8..44290cccda7 100644 --- a/esp-hal-procmacros/Cargo.toml +++ b/esp-hal-procmacros/Cargo.toml @@ -8,6 +8,8 @@ documentation = "https://docs.espressif.com/projects/rust/esp-hal-procmacros/lat repository = "https://github.com/esp-rs/esp-hal" license = "MIT OR Apache-2.0" +exclude = ["testdata"] + [package.metadata.espressif] doc-config = { features = [] } check-configs = [ diff --git a/esp-hal-procmacros/src/blocking.rs b/esp-hal-procmacros/src/blocking.rs index fabaa42a5bc..b5eae0b8462 100644 --- a/esp-hal-procmacros/src/blocking.rs +++ b/esp-hal-procmacros/src/blocking.rs @@ -1,30 +1,24 @@ #![deny(warnings)] -use proc_macro::TokenStream; -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; use syn::{ ItemFn, parse::{self}, - parse_macro_input, }; /// Marks the firmware entry point. -pub fn main(args: TokenStream, input: TokenStream) -> TokenStream { - let f = parse_macro_input!(input as ItemFn); - +pub fn main(args: TokenStream, f: ItemFn) -> TokenStream { if f.sig.asyncness.is_some() { return parse::Error::new( Span::call_site(), "If you want to use `async` please use `esp-rtos`'s `#[esp_rtos::main]` macro instead.", ) - .to_compile_error() - .into(); + .to_compile_error(); } if !args.is_empty() { return parse::Error::new(Span::call_site(), "This attribute accepts no arguments") - .to_compile_error() - .into(); + .to_compile_error(); } let root = match proc_macro_crate::crate_name("esp-hal") { @@ -37,5 +31,68 @@ pub fn main(args: TokenStream, input: TokenStream) -> TokenStream { #[#root::__macro_implementation::__entry] #f ) - .into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() { + let result = main( + quote::quote! {}.into(), + syn::parse2(quote::quote! { + fn main() {} + }) + .unwrap(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[doc = "The main entry point of the firmware, generated by the `#[main]` macro."] + #[esp_hal::__macro_implementation::__entry] + fn main () { } + } + .to_string() + ); + } + + #[test] + fn test_try_async() { + let result = main( + quote::quote! {}.into(), + syn::parse2(quote::quote! { + async fn main() {} + }) + .unwrap(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "If you want to use `async` please use `esp-rtos`'s `#[esp_rtos::main]` macro instead." } + } + .to_string() + ); + } + + #[test] + fn test_try_non_emptyargs() { + let result = main( + quote::quote! {non_empty}.into(), + syn::parse2(quote::quote! { + fn main() {} + }) + .unwrap(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "This attribute accepts no arguments" } + } + .to_string() + ); + } } diff --git a/esp-hal-procmacros/src/builder.rs b/esp-hal-procmacros/src/builder.rs index 565f1c44f08..2bee573d050 100644 --- a/esp-hal-procmacros/src/builder.rs +++ b/esp-hal-procmacros/src/builder.rs @@ -1,4 +1,4 @@ -use proc_macro::TokenStream; +use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ Attribute, @@ -36,7 +36,7 @@ const KNOWN_HELPERS: &[&str] = &[ /// /// pub fn builder_lite_derive(item: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(item as syn::DeriveInput); + let input: syn::DeriveInput = crate::unwrap_or_compile_error!(syn::parse2(item)); let span = input.span(); let ident = input.ident; @@ -48,13 +48,12 @@ pub fn builder_lite_derive(item: TokenStream) -> TokenStream { span, "#[derive(Builder)] is only defined for structs, not for enums or unions!", ) - .to_compile_error() - .into(); + .to_compile_error(); }; for field in fields { let helper_attributes = match collect_helper_attrs(&field.attrs) { Ok(attr) => attr, - Err(err) => return err.to_compile_error().into(), + Err(err) => return err.to_compile_error(), }; // Ignore field if it has a `skip` helper attribute. @@ -169,7 +168,7 @@ pub fn builder_lite_derive(item: TokenStream) -> TokenStream { } }; - implementation.into() + implementation } // https://stackoverflow.com/a/56264023 @@ -220,3 +219,292 @@ fn collect_helper_attrs(attrs: &[Attribute]) -> Result, syn::Error> { Ok(helper_attributes) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wrong_item() { + let result = builder_lite_derive( + quote::quote! { + fn main() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{"expected one of: `struct`, `enum`, `union`"} + } + .to_string() + ); + } + + #[test] + fn test_wrong_item2() { + let result = builder_lite_derive( + quote::quote! { + enum Foo {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{"#[derive(Builder)] is only defined for structs, not for enums or unions!"} + } + .to_string() + ); + } + + #[test] + fn test_wrong_item_attr() { + let result = builder_lite_derive( + quote::quote! { + struct Foo { + #[builder_lite(foo)] + foo: u32, + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{"Unknown helper attribute `foo`. Only the following are allowed: into, skip, skip_setter, skip_getter, unstable, reference"} + } + .to_string() + ); + } + + #[test] + fn test_basic() { + let result = builder_lite_derive( + quote::quote! { + struct Foo { + #[doc = "keep docs"] + bar: u32, + baz: bool, + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[automatically_derived] + impl Foo { + #[doc = concat!(" Assign the given value to the `" , stringify ! (bar) , "` field.")] + #[must_use] + pub fn with_bar(mut self , bar : u32) -> Self { + self.bar = bar; + self + } + + #[doc = "keep docs"] + pub fn bar(&self) -> u32 { + self.bar + } + + #[doc = concat!(" Assign the given value to the `" , stringify ! (baz) , "` field.")] + #[must_use] + pub fn with_baz(mut self, baz: bool) -> Self { + self.baz = baz; + self + } + + pub fn baz (& self) -> bool { + self.baz + } + } + } + .to_string() + ); + } + + #[test] + fn test_option_field() { + let result = builder_lite_derive( + quote::quote! { + struct Foo { + bar: Option, + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[automatically_derived] + impl Foo { + #[doc = concat!(" Assign the given value to the `" , stringify!(bar) , "` field.")] + #[must_use] + pub fn with_bar(mut self, bar: u32) -> Self { + self.bar = Some(bar); + self + } + + #[doc = concat!(" Set the value of `" , stringify ! (bar) , "` to `None`.")] + #[must_use] + pub fn with_bar_none (mut self) -> Self { + self.bar = None; + self + } + + pub fn bar(&self) -> Option { + self.bar + } + } + } + .to_string() + ); + } + + #[test] + fn test_field_attrs() { + let result = builder_lite_derive( + quote::quote! { + struct Foo { + #[builder_lite(unstable)] + bar: u32, + + #[builder_lite(skip_setter)] + baz: bool, + + #[builder_lite(reference)] + boo: String, + + #[builder_lite(into)] + bam: Foo, + + #[builder_lite(unstable)] + #[builder_lite(into)] + foo: Foo, + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[automatically_derived] + impl Foo { + # [doc = concat ! (" Assign the given value to the `" , stringify ! (bar) , "` field.")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[cfg(feature = "unstable")] + #[must_use] + pub fn with_bar(mut self, bar: u32) -> Self { + self.bar = bar; + self + } + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[cfg(feature = "unstable")] + pub fn bar(&self) -> u32 { + self.bar + } + pub fn baz(&self) -> bool { + self.baz + } + # [doc = concat ! (" Assign the given value to the `" , stringify ! (boo) , "` field.")] + #[must_use] + pub fn with_boo(mut self, boo: String) -> Self { + self.boo = boo; + self + } + pub fn boo(&self) -> &String { + &self.boo + } + # [doc = concat ! (" Assign the given value to the `" , stringify ! (bam) , "` field.")] + #[must_use] + pub fn with_bam(mut self, bam: impl Into) -> Self { + self.bam = bam.into(); + self + } + pub fn bam(&self) -> Foo { + self.bam + } + # [doc = concat ! (" Assign the given value to the `" , stringify ! (foo) , "` field.")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[cfg(feature = "unstable")] + #[must_use] + pub fn with_foo(mut self, foo: impl Into) -> Self { + self.foo = foo.into(); + self + } + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[cfg(feature = "unstable")] + pub fn foo(&self) -> Foo { + self.foo + } + } + } + .to_string() + ); + } + + #[test] + fn test_skip_getter() { + let result = builder_lite_derive( + quote::quote! { + struct Foo { + #[builder_lite(skip_getter)] + bar: Option, + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[automatically_derived] + impl Foo { + #[doc = concat!(" Assign the given value to the `" , stringify!(bar) , "` field.")] + #[must_use] + pub fn with_bar(mut self, bar: u32) -> Self { + self.bar = Some(bar); + self + } + + #[doc = concat!(" Set the value of `" , stringify ! (bar) , "` to `None`.")] + #[must_use] + pub fn with_bar_none (mut self) -> Self { + self.bar = None; + self + } + } + } + .to_string() + ); + } + + #[test] + fn test_skip() { + let result = builder_lite_derive( + quote::quote! { + struct Foo { + #[builder_lite(skip)] + bar: Option, + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[automatically_derived] + impl Foo { + } + } + .to_string() + ); + } +} diff --git a/esp-hal-procmacros/src/doc_replace.rs b/esp-hal-procmacros/src/doc_replace.rs index 89bfcd93ddd..a08cf421fd0 100644 --- a/esp-hal-procmacros/src/doc_replace.rs +++ b/esp-hal-procmacros/src/doc_replace.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{TokenStream, TokenStream as TokenStream2}; use syn::{ AttrStyle, Attribute, @@ -170,12 +169,12 @@ impl Parse for Branch { } pub(crate) fn replace(attr: TokenStream, input: TokenStream) -> TokenStream { - let replacements: Replacements = match syn::parse(attr) { + let replacements: Replacements = match syn::parse2(attr) { Ok(replacements) => replacements, - Err(e) => return e.into_compile_error().into(), + Err(e) => return e.into_compile_error(), }; - let mut item: Item = syn::parse(input).expect("failed to parse input"); + let mut item: Item = crate::unwrap_or_compile_error!(syn::parse2(input)); let mut replacement_attrs = Vec::new(); @@ -197,7 +196,7 @@ pub(crate) fn replace(attr: TokenStream, input: TokenStream) -> TokenStream { *item.attrs_mut() = replacement_attrs; - quote::quote! { #item }.into() + quote::quote! { #item } } trait ItemLike { @@ -217,3 +216,266 @@ impl ItemLike for Item { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() { + let result = replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "# Configuration"] + #[doc = "## Overview"] + #[doc = "This module contains the initial configuration for the system."] + #[doc = "## Configuration"] + #[doc = "In the [`esp_hal::init()`][crate::init] method, we can configure different"] + #[doc = "parameters for the system:"] + #[doc = "- CPU clock configuration."] + #[doc = "- Watchdog configuration."] + #[doc = "## Examples"] + #[doc = "### Default initialization"] + #[doc = "```rust, no_run"] + #[doc = "# {before_snippet}"] + #[doc = "let peripherals = esp_hal::init(esp_hal::Config::default());"] + #[doc = "# {after_snippet}"] + #[doc = "```"] + struct Foo { + } + } + .into(), + ); + + assert_eq!(result.to_string(), quote::quote! { + #[doc = "# Configuration"] + #[doc = "## Overview"] + #[doc = "This module contains the initial configuration for the system."] + #[doc = "## Configuration"] + #[doc = "In the [`esp_hal::init()`][crate::init] method, we can configure different"] + #[doc = "parameters for the system:"] + #[doc = "- CPU clock configuration."] + #[doc = "- Watchdog configuration."] + #[doc = "## Examples"] + #[doc = "### Default initialization"] + #[doc = "```rust, no_run"] + #[doc = crate::before_snippet!()] + #[doc = "let peripherals = esp_hal::init(esp_hal::Config::default());"] + #[doc = crate::after_snippet!()] + #[doc = "```"] + struct Foo {} + }.to_string()); + } + + #[test] + fn test_custom_replacements() { + let result = replace( + quote::quote! { + "freq" => { + cfg(esp32h2) => "let freq = Rate::from_mhz(32);", + _ => "let freq = Rate::from_mhz(80);" + }, + "other" => "replacement" + }.into(), + quote::quote! { + #[doc = "# Configuration"] + #[doc = "## Overview"] + #[doc = "This module contains the initial configuration for the system."] + #[doc = "## Configuration"] + #[doc = "In the [`esp_hal::init()`][crate::init] method, we can configure different"] + #[doc = "parameters for the system:"] + #[doc = "- CPU clock configuration."] + #[doc = "- Watchdog configuration."] + #[doc = "## Examples"] + #[doc = "### Default initialization"] + #[doc = "```rust, no_run"] + #[doc = "# {freq}"] + #[doc = "# {before_snippet}"] + #[doc = "let peripherals = esp_hal::init(esp_hal::Config::default());"] + #[doc = "# {after_snippet}"] + #[doc = "```"] + struct Foo { + } + } + .into(), + ); + + assert_eq!(result.to_string(), quote::quote! { + #[doc = "# Configuration"] + #[doc = "## Overview"] + #[doc = "This module contains the initial configuration for the system."] + #[doc = "## Configuration"] + #[doc = "In the [`esp_hal::init()`][crate::init] method, we can configure different"] + #[doc = "parameters for the system:"] + #[doc = "- CPU clock configuration."] + #[doc = "- Watchdog configuration."] + #[doc = "## Examples"] + #[doc = "### Default initialization"] + #[doc = "```rust, no_run"] + #[cfg_attr (esp32h2 , doc = "let freq = Rate::from_mhz(32);")] + #[cfg_attr (not (any (esp32h2)) , doc = "let freq = Rate::from_mhz(80);")] + #[doc = crate::before_snippet!()] + #[doc = "let peripherals = esp_hal::init(esp_hal::Config::default());"] + #[doc = crate::after_snippet!()] + #[doc = "```"] + struct Foo {} + }.to_string()); + } + + #[test] + fn test_custom_fail() { + let result = replace( + quote::quote! { + "freq" => { + abc(esp32h2) => "let freq = Rate::from_mhz(32);", + }, + } + .into(), + quote::quote! {}.into(), + ); + + assert_eq!(result.to_string(), quote::quote! { + ::core::compile_error!{ "Expected a cfg condition or catch-all condition using `_`" } + }.to_string()); + } + + #[test] + fn test_basic_fn() { + let result = replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "docs"] + #[doc = "# {before_snippet}"] + fn foo() { + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[doc = "docs"] + #[doc = crate::before_snippet!()] + fn foo () { } + } + .to_string() + ); + } + + #[test] + fn test_basic_enum() { + let result = replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "docs"] + #[doc = "# {before_snippet}"] + enum Foo { + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[doc = "docs"] + #[doc = crate::before_snippet!()] + enum Foo { } + } + .to_string() + ); + } + + #[test] + fn test_basic_trait() { + let result = replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "docs"] + #[doc = "# {before_snippet}"] + trait Foo { + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[doc = "docs"] + #[doc = crate::before_snippet!()] + trait Foo { } + } + .to_string() + ); + } + + #[test] + fn test_basic_mod() { + let result = replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "docs"] + #[doc = "# {before_snippet}"] + mod foo { + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[doc = "docs"] + #[doc = crate::before_snippet!()] + mod foo { } + } + .to_string() + ); + } + + #[test] + fn test_basic_macro() { + let result = replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "docs"] + #[doc = "# {before_snippet}"] + macro_rules! foo { + () => { + }; + } + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[doc = "docs"] + #[doc = crate::before_snippet!()] + macro_rules! foo { + () => { + }; + } + } + .to_string() + ); + } + + // TODO panicking is not the nicest way to handle this + #[test] + #[should_panic] + fn test_basic_fail_wrong_item() { + replace( + quote::quote! {}.into(), + quote::quote! { + #[doc = "docs"] + #[doc = "# {before_snippet}"] + static FOO: u32 = 0u32; + } + .into(), + ); + } +} diff --git a/esp-hal-procmacros/src/interrupt.rs b/esp-hal-procmacros/src/interrupt.rs index b663808c1c3..444fdf20cd7 100644 --- a/esp-hal-procmacros/src/interrupt.rs +++ b/esp-hal-procmacros/src/interrupt.rs @@ -1,6 +1,5 @@ -use proc_macro::{Span, TokenStream}; use proc_macro_crate::{FoundCrate, crate_name}; -use proc_macro2::Ident; +use proc_macro2::{Ident, Span, TokenStream}; use syn::{ AttrStyle, Attribute, @@ -19,12 +18,12 @@ pub enum WhiteListCaller { } pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream { - let mut f: ItemFn = syn::parse(input).expect("`#[handler]` must be applied to a function"); + let mut f: ItemFn = crate::unwrap_or_compile_error!(syn::parse2(input)); let original_span = f.span(); - let attr_args = match Punctuated::::parse_terminated.parse2(args.into()) { + let attr_args = match Punctuated::::parse_terminated.parse2(args) { Ok(v) => v, - Err(e) => return e.into_compile_error().into(), + Err(e) => return e.into_compile_error(), }; let mut priority = None; @@ -38,20 +37,17 @@ pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream { meta_name_value.span(), "duplicate `priority` attribute", ) - .into_compile_error() - .into(); + .into_compile_error(); } priority = Some(meta_name_value.value); } else { return SynError::new(meta_name_value.span(), "expected `priority = `") - .into_compile_error() - .into(); + .into_compile_error(); } } other => { return SynError::new(other.span(), "expected `priority = `") - .into_compile_error() - .into(); + .into_compile_error(); } } } @@ -61,7 +57,7 @@ pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream { Ok(FoundCrate::Name(ref name)) => name, _ => "crate", }, - Span::call_site().into(), + Span::call_site(), ); let priority = match priority { @@ -104,8 +100,7 @@ pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream { f.span(), "`#[handler]` handlers must have signature `[unsafe] fn([&mut Context]) [-> !]`", ) - .to_compile_error() - .into(); + .to_compile_error(); } f.sig.abi = syn::parse_quote_spanned!(original_span => extern "C"); @@ -123,7 +118,6 @@ pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream { #[allow(non_upper_case_globals)] #vis const #orig: #root::interrupt::InterruptHandler = #root::interrupt::InterruptHandler::new(#new, #priority); ) - .into() } pub fn check_attr_whitelist( @@ -156,9 +150,7 @@ pub fn check_attr_whitelist( } }; - return Err(SynError::new(attr.span(), err_str) - .to_compile_error() - .into()); + return Err(SynError::new(attr.span(), err_str).to_compile_error()); } Ok(()) @@ -168,3 +160,184 @@ pub fn check_attr_whitelist( fn eq(attr: &Attribute, name: &str) -> bool { attr.style == AttrStyle::Outer && attr.path().is_ident(name) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() { + let result = handler( + quote::quote! {}.into(), + quote::quote! { + fn foo(){} + } + .into(), + ); + + assert_eq!(result.to_string(), quote::quote! { + extern "C" fn __esp_hal_internal_foo() {} + #[allow(non_upper_case_globals)] + const foo: crate::interrupt::InterruptHandler = crate::interrupt::InterruptHandler::new( + __esp_hal_internal_foo, + crate::interrupt::Priority::min() + ); + }.to_string()); + } + + #[test] + fn test_priority() { + let result = handler( + quote::quote! { + priority = esp_hal::interrupt::Priority::Priority2 + } + .into(), + quote::quote! { + fn foo(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + extern "C" fn __esp_hal_internal_foo() {} + #[allow(non_upper_case_globals)] + const foo: crate::interrupt::InterruptHandler = + crate::interrupt::InterruptHandler::new(__esp_hal_internal_foo, { + const { + core::assert!( + !matches!( + esp_hal::interrupt::Priority::Priority2, + crate::interrupt::Priority::None + ), + "Priority::None is not supported", + ); + }; + esp_hal::interrupt::Priority::Priority2 + }); + } + .to_string() + ); + } + + #[test] + fn test_forbidden_attr() { + let result = handler( + quote::quote! {}.into(), + quote::quote! { + #[forbidden] + fn foo(){} + } + .into(), + ); + + assert_eq!(result.to_string(), quote::quote! { + ::core::compile_error!{ "this attribute is not allowed on an interrupt handler controlled by esp-hal" } + }.to_string()); + } + + #[test] + fn test_duplicate_priority() { + let result = handler( + quote::quote! { + priority = esp_hal::interrupt::Priority::Priority2, + priority = esp_hal::interrupt::Priority::Priority1, + } + .into(), + quote::quote! { + fn foo(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "duplicate `priority` attribute" } + } + .to_string() + ); + } + + #[test] + fn test_wrong_args() { + let result = handler( + quote::quote! { + true + } + .into(), + quote::quote! { + fn foo(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "expected identifier, found keyword `true`" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg() { + let result = handler( + quote::quote! { + not_allowed = true, + } + .into(), + quote::quote! { + fn foo(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "expected `priority = `" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg2() { + let result = handler( + quote::quote! { + A,B + } + .into(), + quote::quote! { + fn foo(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "expected `priority = `" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_sig() { + let result = handler( + quote::quote! {}.into(), + quote::quote! { + fn foo() -> u32 {} + } + .into(), + ); + + assert_eq!(result.to_string(), quote::quote! { + ::core::compile_error!{ "`#[handler]` handlers must have signature `[unsafe] fn([&mut Context]) [-> !]`" } + }.to_string()); + } +} diff --git a/esp-hal-procmacros/src/lib.rs b/esp-hal-procmacros/src/lib.rs index 461a081d2f0..2617f238a3b 100644 --- a/esp-hal-procmacros/src/lib.rs +++ b/esp-hal-procmacros/src/lib.rs @@ -32,7 +32,7 @@ //! //! Requires the `embassy` feature to be enabled. //! -//! ```rust, no_run +//! ```rust,ignore //! #[main] //! async fn main(spawner: Spawner) { //! // Your application's entry point @@ -96,7 +96,7 @@ mod rtos_main; /// /// # Examples /// -/// ```rust, no_run +/// ```rust, ignore /// #[ram(unstable(rtc_fast))] /// static mut SOME_INITED_DATA: [u8; 2] = [0xaa, 0xbb]; /// @@ -113,7 +113,7 @@ mod rtos_main; /// [`bytemuck::Zeroable`]: https://docs.rs/bytemuck/1.9.0/bytemuck/trait.Zeroable.html #[proc_macro_attribute] pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { - ram::ram(args, input) + ram::ram(args.into(), input.into()).into() } /// Replaces placeholders in rustdoc doc comments. @@ -134,7 +134,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { /// /// ## Examples /// -/// ```rust, no_run +/// ```rust, ignore /// #[doc_replace( /// "literal_placeholder" => "literal value", /// "conditional_placeholder" => { @@ -158,7 +158,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] pub fn doc_replace(args: TokenStream, input: TokenStream) -> TokenStream { - doc_replace::replace(args, input) + doc_replace::replace(args.into(), input.into()).into() } /// Mark a function as an interrupt handler. @@ -169,27 +169,27 @@ pub fn doc_replace(args: TokenStream, input: TokenStream) -> TokenStream { /// If no priority is given, `Priority::min()` is assumed #[proc_macro_attribute] pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream { - interrupt::handler(args, input) + interrupt::handler(args.into(), input.into()).into() } /// Load code to be run on the LP/ULP core. /// /// ## Example -/// ```rust, no_run +/// ```rust, ignore /// let lp_core_code = load_lp_code!("path.elf"); /// lp_core_code.run(&mut lp_core, lp_core::LpCoreWakeupSource::HpCpu, lp_pin); /// ```` #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] #[proc_macro] pub fn load_lp_code(input: TokenStream) -> TokenStream { - lp_core::load_lp_code(input) + lp_core::load_lp_code(input.into(), lp_core::RealFilesystem).into() } /// Marks the entry function of a LP core / ULP program. #[cfg(any(feature = "is-lp-core", feature = "is-ulp-core"))] #[proc_macro_attribute] pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { - lp_core::entry(args, input) + lp_core::entry(args.into(), input.into()).into() } /// Creates a new instance of `esp_rtos::embassy::Executor` and declares an application entry point @@ -206,7 +206,7 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { /// ## Examples /// Spawning a task: /// -/// ``` rust +/// ```rust,ignore /// #[esp_rtos::main] /// async fn main(_s: embassy_executor::Spawner) { /// // Function body @@ -214,7 +214,7 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] pub fn rtos_main(args: TokenStream, item: TokenStream) -> TokenStream { - rtos_main::main(args, item) + rtos_main::main(args.into(), item.into()).into() } /// Attribute to declare the entry point of the program @@ -235,7 +235,7 @@ pub fn rtos_main(args: TokenStream, item: TokenStream) -> TokenStream { /// /// - Simple entry point /// -/// ``` no_run +/// ```ignore /// #[main] /// fn main() -> ! { /// loop { /* .. */ } @@ -243,7 +243,8 @@ pub fn rtos_main(args: TokenStream, item: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] pub fn blocking_main(args: TokenStream, input: TokenStream) -> TokenStream { - blocking::main(args, input) + let f = syn::parse_macro_input!(input as syn::ItemFn); + blocking::main(args.into(), f).into() } /// Automatically implement the [Builder Lite] pattern for a struct. @@ -255,7 +256,7 @@ pub fn blocking_main(args: TokenStream, input: TokenStream) -> TokenStream { /// /// ## Example /// -/// ```rust, no_run +/// ```rust, ignore /// #[derive(Default)] /// enum MyEnum { /// #[default] @@ -280,7 +281,7 @@ pub fn blocking_main(args: TokenStream, input: TokenStream) -> TokenStream { /// [Builder Lite]: https://matklad.github.io/2022/05/29/builder-lite.html #[proc_macro_derive(BuilderLite, attributes(builder_lite))] pub fn builder_lite_derive(item: TokenStream) -> TokenStream { - builder::builder_lite_derive(item) + builder::builder_lite_derive(item.into()).into() } /// Print a build error and terminate the process. @@ -291,7 +292,7 @@ pub fn builder_lite_derive(item: TokenStream) -> TokenStream { /// /// ## Example /// -/// ```rust +/// ```rust, ignore /// esp_hal_procmacros::error! {" /// ERROR: something really bad has happened! /// "} @@ -320,3 +321,16 @@ pub fn error(input: TokenStream) -> TokenStream { pub fn warning(input: TokenStream) -> TokenStream { alert::do_alert(termcolor::Color::Yellow, input) } + +macro_rules! unwrap_or_compile_error { + ($($x:tt)*) => { + match $($x)* { + Ok(x) => x, + Err(e) => { + return e.into_compile_error() + } + } + }; +} + +pub(crate) use unwrap_or_compile_error; diff --git a/esp-hal-procmacros/src/lp_core.rs b/esp-hal-procmacros/src/lp_core.rs index 934880775c6..cfc20f35c9a 100644 --- a/esp-hal-procmacros/src/lp_core.rs +++ b/esp-hal-procmacros/src/lp_core.rs @@ -1,10 +1,12 @@ #[allow(unused)] -use proc_macro::TokenStream; +use proc_macro2::TokenStream; use quote::quote; #[cfg(any(feature = "is-lp-core", feature = "is-ulp-core"))] pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { - use proc_macro_crate::{FoundCrate, crate_name}; + use proc_macro_crate::FoundCrate; + #[cfg(not(test))] + use proc_macro_crate::crate_name; use proc_macro2::{Ident, Span}; use quote::format_ident; use syn::{ @@ -15,7 +17,6 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { PathArguments, Type, parse::Error, - parse_macro_input, spanned::Spanned, }; @@ -94,7 +95,12 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { res } + #[cfg(not(test))] let found_crate = crate_name("esp-lp-hal").expect("esp-lp-hal is present in `Cargo.toml`"); + + #[cfg(test)] + let found_crate = FoundCrate::Itself; + let hal_crate = match found_crate { FoundCrate::Itself => quote!(esp_lp_hal), FoundCrate::Name(name) => { @@ -105,11 +111,10 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { if !args.is_empty() { return Error::new(Span::call_site(), "This attribute accepts no arguments") - .to_compile_error() - .into(); + .to_compile_error(); } - let f = parse_macro_input!(input as ItemFn); + let f: ItemFn = crate::unwrap_or_compile_error!(syn::parse2(input)); let mut argument_types = Vec::new(); let mut create_peripheral = Vec::new(); @@ -120,18 +125,14 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { let param_name = format_ident!("param{}", num); match arg { FnArg::Receiver(_) => { - return Error::new(arg.span(), "invalid argument") - .to_compile_error() - .into(); + return Error::new(arg.span(), "invalid argument").to_compile_error(); } FnArg::Typed(t) => { match simplename(&t.ty).as_str() { "Output" => { let pin = extract_pin(&t.ty); if used_pins.contains(&pin) { - return Error::new(arg.span(), "duplicate pin") - .to_compile_error() - .into(); + return Error::new(arg.span(), "duplicate pin").to_compile_error(); } used_pins.push(pin); create_peripheral.push(quote!( @@ -141,9 +142,7 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { "Input" => { let pin = extract_pin(&t.ty); if used_pins.contains(&pin) { - return Error::new(arg.span(), "duplicate pin") - .to_compile_error() - .into(); + return Error::new(arg.span(), "duplicate pin").to_compile_error(); } used_pins.push(pin); create_peripheral.push(quote!( @@ -162,8 +161,7 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { } _ => { return Error::new(arg.span(), "invalid argument to main") - .to_compile_error() - .into(); + .to_compile_error(); } } argument_types.push(t); @@ -190,7 +188,7 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { use #hal_crate as the_hal; #( - #create_peripheral; + #create_peripheral )* main(#(#param_names),*); @@ -198,17 +196,14 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { #f ) - .into() } #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] -pub fn load_lp_code(input: TokenStream) -> TokenStream { - use std::{fs, path::Path}; - +pub fn load_lp_code(input: TokenStream, fs: impl Filesystem) -> TokenStream { use object::{File, Object, ObjectSection, ObjectSymbol, Section, SectionKind}; use parse::Error; - use proc_macro::Span; use proc_macro_crate::{FoundCrate, crate_name}; + use proc_macro2::Span; use syn::{Ident, LitStr, parse}; let hal_crate = if cfg!(any(feature = "is-lp-core", feature = "is-ulp-core")) { @@ -218,27 +213,30 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream { }; let hal_crate = if let Ok(FoundCrate::Name(ref name)) = hal_crate { - let ident = Ident::new(name, Span::call_site().into()); + let ident = Ident::new(name, proc_macro2::Span::call_site()); quote!( #ident ) } else { quote!(crate) }; - let lit: LitStr = match syn::parse(input) { + let lit: LitStr = match syn::parse2(input) { Ok(lit) => lit, - Err(e) => return e.into_compile_error().into(), + Err(e) => return e.into_compile_error(), }; let elf_file = lit.value(); - if !Path::new(&elf_file).exists() { - return Error::new(Span::call_site().into(), "File not found") - .to_compile_error() - .into(); + if !fs.exists(&elf_file) { + return Error::new(Span::call_site(), "File not found").to_compile_error(); } - let bin_data = fs::read(elf_file).unwrap(); - let obj_file = File::parse(&*bin_data).unwrap(); + let bin_data = fs.read(&elf_file).unwrap(); + let obj_file = match File::parse(&*bin_data) { + Ok(obj_file) => obj_file, + Err(e) => { + return Error::new(Span::call_site(), format!("Error: {}", e)).to_compile_error(); + } + }; let sections = obj_file.sections(); let mut sections: Vec
= sections @@ -265,7 +263,7 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream { for section in sections { if section.address() > last_address { let fill = section.address() - last_address; - binary.extend(std::iter::repeat(0).take(fill as usize)); + binary.extend(std::iter::repeat_n(0, fill as usize)); } binary.extend_from_slice(section.data().unwrap()); @@ -283,8 +281,7 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream { Span::call_site().into(), "Given file doesn't seem to be an LP/ULP core application.", ) - .to_compile_error() - .into(); + .to_compile_error(); }; let magic_symbol = magic_symbol.trim_start_matches("__ULP_MAGIC_"); @@ -353,5 +350,242 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream { LpCoreCode {} } } - .into() +} + +#[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] +pub(crate) trait Filesystem { + fn read(&self, path: &str) -> std::io::Result>; + + fn exists(&self, path: &str) -> bool; +} + +#[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] +pub(crate) struct RealFilesystem; + +#[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] +impl Filesystem for RealFilesystem { + fn read(&self, path: &str) -> std::io::Result> { + std::fs::read(path) + } + + fn exists(&self, path: &str) -> bool { + std::path::Path::new(path).exists() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] + struct TestFilesystem; + + #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] + impl Filesystem for TestFilesystem { + fn read(&self, path: &str) -> std::io::Result> { + if path == "doesnt_exist.elf" { + Err(std::io::ErrorKind::NotFound.into()) + } else if path == "bad.elf" { + Ok(Vec::from(&[1, 2, 3, 4])) + } else { + std::fs::read("./testdata/ulp_code.elf") + } + } + + fn exists(&self, path: &str) -> bool { + if path == "doesnt_exist.elf" { + false + } else { + true + } + } + } + + #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] + #[test] + fn test_load_lp_code_file_not_found() { + let result = load_lp_code(quote::quote! {"doesnt_exist.elf"}.into(), TestFilesystem); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error! { "File not found" } + } + .to_string() + ); + } + + #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] + #[test] + fn test_load_lp_code_bad() { + let result = load_lp_code(quote::quote! {"bad.elf"}.into(), TestFilesystem); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error! { "Error: Could not read file magic" } + } + .to_string() + ); + } + + #[cfg(any(feature = "has-lp-core", feature = "has-ulp-core"))] + #[test] + fn test_load_lp_code_basic() { + let result = load_lp_code(quote::quote! {"good.elf"}.into(), TestFilesystem); + + assert_eq!( + result.to_string(), + quote::quote! { + { + use crate::lp_core::LpCore; + use crate::lp_core::LpCoreWakeupSource; + use crate::gpio::lp_io::LowPowerOutput; + use crate::gpio::*; + use crate::uart::lp_uart::LpUart; + use crate::i2c::lp_i2c::LpI2c; + + struct LpCoreCode {} + static LP_CODE: &[u8] = &[ + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, 19u8, 0u8, 0u8, 0u8, + 9u8, 160u8, 23u8, 65u8, 0u8, 0u8, 19u8, 1u8, 225u8, 247u8, 151u8, 0u8, 0u8, 0u8, 231u8, + 128u8, 160u8, 0u8, 1u8, 160u8, 55u8, 5u8, 11u8, 96u8, 3u8, 37u8, 5u8, 64u8, 17u8, + 137u8, 9u8, 201u8, 55u8, 5u8, 0u8, 80u8, 183u8, 53u8, 49u8, 1u8, 147u8, 133u8, 5u8, + 208u8, 35u8, 46u8, 181u8, 22u8, 151u8, 0u8, 0u8, 0u8, 231u8, 128u8, 192u8, 11u8, 129u8, + 67u8, 183u8, 40u8, 11u8, 96u8, 55u8, 40u8, 0u8, 80u8, 137u8, 66u8, 55u8, 3u8, 0u8, + 80u8, 133u8, 3u8, 35u8, 32u8, 120u8, 0u8, 35u8, 162u8, 88u8, 0u8, 131u8, 39u8, 195u8, + 23u8, 243u8, 37u8, 0u8, 184u8, 243u8, 38u8, 0u8, 176u8, 115u8, 38u8, 0u8, 184u8, 227u8, + 154u8, 197u8, 254u8, 133u8, 131u8, 51u8, 6u8, 208u8, 64u8, 179u8, 54u8, 208u8, 0u8, + 179u8, 5u8, 176u8, 64u8, 149u8, 141u8, 243u8, 38u8, 0u8, 184u8, 115u8, 39u8, 0u8, + 176u8, 115u8, 37u8, 0u8, 184u8, 227u8, 154u8, 166u8, 254u8, 50u8, 151u8, 174u8, 150u8, + 51u8, 53u8, 199u8, 0u8, 54u8, 149u8, 179u8, 182u8, 231u8, 0u8, 51u8, 53u8, 160u8, 0u8, + 85u8, 141u8, 113u8, 221u8, 35u8, 164u8, 88u8, 0u8, 3u8, 38u8, 195u8, 23u8, 243u8, 37u8, + 0u8, 184u8, 243u8, 38u8, 0u8, 176u8, 115u8, 37u8, 0u8, 184u8, 227u8, 154u8, 165u8, + 254u8, 5u8, 130u8, 179u8, 7u8, 208u8, 64u8, 51u8, 53u8, 208u8, 0u8, 179u8, 5u8, 176u8, + 64u8, 137u8, 141u8, 243u8, 38u8, 0u8, 184u8, 115u8, 39u8, 0u8, 176u8, 115u8, 37u8, 0u8, + 184u8, 227u8, 154u8, 166u8, 254u8, 62u8, 151u8, 174u8, 150u8, 51u8, 53u8, 247u8, 0u8, + 54u8, 149u8, 179u8, 54u8, 230u8, 0u8, 51u8, 53u8, 160u8, 0u8, 85u8, 141u8, 113u8, + 221u8, 185u8, 191u8, 55u8, 5u8, 0u8, 80u8, 3u8, 32u8, 197u8, 23u8, 151u8, 0u8, 0u8, + 0u8, 231u8, 128u8, 64u8, 244u8, 0u8, 36u8, 244u8, 0u8 + ]; + unsafe extern "C" { + static _rtc_fast_data_start: u32; + } + unsafe { + core::ptr::copy_nonoverlapping( + LP_CODE as *const _ as *const u8, + &_rtc_fast_data_start as *const u32 as *mut u8, + LP_CODE.len() + ); + } + impl LpCoreCode { + pub fn run( + &self, + lp_core: &mut LpCore, + wakeup_source: LpCoreWakeupSource, + _: LowPowerOutput<1> + ) { + lp_core.run(wakeup_source); + } + } + LpCoreCode {} + } + } + .to_string() + ); + } + + #[cfg(any(feature = "is-lp-core"))] + #[test] + fn test_lp_entry_basic() { + let result = entry( + quote::quote! {}.into(), + quote::quote! { + fn main(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[allow(non_snake_case)] + #[unsafe(export_name = "main")] + pub fn __risc_v_rt__main () -> ! { + #[unsafe(export_name = "__ULP_MAGIC_")] + static ULP_MAGIC: [u32;0] = [0u32;0]; + unsafe { + ULP_MAGIC.as_ptr().read_volatile (); + } + + use esp_lp_hal as the_hal; + main (); + } + + fn main () { } + } + .to_string() + ); + } + + #[cfg(any(feature = "is-lp-core"))] + #[test] + fn test_lp_entry_with_params() { + let result = entry( + quote::quote! {}.into(), + quote::quote! { + fn main(mut gpio1: Output<1>, mut i2c: LpI2c, mut uart: LpUart){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[allow(non_snake_case)] + #[unsafe(export_name = "main")] + pub fn __risc_v_rt__main () -> ! { + # [unsafe (export_name = "__ULP_MAGIC_Output<1>$LpI2c$LpUart$")] + static ULP_MAGIC: [u32;0] = [0u32;0]; + unsafe { ULP_MAGIC.as_ptr().read_volatile(); + } + + use esp_lp_hal as the_hal ; + let mut param0 = unsafe { the_hal::gpio::conjure_output().unwrap() }; + let mut param1 = unsafe { the_hal::i2c::conjure() }; + let mut param2 = unsafe { the_hal::uart::conjure() }; + main (param0 , param1 , param2) ; } + + fn main (mut gpio1 : Output < 1 > , mut i2c : LpI2c , mut uart : LpUart) { } + } + .to_string() + ); + } + + #[cfg(any(feature = "is-lp-core"))] + #[test] + fn test_lp_entry_non_empty_args() { + let result = entry( + quote::quote! { foo }.into(), + quote::quote! { + fn main(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "This attribute accepts no arguments" } + } + .to_string() + ); + } } diff --git a/esp-hal-procmacros/src/ram.rs b/esp-hal-procmacros/src/ram.rs index 0c0fa5f2936..2d6e090f1a2 100644 --- a/esp-hal-procmacros/src/ram.rs +++ b/esp-hal-procmacros/src/ram.rs @@ -1,11 +1,10 @@ -use proc_macro::TokenStream; -use proc_macro2::{Ident, Span}; -use syn::{Item, Token, parse, parse::Parser, punctuated::Punctuated, spanned::Spanned}; +use proc_macro2::{Ident, Span, TokenStream}; +use syn::{Item, Token, parse::Parser, parse2, punctuated::Punctuated, spanned::Spanned}; pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { - let attr_args = match Punctuated::::parse_terminated.parse2(args.into()) { + let attr_args = match Punctuated::::parse_terminated.parse2(args) { Ok(v) => v, - Err(e) => return e.to_compile_error().into(), + Err(e) => return e.to_compile_error(), }; let mut rtc_fast = false; @@ -22,7 +21,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { .parse2(nested.clone()) { Ok(v) => v, - Err(e) => return e.to_compile_error().into(), + Err(e) => return e.to_compile_error(), }; for meta in nested_args { @@ -33,8 +32,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { path.span(), "Expected identifier inside `unstable(...)`", ) - .into_compile_error() - .into(); + .into_compile_error(); }; let arg = match ident { i if i == "rtc_fast" => &mut rtc_fast, @@ -46,8 +44,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { i.span(), format!("Unknown unstable argument `{i}`"), ) - .into_compile_error() - .into(); + .into_compile_error(); } }; @@ -56,8 +53,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { ident.span(), format!("Argument `{ident}` is already set"), ) - .into_compile_error() - .into(); + .into_compile_error(); } *arg = true; @@ -67,8 +63,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { list.span(), "Expected identifiers inside `unstable(...)`", ) - .into_compile_error() - .into(); + .into_compile_error(); } } } @@ -77,8 +72,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { syn::Meta::Path(path) => { let Some(ident) = path.get_ident() else { return syn::Error::new(path.span(), "Expected identifier") - .into_compile_error() - .into(); + .into_compile_error(); }; let arg = match ident { i if i == "reclaimed" => &mut dram2_uninit, @@ -87,8 +81,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { ident.span(), format!("`{ident}` must be wrapped in `unstable(...)`"), ) - .into_compile_error() - .into(); + .into_compile_error(); } }; @@ -97,21 +90,19 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { ident.span(), format!("Argument `{ident}` is already set"), ) - .into_compile_error() - .into(); + .into_compile_error(); } *arg = true; } _ => { return syn::Error::new(attr_arg.span(), "Unsupported attribute syntax for `ram`") - .into_compile_error() - .into(); + .into_compile_error(); } } } - let item: Item = parse(input).expect("failed to parse input"); + let item: Item = crate::unwrap_or_compile_error!(parse2(input)); #[cfg(not(feature = "rtc-slow"))] if rtc_slow { @@ -119,8 +110,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { Span::call_site(), "rtc_slow is not available for this target", ) - .into_compile_error() - .into(); + .into_compile_error(); } let is_fn = matches!(item, Item::Fn(_)); @@ -153,8 +143,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { }, (_, Err(_)) => { return syn::Error::new(Span::call_site(), "Invalid combination of ram arguments") - .into_compile_error() - .into(); + .into_compile_error(); } }; @@ -188,11 +177,452 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { } }); - let output = quote::quote! { + quote::quote! { #section #item #trait_check - }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rwtext() { + let result = ram( + quote::quote! {}.into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rwtext")] + #[inline (never)] + fn foo () { } + } + .to_string() + ); + } + + #[test] + fn test_rtc_fast_text() { + let result = ram( + quote::quote! { + unstable(rtc_fast) + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); - output.into() + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_fast.text")] + #[inline (never)] + fn foo () { } + } + .to_string() + ); + } + + #[cfg(feature = "rtc-slow")] + #[test] + fn test_rtc_slow_text() { + let result = ram( + quote::quote! { + unstable(rtc_slow) + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_slow.text")] + #[inline (never)] + fn foo () { } + } + .to_string() + ); + } + + #[test] + fn test_data() { + let result = ram( + quote::quote! {}.into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".data")] + static mut FOO:[u8;10] = [0;10]; + } + .to_string() + ); + } + + #[test] + fn test_rtc_fast_data() { + let result = ram( + quote::quote! { + unstable(rtc_fast) + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_fast.data")] + static mut FOO:[u8;10] = [0;10]; + } + .to_string() + ); + } + + #[cfg(feature = "rtc-slow")] + #[test] + fn test_rtc_slow_data() { + let result = ram( + quote::quote! { + unstable(rtc_slow) + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_slow.data")] + static mut FOO:[u8;10] = [0;10]; + } + .to_string() + ); + } + + #[test] + fn test_reclaimed() { + let result = ram( + quote::quote! { + reclaimed + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe(link_section = ".dram2_uninit")] + static mut FOO: [u8;10] = [0;10]; + const _ : () = crate::__macro_implementation::assert_is_uninit::<[u8;10]>(); + } + .to_string() + ); + } + + #[test] + fn test_rtc_fast_data_zeroed() { + let result = ram( + quote::quote! { + unstable(rtc_fast,zeroed) + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_fast.bss")] + static mut FOO:[u8;10] = [0;10]; + const _: () = crate::__macro_implementation::assert_is_zeroable::<[u8; 10]>(); + } + .to_string() + ); + } + + #[test] + fn test_rtc_fast_data_persistent() { + let result = ram( + quote::quote! { + unstable(rtc_fast,persistent) + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_fast.persistent")] + static mut FOO:[u8;10] = [0;10]; + const _: () = crate::__macro_implementation::assert_is_persistable::<[u8; 10]>(); + } + .to_string() + ); + } + + #[cfg(feature = "rtc-slow")] + #[test] + fn test_rtc_slow_data_zeroed() { + let result = ram( + quote::quote! { + unstable(rtc_slow,zeroed) + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_slow.bss")] + static mut FOO:[u8;10] = [0;10]; + const _: () = crate::__macro_implementation::assert_is_zeroable::<[u8; 10]>(); + } + .to_string() + ); + } + + #[cfg(feature = "rtc-slow")] + #[test] + fn test_rtc_slow_data_persistent() { + let result = ram( + quote::quote! { + unstable(rtc_slow,persistent) + } + .into(), + quote::quote! { + static mut FOO: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe (link_section = ".rtc_slow.persistent")] + static mut FOO:[u8;10] = [0;10]; + const _: () = crate::__macro_implementation::assert_is_persistable::<[u8; 10]>(); + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg() { + let result = ram( + quote::quote! { + test() + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "Unsupported attribute syntax for `ram`" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg2() { + let result = ram( + quote::quote! { + unstable(unstable(unstable)) + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "Expected identifiers inside `unstable(...)`" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg3() { + let result = ram( + quote::quote! { + unstable(unknown) + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "Unknown unstable argument `unknown`" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg4() { + let result = ram( + quote::quote! { + unstable(rtc_fast,rtc_fast) + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "Argument `rtc_fast` is already set" } + } + .to_string() + ); + } + + #[cfg(feature = "rtc-slow")] + #[test] + fn test_illegal_arg5() { + let result = ram( + quote::quote! { + unstable(rtc_slow,rtc_fast) + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "Invalid combination of ram arguments" } + } + .to_string() + ); + } + + #[test] + fn test_illegal_arg6() { + let result = ram( + quote::quote! { + rtc_fast + } + .into(), + quote::quote! { + fn foo() {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "`rtc_fast` must be wrapped in `unstable(...)`" } + } + .to_string() + ); + } + + #[test] + fn test_rtc_fast_data_persistent_on_local_var() { + let result = ram( + quote::quote! { + unstable(rtc_fast,persistent) + } + .into(), + quote::quote! { + mut foo: [u8; 10] = [0; 10]; + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "expected one of: `fn`, `extern`, `use`, `static`, `const`, `unsafe`, `mod`, `type`, `struct`, `enum`, `union`, `trait`, `auto`, `impl`, `default`, `macro`, identifier, `self`, `super`, `crate`, `::`" } + } + .to_string() + ); + } + + #[test] + fn test_rtc_fast_data_persistent_on_non_static() { + let result = ram( + quote::quote! { + unstable(rtc_fast,persistent) + } + .into(), + quote::quote! { + struct Foo {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[unsafe(link_section = ".rtc_fast.persistent")] + struct Foo { } + ::core::compile_error!{ "Expected a `static`" } + } + .to_string() + ); + } } diff --git a/esp-hal-procmacros/src/rtos_main.rs b/esp-hal-procmacros/src/rtos_main.rs index 6690cddc844..d2a90a9acec 100644 --- a/esp-hal-procmacros/src/rtos_main.rs +++ b/esp-hal-procmacros/src/rtos_main.rs @@ -1,7 +1,6 @@ use std::{cell::RefCell, fmt::Display, thread}; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{TokenStream, TokenStream as TokenStream2}; use quote::{ToTokens, quote}; use syn::{ Attribute, @@ -29,10 +28,10 @@ impl Parse for Args { /// Procedural macro entry point for the async `main` function. pub fn main(args: TokenStream, item: TokenStream) -> TokenStream { - let args = syn::parse_macro_input!(args as Args); - let f = syn::parse_macro_input!(item as syn::ItemFn); + let args: Args = crate::unwrap_or_compile_error!(syn::parse2(args)); + let f: syn::ItemFn = crate::unwrap_or_compile_error!(syn::parse2(item)); - run(&args.meta, f, main_fn()).unwrap_or_else(|x| x).into() + run(&args.meta, f, main_fn()).unwrap_or_else(|x| x) } /// Expands and validates the async `main` function into a task entry point. @@ -196,3 +195,298 @@ pub fn main_fn() -> TokenStream2 { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(spawner: Spawner){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + pub (crate) mod __main { + use super::*; + #[doc(hidden)] + #[::embassy_executor::task()] + async fn __embassy_main (spawner : Spawner) { + { } + } + + #[doc(hidden)] + unsafe fn __make_static < T > (t : & mut T) -> & 'static mut T { + ::core::mem::transmute(t) + } + + #[esp_hal::main] + fn main () -> ! { + let mut executor = ::esp_rtos::embassy::Executor::new(); + let executor = unsafe { __make_static (& mut executor) }; + executor . run (| spawner | { + spawner.must_spawn(__embassy_main (spawner)); + }) + } + } + } + .to_string() + ); + } + + #[test] + fn test_non_async_fn() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + fn foo(spawner: Spawner){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "main function must be async" } + } + .to_string() + ); + } + + #[test] + fn test_no_arg() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "main function must have 1 argument: the spawner." } + } + .to_string() + ); + } + + #[test] + fn test_not_generic() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(spawner: S){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "main function must not be generic" } + } + .to_string() + ); + } + + #[test] + fn test_not_abi() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async extern "C" fn foo(spawner: Spawner){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "main function must not have an ABI qualifier" } + } + .to_string() + ); + } + + #[test] + fn test_not_variadic() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(spawner: ...){} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "main function must not be variadic" } + ::core::compile_error!{ "main function must have 1 argument: the spawner." } + } + .to_string() + ); + } + + #[test] + fn test_not_return_value() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(spawner: Spawner) -> u32 {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + ::core::compile_error!{ "main function must either not return a value, return `()` or return `!`" } + } + .to_string() + ); + } + + #[test] + fn test_basic_return_never() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(spawner: Spawner) -> ! {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + pub (crate) mod __main { + use super::*; + #[doc(hidden)] + #[::embassy_executor::task()] + async fn __embassy_main (spawner : Spawner) -> ! { + { } + } + + #[doc(hidden)] + unsafe fn __make_static < T > (t : & mut T) -> & 'static mut T { + ::core::mem::transmute(t) + } + + # [esp_hal::main] + fn main () -> ! { + let mut executor = ::esp_rtos::embassy::Executor::new(); + let executor = unsafe { __make_static (& mut executor) }; + executor.run (| spawner | { + spawner.must_spawn(__embassy_main (spawner)); + }) + } + } + } + .to_string() + ); + } + + #[test] + fn test_basic_return_tuple() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + async fn foo(spawner: Spawner) -> () {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + pub (crate) mod __main { + use super::*; + #[doc(hidden)] + #[::embassy_executor::task()] + async fn __embassy_main (spawner : Spawner) -> () { + { } + } + + #[doc(hidden)] + unsafe fn __make_static < T > (t : & mut T) -> & 'static mut T { + ::core::mem::transmute(t) + } + + #[esp_hal::main] + fn main () -> ! { + let mut executor = ::esp_rtos::embassy::Executor::new(); + let executor = unsafe { __make_static (& mut executor) }; + executor.run (| spawner | { + spawner.must_spawn(__embassy_main (spawner)); + }) + } + } + } + .to_string() + ); + } + + #[test] + fn test_basic_propagate_lint_attrs() { + let result = main( + quote::quote! {}.into(), + quote::quote! { + #[allow(allowed)] + #[deny(denied)] + #[warn(warning)] + #[ram] + async fn foo(spawner: Spawner) -> () {} + } + .into(), + ); + + assert_eq!( + result.to_string(), + quote::quote! { + #[allow(allowed)] + #[deny(denied)] + #[warn (warning)] + pub (crate) mod __main { + use super::*; + #[doc(hidden)] + #[allow(allowed)] + #[deny(denied)] + #[warn (warning)] + #[ram] + #[::embassy_executor::task()] + async fn __embassy_main (spawner : Spawner) -> () { + { } + } + + #[doc(hidden)] + unsafe fn __make_static < T > (t : & mut T) -> & 'static mut T { + ::core::mem::transmute(t) + } + + #[allow(allowed)] + #[deny(denied)] + #[warn(warning)] + #[ram] + #[esp_hal::main] + fn main () -> ! { + let mut executor = ::esp_rtos::embassy::Executor::new(); + let executor = unsafe { __make_static (& mut executor) }; + executor.run (| spawner | { + spawner.must_spawn(__embassy_main (spawner)); + }) + } + } + } + .to_string() + ); + } +} diff --git a/esp-hal-procmacros/testdata/ulp_code.elf b/esp-hal-procmacros/testdata/ulp_code.elf new file mode 100644 index 00000000000..29d18005abf Binary files /dev/null and b/esp-hal-procmacros/testdata/ulp_code.elf differ diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index 5e8a2430b3a..fe3aef0bd5f 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -761,6 +761,19 @@ pub fn run_host_tests(workspace: &Path, package: Package) -> Result<()> { &package_path, ); } + Package::EspHalProcmacros => { + return cargo::run( + &cmd.clone() + .subcommand("test") + .features(&vec![ + "has-lp-core".into(), + "is-lp-core".into(), + "rtc-slow".into(), + ]) + .build(), + &package_path, + ); + } _ => Err(anyhow!( "Instructions for host testing were not provided for: '{}'", package,