Repository: knurling-rs/defmt
Pull request #531 (2021-07-15 12:47:59)

refactor the `macros` crate
~~NOT READY FOR REVIEW. I just wanted to signal that I'm working on this~~

japaric: (done with the refactor but now this needs a rebase. will rebase later and mark the PR as 'ready for review')

Patch:
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
index af7b200..21c2ad5 100644
--- a/macros/Cargo.toml
+++ b/macros/Cargo.toml
@@ -18,6 +18,7 @@ unstable-test = []
 
 [dependencies]
 defmt-parser = { path = "../parser", features = ["unstable"], version = "=0.2.2" }
+proc-macro-error = "1.0.4"
 proc-macro2 = "1.0.27"
 quote = "1.0.9"
 syn = { version = "1.0", features = ["full"] }
diff --git a/macros/src/attributes.rs b/macros/src/attributes.rs
new file mode 100644
index 0000000..a99c228
--- /dev/null
+++ b/macros/src/attributes.rs
@@ -0,0 +1,4 @@
+//! Procedural macros that are attributes
+
+pub(crate) mod global_logger;
+pub(crate) mod panic_handler;
diff --git a/macros/src/attributes/global_logger.rs b/macros/src/attributes/global_logger.rs
new file mode 100644
index 0000000..e982ec2
--- /dev/null
+++ b/macros/src/attributes/global_logger.rs
@@ -0,0 +1,57 @@
+use proc_macro::TokenStream;
+use proc_macro_error::{abort, abort_call_site};
+use quote::quote;
+use syn::{parse_macro_input, Fields, ItemStruct};
+
+pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
+    if !args.is_empty() {
+        abort_call_site!("`#[global_logger]` attribute takes no arguments")
+    }
+
+    let strukt = parse_macro_input!(item as ItemStruct);
+
+    validate(&strukt);
+
+    codegen(&strukt)
+}
+
+fn validate(strukt: &ItemStruct) {
+    let is_unit_struct = matches!(strukt.fields, Fields::Unit);
+
+    if !strukt.generics.params.is_empty()
+        || strukt.generics.where_clause.is_some()
+        || !is_unit_struct
+    {
+        abort!(
+            strukt,
+            "struct must be a non-generic unit struct (e.g. `struct S;`)"
+        );
+    }
+}
+
+fn codegen(strukt: &ItemStruct) -> TokenStream {
+    let attrs = &strukt.attrs;
+    let ident = &strukt.ident;
+    let vis = &strukt.vis;
+
+    quote!(
+        #(#attrs)*
+        #vis struct #ident;
+
+        #[no_mangle]
+        unsafe fn _defmt_acquire()  {
+            <#ident as defmt::Logger>::acquire()
+        }
+
+        #[no_mangle]
+        unsafe fn _defmt_release()  {
+            <#ident as defmt::Logger>::release()
+        }
+
+        #[no_mangle]
+        unsafe fn _defmt_write(bytes: &[u8])  {
+            <#ident as defmt::Logger>::write(bytes)
+        }
+    )
+    .into()
+}
diff --git a/macros/src/attributes/panic_handler.rs b/macros/src/attributes/panic_handler.rs
new file mode 100644
index 0000000..76ea4c8
--- /dev/null
+++ b/macros/src/attributes/panic_handler.rs
@@ -0,0 +1,77 @@
+use proc_macro::TokenStream;
+use proc_macro_error::{abort, abort_call_site};
+use quote::quote;
+use syn::{parse_macro_input, Attribute, ItemFn, ReturnType, Type};
+
+pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
+    if !args.is_empty() {
+        abort_call_site!("`#[defmt::panic_handler]` attribute takes no arguments");
+    }
+
+    let fun = parse_macro_input!(item as ItemFn);
+
+    validate(&fun);
+
+    codegen(&fun)
+}
+
+fn validate(fun: &ItemFn) {
+    let is_divergent = match &fun.sig.output {
+        ReturnType::Default => false,
+        ReturnType::Type(_, ty) => matches!(&**ty, Type::Never(_)),
+    };
+
+    if fun.sig.constness.is_some()
+        || fun.sig.asyncness.is_some()
+        || fun.sig.unsafety.is_some()
+        || fun.sig.abi.is_some()
+        || !fun.sig.generics.params.is_empty()
+        || fun.sig.generics.where_clause.is_some()
+        || fun.sig.variadic.is_some()
+        || !fun.sig.inputs.is_empty()
+        || !is_divergent
+    {
+        abort!(fun.sig.ident, "function must have signature `fn() -> !`");
+    }
+
+    check_for_attribute_conflicts("panic_handler", &fun.attrs, &["export_name", "no_mangle"]);
+}
+
+/// Checks if any attribute in `attrs_to_check` is in `reject_list` and returns a compiler error if there's a match
+///
+/// The compiler error will indicate that the attribute conflicts with `attr_name`
+fn check_for_attribute_conflicts(
+    attr_name: &str,
+    attrs_to_check: &[Attribute],
+    reject_list: &[&str],
+) {
+    for attr in attrs_to_check {
+        if let Some(ident) = attr.path.get_ident() {
+            let ident = ident.to_string();
+
+            if reject_list.contains(&ident.as_str()) {
+                abort!(
+                    attr,
+                    "`#[{}]` attribute cannot be used together with `#[{}]`",
+                    attr_name,
+                    ident
+                )
+            }
+        }
+    }
+}
+
+fn codegen(fun: &ItemFn) -> TokenStream {
+    let attrs = &fun.attrs;
+    let block = &fun.block;
+    let ident = &fun.sig.ident;
+
+    quote!(
+        #(#attrs)*
+        #[export_name = "_defmt_panic"]
+        fn #ident() -> ! {
+            #block
+        }
+    )
+    .into()
+}
diff --git a/macros/src/bitflags.rs b/macros/src/bitflags.rs
deleted file mode 100644
index 618f611..0000000
--- a/macros/src/bitflags.rs
+++ /dev/null
@@ -1,200 +0,0 @@
-use proc_macro::TokenStream;
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{quote, ToTokens};
-use syn::parse_macro_input;
-
-use crate::{mksym, symbol::Symbol};
-
-pub(super) fn expand(ts: TokenStream) -> TokenStream {
-    let ts2 = proc_macro2::TokenStream::from(ts.clone());
-    let input: parser::Input = parse_macro_input!(ts);
-
-    let codegen = Codegen::new(&input);
-
-    // Encode package and disambiguator to provide the decoder with all info it needs (even if
-    // technically redundant, since it's also stored in the symbol we create).
-    let format_str = format!(
-        "{{={}:__internal_bitflags_{}@{}@{}}}",
-        input.ty().to_token_stream(),
-        input.ident(),
-        crate::symbol::package(),
-        crate::symbol::disambiguator(),
-    );
-    let sym = mksym(&format_str, "bitflags", false);
-
-    let ident = input.ident();
-    let ty = input.ty();
-    let flags = codegen.flags.iter().map(|f| &f.def);
-    let result = quote! {
-        const _: () = {
-            fn assert<T: defmt::export::UnsignedInt>() {}
-            assert::<#ty>;
-
-            #(#flags)*
-        };
-
-        defmt::export::bitflags! {
-            #ts2
-        }
-
-        impl defmt::Format for #ident {
-            fn format(&self, f: defmt::Formatter) {
-                unreachable!()
-            }
-            fn _format_tag() -> defmt::Str {
-                #sym
-            }
-            fn _format_data(&self) {
-                // There's a method available for every supported bitflags type.
-                defmt::export::#ty(&self.bits());
-            }
-        }
-    };
-    result.into()
-}
-
-struct FlagStatic {
-    def: TokenStream2,
-}
-
-struct Codegen {
-    flags: Vec<FlagStatic>,
-}
-
-impl Codegen {
-    fn new(input: &parser::Input) -> Self {
-        let flags = input
-            .flags()
-            .map(|flag| {
-                let cfg_attrs = flag.cfg_attrs();
-                let name = &flag.ident();
-                let value = &flag.value();
-                let repr_ty = &input.ty();
-
-                let sym = Symbol::new(
-                    "bitflags_value",
-                    &format!("{}::{}", input.ident(), flag.ident()),
-                )
-                .mangle();
-
-                let def = quote! {
-                    #(#cfg_attrs)*
-                    #[cfg_attr(target_os = "macos", link_section = ".defmt,end")]
-                    #[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end")]
-                    #[export_name = #sym]
-                    static #name: u128 = {
-                        // NB: It might be tempting to just do `#value as u128` here, but that
-                        // causes a value such as `1 << 127` to be evaluated as an `i32`, which
-                        // overflows. So we instead coerce (but don't cast) it to the bitflags' raw
-                        // type, and then cast that to u128.
-                        let coerced_value: #repr_ty = #value;
-                        coerced_value as u128
-                    };
-                };
-                FlagStatic { def }
-            })
-            .collect::<Vec<_>>();
-
-        Codegen { flags }
-    }
-}
-
-mod parser {
-    use syn::{
-        parse::{Parse, ParseStream},
-        punctuated::Punctuated,
-        token, Attribute, Expr, Ident, Token, Type, Visibility,
-    };
-
-    #[allow(dead_code)]
-    pub(super) struct Input {
-        struct_attrs: Vec<Attribute>,
-        vis: Visibility,
-        struct_token: Token![struct],
-        ident: Ident,
-        colon_token: Token![:],
-        ty: Type,
-        brace_token: token::Brace,
-        flags: Punctuated<Flag, Token![;]>,
-    }
-
-    impl Parse for Input {
-        fn parse(input: ParseStream) -> syn::Result<Self> {
-            let flags;
-            Ok(Self {
-                struct_attrs: Attribute::parse_outer(input)?,
-                vis: input.parse()?,
-                struct_token: input.parse()?,
-                ident: input.parse()?,
-                colon_token: input.parse()?,
-                ty: input.parse()?,
-                brace_token: syn::braced!(flags in input),
-                flags: Punctuated::parse_terminated(&flags)?,
-            })
-        }
-    }
-
-    impl Input {
-        pub(super) fn flags(&self) -> impl Iterator<Item = &Flag> {
-            self.flags.iter()
-        }
-
-        pub(super) fn ident(&self) -> &Ident {
-            &self.ident
-        }
-
-        pub(super) fn ty(&self) -> &Type {
-            &self.ty
-        }
-    }
-
-    #[allow(dead_code)]
-    pub(super) struct Flag {
-        cfg_attrs: Vec<Attribute>,
-        const_attrs: Vec<Attribute>,
-        const_token: Token![const],
-        ident: Ident,
-        eq_token: Token![=],
-        value: Expr,
-    }
-
-    impl Parse for Flag {
-        fn parse(input: ParseStream) -> syn::Result<Self> {
-            let const_attrs = Attribute::parse_outer(input)?;
-            Ok(Self {
-                cfg_attrs: extract_cfgs(&const_attrs),
-                const_attrs,
-                const_token: input.parse()?,
-                ident: input.parse()?,
-                eq_token: input.parse()?,
-                value: input.parse()?,
-            })
-        }
-    }
-
-    impl Flag {
-        pub(super) fn cfg_attrs(&self) -> &[Attribute] {
-            &self.cfg_attrs
-        }
-
-        pub(super) fn ident(&self) -> &Ident {
-            &self.ident
-        }
-
-        pub(super) fn value(&self) -> &Expr {
-            &self.value
-        }
-    }
-
-    fn extract_cfgs(attrs: &[Attribute]) -> Vec<Attribute> {
-        let mut cfgs = vec![];
-
-        for attr in attrs {
-            if attr.path.is_ident("cfg") {
-                cfgs.push(attr.clone());
-            }
-        }
-
-        cfgs
-    }
-}
diff --git a/macros/src/cargo.rs b/macros/src/cargo.rs
new file mode 100644
index 0000000..839ca22
--- /dev/null
+++ b/macros/src/cargo.rs
@@ -0,0 +1,5 @@
+use std::env;
+
+pub(crate) fn package_name() -> String {
+    env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "<unknown>".to_string())
+}
diff --git a/macros/src/construct.rs b/macros/src/construct.rs
new file mode 100644
index 0000000..bc79052
--- /dev/null
+++ b/macros/src/construct.rs
@@ -0,0 +1,98 @@
+use std::{
+    collections::hash_map::DefaultHasher,
+    hash::{Hash as _, Hasher as _},
+};
+
+use proc_macro::Span;
+use proc_macro2::{Ident as Ident2, Span as Span2, TokenStream as TokenStream2};
+use quote::{format_ident, quote};
+use syn::{parse_quote, Expr, Ident, LitStr};
+
+pub(crate) use symbol::mangled as mangled_symbol_name;
+
+mod symbol;
+
+pub(crate) fn crate_local_disambiguator() -> u64 {
+    // We want a deterministic, but unique-per-macro-invocation identifier. For that we
+    // hash the call site `Span`'s debug representation, which contains a counter that
+    // should disambiguate macro invocations within a crate.
+    hash(&format!("{:?}", Span::call_site()))
+}
+
+pub(crate) fn escaped_expr_string(expr: &Expr) -> String {
+    quote!(#expr)
+        .to_string()
+        .replace("{", "{{")
+        .replace("}", "}}")
+}
+
+pub(crate) fn interned_string(string: &str, tag: &str, is_log_statement: bool) -> TokenStream2 {
+    // NOTE we rely on this variable name when extracting file location information from the DWARF
+    // without it we have no other mean to differentiate static variables produced by `info!` vs
+    // produced by `intern!` (or `internp`)
+    let var_name = if is_log_statement {
+        format_ident!("DEFMT_LOG_STATEMENT")
+    } else {
+        format_ident!("S")
+    };
+
+    let var_addr = if cfg!(feature = "unstable-test") {
+        quote!({ defmt::export::fetch_add_string_index() })
+    } else {
+        let var_item = static_variable(&var_name, string, tag);
+        quote!({
+            #var_item
+            &#var_name as *const u8 as u16
+        })
+    };
+
+    quote!({
+        defmt::export::make_istr(#var_addr)
+    })
+}
+
+/// work around restrictions on length and allowed characters imposed by macos linker
+/// returns (note the comma character for macos):
+///   under macos: ".defmt," + 16 character hex digest of symbol's hash
+///   otherwise:   ".defmt." + prefix + symbol
+pub(crate) fn linker_section(for_macos: bool, prefix: Option<&str>, symbol: &str) -> String {
+    let mut sub_section = if let Some(prefix) = prefix {
+        format!(".{}.{}", prefix, symbol)
+    } else {
+        format!(".{}", symbol)
+    };
+
+    if for_macos {
+        sub_section = format!(",{:x}", hash(&sub_section));
+    }
+
+    format!(".defmt{}", sub_section)
+}
+
+pub(crate) fn static_variable(name: &Ident2, data: &str, tag: &str) -> TokenStream2 {
+    let sym_name = mangled_symbol_name(tag, data);
+    let section = linker_section(false, None, &sym_name);
+    let section_for_macos = linker_section(true, None, &sym_name);
+
+    quote!(
+        #[cfg_attr(target_os = "macos", link_section = #section_for_macos)]
+        #[cfg_attr(not(target_os = "macos"), link_section = #section)]
+        #[export_name = #sym_name]
+        static #name: u8 = 0;
+    )
+}
+
+pub(crate) fn string_literal(content: &str) -> LitStr {
+    LitStr::new(content, Span2::call_site())
+}
+
+pub(crate) fn variable(name: &str) -> Expr {
+    let ident = Ident::new(name, Span2::call_site());
+    parse_quote!(#ident)
+}
+
+fn hash(string: &str) -> u64 {
+    let mut hasher = DefaultHasher::new();
+    string.hash(&mut hasher);
+    hasher.finish()
+}
diff --git a/macros/src/symbol.rs b/macros/src/construct/symbol.rs
similarity index 60%
rename from macros/src/symbol.rs
rename to macros/src/construct/symbol.rs
index d89257c..c75ea13 100644
--- a/macros/src/symbol.rs
+++ b/macros/src/construct/symbol.rs
@@ -1,11 +1,12 @@
-use std::collections::hash_map::DefaultHasher;
-use std::env;
 use std::fmt::Write;
-use std::hash::{Hash, Hasher};
 
-use proc_macro::Span;
+use crate::cargo;
 
-pub struct Symbol<'a> {
+pub(crate) fn mangled(defmt_tag: &str, data: &str) -> String {
+    Symbol::new(defmt_tag, data).mangle()
+}
+
+struct Symbol<'a> {
     /// Name of the Cargo package in which the symbol is being instantiated. Used for avoiding
     /// symbol name collisions.
     package: String,
@@ -35,51 +36,37 @@ pub struct Symbol<'a> {
 }
 
 impl<'a> Symbol<'a> {
-    pub fn new(tag: &'a str, data: &'a str) -> Self {
+    fn new(tag: &'a str, data: &'a str) -> Self {
         Self {
             // `CARGO_PKG_NAME` is set to the invoking package's name.
-            package: package(),
-            disambiguator: disambiguator(),
+            package: cargo::package_name(),
+            disambiguator: super::crate_local_disambiguator(),
             tag: format!("defmt_{}", tag),
             data,
         }
     }
 
-    pub fn mangle(&self) -> String {
+    fn mangle(&self) -> String {
         format!(
             r#"{{"package":"{}","tag":"{}","data":"{}","disambiguator":"{}"}}"#,
-            escape(&self.package),
-            escape(&self.tag),
-            escape(self.data),
+            json_escape(&self.package),
+            json_escape(&self.tag),
+            json_escape(self.data),
             self.disambiguator,
         )
     }
 }
 
-fn escape(s: &str) -> String {
-    let mut out = String::new();
-    for c in s.chars() {
+fn json_escape(string: &str) -> String {
+    let mut escaped = String::new();
+    for c in string.chars() {
         match c {
-            '\\' => out.push_str("\\\\"),
-            '\"' => out.push_str("\\\""),
-            '\n' => out.push_str("\\n"),
-            c if c.is_control() || c == '@' => write!(out, "\\u{:04x}", c as u32).unwrap(),
-            c => out.push(c),
+            '\\' => escaped.push_str("\\\\"),
+            '\"' => escaped.push_str("\\\""),
+            '\n' => escaped.push_str("\\n"),
+            c if c.is_control() || c == '@' => write!(escaped, "\\u{:04x}", c as u32).unwrap(),
+            c => escaped.push(c),
         }
     }
-    out
-}
-
-pub fn package() -> String {
-    env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "<unknown>".to_string())
-}
-
-pub fn disambiguator() -> u64 {
-    // We want a deterministic, but unique-per-macro-invocation identifier. For that we
-    // hash the call site `Span`'s debug representation, which contains a counter that
-    // should disambiguate macro invocations within a crate.
-    let s = format!("{:?}", Span::call_site());
-    let mut hasher = DefaultHasher::new();
-    s.hash(&mut hasher);
-    hasher.finish()
+    escaped
 }
diff --git a/macros/src/consts.rs b/macros/src/consts.rs
new file mode 100644
index 0000000..c8554b1
--- /dev/null
+++ b/macros/src/consts.rs
@@ -0,0 +1,2 @@
+/// The (erased) "Format" type as it appears in `defmt` format strings
+pub(crate) const TYPE_FORMAT: &str = "?";
diff --git a/macros/src/derives.rs b/macros/src/derives.rs
new file mode 100644
index 0000000..04cb176
--- /dev/null
+++ b/macros/src/derives.rs
@@ -0,0 +1,3 @@
+//! Procedural macros that are `#[derive(*)]` attributes
+
+pub(crate) mod format;
diff --git a/macros/src/derives/format.rs b/macros/src/derives/format.rs
new file mode 100644
index 0000000..a35a79e
--- /dev/null
+++ b/macros/src/derives/format.rs
@@ -0,0 +1,40 @@
+use proc_macro::TokenStream;
+use proc_macro_error::abort_call_site;
+use quote::quote;
+use syn::{parse_macro_input, Data, DeriveInput};
+
+mod codegen;
+
+pub(crate) fn expand(input: TokenStream) -> TokenStream {
+    let mut input = parse_macro_input!(input as DeriveInput);
+
+    let ident = &input.ident;
+    let codegen::EncodeData { format_tag, stmts } = match &input.data {
+        Data::Enum(data) => codegen::encode_enum_data(ident, data),
+        Data::Struct(data) => codegen::encode_struct_data(ident, data),
+        Data::Union(_) => abort_call_site!("`#[derive(Format)]` does not support unions"),
+    };
+
+    let codegen::Generics {
+        impl_generics,
+        type_generics,
+        where_clause,
+    } = codegen::Generics::codegen(&mut input.generics);
+
+    quote!(
+        impl #impl_generics defmt::Format for #ident #type_generics #where_clause {
+            fn format(&self, f: defmt::Formatter) {
+                unreachable!()
+            }
+
+            fn _format_tag() -> defmt::Str {
+                #format_tag
+            }
+
+            fn _format_data(&self) {
+                #(#stmts)*
+            }
+        }
+    )
+    .into()
+}
diff --git a/macros/src/derives/format/codegen.rs b/macros/src/derives/format/codegen.rs
new file mode 100644
index 0000000..bbbf7c6
--- /dev/null
+++ b/macros/src/derives/format/codegen.rs
@@ -0,0 +1,63 @@
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::{parse_quote, DataStruct, GenericParam, Ident, ImplGenerics, TypeGenerics, WhereClause};
+
+pub(crate) use enum_data::encode as encode_enum_data;
+
+use crate::construct;
+
+mod enum_data;
+mod fields;
+
+pub(crate) struct EncodeData {
+    pub(crate) format_tag: TokenStream2,
+    pub(crate) stmts: Vec<TokenStream2>,
+}
+
+pub(crate) fn encode_struct_data(ident: &Ident, data: &DataStruct) -> EncodeData {
+    let mut format_string = ident.to_string();
+    let mut stmts = vec![];
+    let mut field_patterns = vec![];
+
+    let encode_fields_stmts =
+        fields::codegen(&data.fields, &mut format_string, &mut field_patterns);
+
+    stmts.push(quote!(match self {
+        Self { #(#field_patterns),* } => {
+            #(#encode_fields_stmts;)*
+        }
+    }));
+
+    let format_tag = construct::interned_string(&format_string, "derived", false);
+    EncodeData { format_tag, stmts }
+}
+
+pub(crate) struct Generics<'a> {
+    pub(crate) impl_generics: ImplGenerics<'a>,
+    pub(crate) type_generics: TypeGenerics<'a>,
+    pub(crate) where_clause: WhereClause,
+}
+
+impl<'a> Generics<'a> {
+    pub(crate) fn codegen(generics: &'a mut syn::Generics) -> Self {
+        let mut where_clause = generics.make_where_clause().clone();
+        let (impl_generics, type_generics, _) = generics.split_for_impl();
+
+        // Extend where-clause with `Format` bounds for type parameters.
+        for param in &generics.params {
+            if let GenericParam::Type(ty) = param {
+                let ident = &ty.ident;
+
+                where_clause
+                    .predicates
+                    .push(parse_quote!(#ident: defmt::Format));
+            }
+        }
+
+        Self {
+            impl_generics,
+            type_generics,
+            where_clause,
+        }
+    }
+}
diff --git a/macros/src/derives/format/codegen/enum_data.rs b/macros/src/derives/format/codegen/enum_data.rs
new file mode 100644
index 0000000..4823245
--- /dev/null
+++ b/macros/src/derives/format/codegen/enum_data.rs
@@ -0,0 +1,109 @@
+use proc_macro2::TokenStream as TokenStream2;
+use proc_macro_error::abort_call_site;
+use quote::quote;
+use syn::{DataEnum, Ident};
+
+use crate::construct;
+
+use super::EncodeData;
+
+pub(crate) fn encode(ident: &Ident, data: &DataEnum) -> EncodeData {
+    if data.variants.is_empty() {
+        return EncodeData {
+            stmts: vec![quote!(match *self {})],
+            format_tag: construct::interned_string("!", "derived", false),
+        };
+    }
+
+    let mut format_string = String::new();
+
+    let mut match_arms = vec![];
+    let mut is_first_variant = true;
+    let discriminant_encoder = DiscriminantEncoder::new(data.variants.len());
+    let enum_ident = ident;
+    for (index, variant) in data.variants.iter().enumerate() {
+        let variant_ident = &variant.ident;
+
+        if is_first_variant {
+            is_first_variant = false;
+        } else {
+            format_string.push('|');
+        }
+        format_string.push_str(&variant_ident.to_string());
+
+        let mut field_patterns = vec![];
+        let encode_fields_stmts =
+            super::fields::codegen(&variant.fields, &mut format_string, &mut field_patterns);
+        let pattern = quote!( { #(#field_patterns),* } );
+
+        let encode_discriminant_stmt = discriminant_encoder.encode(index);
+
+        match_arms.push(quote!(
+            #enum_ident::#variant_ident #pattern => {
+                #encode_discriminant_stmt
+                #(#encode_fields_stmts;)*
+            }
+        ))
+    }
+
+    let format_tag = construct::interned_string(&format_string, "derived", false);
+    let stmts = vec![quote!(match self {
+        #(#match_arms)*
+    })];
+
+    EncodeData { format_tag, stmts }
+}
+
+enum DiscriminantEncoder {
+    Nop,
+    U8,
+    U16,
+    U32,
+    U64,
+}
+
+impl DiscriminantEncoder {
+    fn new(number_of_variants: usize) -> Self {
+        if number_of_variants == 1 {
+            Self::Nop
+        } else if number_of_variants <= usize::from(u8::MAX) {
+            Self::U8
+        } else if number_of_variants <= usize::from(u16::MAX) {
+            Self::U16
+        } else if number_of_variants as u128 > u128::from(u64::MAX) {
+            // unreachable on existing hardware?
+            abort_call_site!(
+                "`#[derive(Format)]` does not support enums with more than {} variants",
+                number_of_variants
+            )
+        } else if number_of_variants as u64 <= u64::from(u32::MAX) {
+            Self::U32
+        } else {
+            Self::U64
+        }
+    }
+
+    // NOTE this assumes `index` < `number_of_variants` used to construct `self`
+    fn encode(&self, index: usize) -> TokenStream2 {
+        match self {
+            // For single-variant enums, there is no need to encode the discriminant.
+            DiscriminantEncoder::Nop => quote!(),
+            DiscriminantEncoder::U8 => {
+                let index = index as u8;
+                quote!(defmt::export::u8(&#index);)
+            }
+            DiscriminantEncoder::U16 => {
+                let index = index as u16;
+                quote!(defmt::export::u16(&#index);)
+            }
+            DiscriminantEncoder::U32 => {
+                let index = index as u32;
+                quote!(defmt::export::u32(&#index);)
+            }
+            DiscriminantEncoder::U64 => {
+                let index = index as u64;
+                quote!(defmt::export::u64(&#index);)
+            }
+        }
+    }
+}
diff --git a/macros/src/derives/format/codegen/fields.rs b/macros/src/derives/format/codegen/fields.rs
new file mode 100644
index 0000000..c47d283
--- /dev/null
+++ b/macros/src/derives/format/codegen/fields.rs
@@ -0,0 +1,100 @@
+use std::fmt::Write as _;
+
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{Fields, Index, Type};
+
+use crate::consts;
+
+pub(crate) fn codegen(
+    fields: &Fields,
+    format_string: &mut String,
+    patterns: &mut Vec<TokenStream2>,
+) -> Vec<TokenStream2> {
+    let (fields, fields_are_named) = match fields {
+        Fields::Named(named) => (&named.named, true),
+        Fields::Unit => return vec![],
+        Fields::Unnamed(unnamed) => (&unnamed.unnamed, false),
+    };
+
+    if fields.is_empty() {
+        return vec![];
+    }
+
+    if fields_are_named {
+        format_string.push_str(" {{ ");
+    } else {
+        format_string.push('(');
+    }
+
+    let mut stmts = vec![];
+    let mut is_first = true;
+    for (index, field) in fields.iter().enumerate() {
+        if is_first {
+            is_first = false;
+        } else {
+            format_string.push_str(", ");
+        }
+
+        let ty = as_native_type(&field.ty).unwrap_or_else(|| consts::TYPE_FORMAT.to_string());
+        if let Some(ident) = field.ident.as_ref() {
+            write!(format_string, "{}: {{={}:?}}", ident, ty).ok();
+
+            if ty == consts::TYPE_FORMAT {
+                stmts.push(quote!(defmt::export::fmt(#ident)));
+            } else {
+                let method = format_ident!("{}", ty);
+                stmts.push(quote!(defmt::export::#method(#ident)));
+            }
+
+            patterns.push(quote!( #ident ));
+        } else {
+            // Unnamed (tuple) field.
+
+            write!(format_string, "{{={}}}", ty).ok();
+
+            let ident = format_ident!("arg{}", index);
+            if ty == consts::TYPE_FORMAT {
+                stmts.push(quote!(defmt::export::fmt(#ident)));
+            } else {
+                let method = format_ident!("{}", ty);
+                stmts.push(quote!(defmt::export::#method(#ident)));
+            }
+
+            let index = Index::from(index);
+            patterns.push(quote!( #index: #ident ));
+        }
+    }
+
+    if fields_are_named {
+        format_string.push_str(" }}");
+    } else {
+        format_string.push(')');
+    }
+
+    stmts
+}
+
+/// Returns `Some` if `ty` refers to a builtin Rust type that has native support from defmt and does
+/// not have to go through the `Format` trait.
+///
+/// This should return `Some` for all types that can be used as `{=TYPE}`.
+///
+/// Note: This is technically incorrect, since builtin types can be shadowed. However the efficiency
+/// gains are too big to pass up, so we expect user code to not do that.
+fn as_native_type(ty: &Type) -> Option<String> {
+    match ty {
+        Type::Path(path) => {
+            let ident = path.path.get_ident()?;
+            let ty_name = ident.to_string();
+
+            match &*ty_name {
+                "u8" | "u16" | "u32" | "usize" | "i8" | "i16" | "i32" | "isize" | "f32" | "f64"
+                | "bool" | "str" => Some(ty_name),
+                _ => None,
+            }
+        }
+        Type::Reference(ty_ref) => as_native_type(&ty_ref.elem),
+        _ => None,
+    }
+}
diff --git a/macros/src/function_like.rs b/macros/src/function_like.rs
new file mode 100644
index 0000000..d9bc116
--- /dev/null
+++ b/macros/src/function_like.rs
@@ -0,0 +1,8 @@
+pub(crate) mod assert_binop;
+pub(crate) mod assert_like;
+pub(crate) mod dbg;
+pub(crate) mod intern;
+pub(crate) mod internp;
+pub(crate) mod log;
+pub(crate) mod panic_like;
+pub(crate) mod write;
diff --git a/macros/src/function_like/assert_binop.rs b/macros/src/function_like/assert_binop.rs
new file mode 100644
index 0000000..3a877cd
--- /dev/null
+++ b/macros/src/function_like/assert_binop.rs
@@ -0,0 +1,90 @@
+use defmt_parser::Level;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, punctuated::Punctuated};
+
+use crate::{construct, function_like::log};
+
+use self::args::Args;
+
+mod args;
+
+pub(crate) fn eq(args: TokenStream) -> TokenStream {
+    expand(args, BinOp::Eq)
+}
+
+pub(crate) fn ne(args: TokenStream) -> TokenStream {
+    expand(args, BinOp::Ne)
+}
+
+fn expand(args: TokenStream, binop: BinOp) -> TokenStream {
+    let args = parse_macro_input!(args as Args);
+
+    let left = args.left;
+    let right = args.right;
+
+    let mut formatting_args = Punctuated::new();
+
+    let extra_string = if let Some(log_args) = args.log_args {
+        if let Some(args) = log_args.formatting_args {
+            formatting_args.extend(args);
+        }
+        format!(": {}", log_args.format_string.value())
+    } else {
+        String::new()
+    };
+
+    let vals = match binop {
+        BinOp::Eq => &["left_val", "right_val"][..],
+        BinOp::Ne => &["left_val"][..],
+    };
+
+    for val in vals {
+        formatting_args.push(construct::variable(*val));
+    }
+
+    let panic_msg = match binop {
+        BinOp::Eq => format!(
+            "panicked at 'assertion failed: `(left == right)`{}'
+ left: `{{:?}}`
+right: `{{:?}}`",
+            extra_string
+        ),
+        BinOp::Ne => format!(
+            "panicked at 'assertion failed: `(left != right)`{}'
+left/right: `{{:?}}`",
+            extra_string
+        ),
+    };
+
+    let log_args = log::Args {
+        format_string: construct::string_literal(&panic_msg),
+        formatting_args: Some(formatting_args),
+    };
+    let log_stmt = log::expand_parsed(Level::Error, log_args);
+
+    let mut cond = quote!(*left_val == *right_val);
+    if binop == BinOp::Eq {
+        cond = quote!(!(#cond));
+    }
+
+    quote!(
+        // evaluate arguments first
+        match (&(#left), &(#right)) {
+            (left_val, right_val) => {
+                // following `core::assert_eq!`
+                if #cond {
+                    #log_stmt;
+                    defmt::export::panic()
+                }
+            }
+        }
+    )
+    .into()
+}
+
+#[derive(PartialEq)]
+enum BinOp {
+    Eq,
+    Ne,
+}
diff --git a/macros/src/function_like/assert_binop/args.rs b/macros/src/function_like/assert_binop/args.rs
new file mode 100644
index 0000000..dd44981
--- /dev/null
+++ b/macros/src/function_like/assert_binop/args.rs
@@ -0,0 +1,47 @@
+use syn::{
+    parse::{self, Parse, ParseStream},
+    Expr, Token,
+};
+
+use crate::function_like::log;
+
+pub(crate) struct Args {
+    pub(crate) left: Expr,
+    pub(crate) right: Expr,
+    pub(crate) log_args: Option<log::Args>,
+}
+
+impl Parse for Args {
+    fn parse(input: ParseStream) -> parse::Result<Self> {
+        let left = input.parse()?;
+        let _comma: Token![,] = input.parse()?;
+        let right = input.parse()?;
+
+        if input.is_empty() {
+            // assert_eq!(a, b)
+            return Ok(Args {
+                left,
+                right,
+                log_args: None,
+            });
+        }
+
+        let _comma: Token![,] = input.parse()?;
+
+        if input.is_empty() {
+            // assert_eq!(a, b,)
+            Ok(Args {
+                left,
+                right,
+                log_args: None,
+            })
+        } else {
+            // assert_eq!(a, b, "c", d)
+            Ok(Args {
+                left,
+                right,
+                log_args: Some(input.parse()?),
+            })
+        }
+    }
+}
diff --git a/macros/src/function_like/assert_like.rs b/macros/src/function_like/assert_like.rs
new file mode 100644
index 0000000..a90bcaf
--- /dev/null
+++ b/macros/src/function_like/assert_like.rs
@@ -0,0 +1,43 @@
+use syn::{
+    parse::{self, Parse, ParseStream},
+    Expr, Token,
+};
+
+use super::log;
+
+pub(crate) mod assert;
+pub(crate) mod unwrap;
+
+struct Args {
+    condition: Expr,
+    log_args: Option<log::Args>,
+}
+
+impl Parse for Args {
+    fn parse(input: ParseStream) -> parse::Result<Self> {
+        let condition = input.parse()?;
+        if input.is_empty() {
+            // assert!(a)
+            return Ok(Args {
+                log_args: None,
+                condition,
+            });
+        }
+
+        let _comma: Token![,] = input.parse()?;
+
+        if input.is_empty() {
+            // assert!(a,)
+            Ok(Args {
+                log_args: None,
+                condition,
+            })
+        } else {
+            // assert!(a, "b", c)
+            Ok(Args {
+                log_args: Some(input.parse()?),
+                condition,
+            })
+        }
+    }
+}
diff --git a/macros/src/function_like/assert_like/assert.rs b/macros/src/function_like/assert_like/assert.rs
new file mode 100644
index 0000000..b787c6f
--- /dev/null
+++ b/macros/src/function_like/assert_like/assert.rs
@@ -0,0 +1,39 @@
+use defmt_parser::Level;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::{construct, function_like::log};
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let args = parse_macro_input!(args as super::Args);
+
+    let condition = args.condition;
+    let (format_string, formatting_args) = if let Some(log_args) = args.log_args {
+        let format_string = format!("panicked at '{}'", log_args.format_string.value());
+        (format_string, log_args.formatting_args)
+    } else {
+        let format_string = format!(
+            "panicked at 'assertion failed: {}'",
+            construct::escaped_expr_string(&condition)
+        );
+        (format_string, None)
+    };
+
+    let format_string = construct::string_literal(&format_string);
+    let log_stmt = log::expand_parsed(
+        Level::Error,
+        log::Args {
+            format_string,
+            formatting_args,
+        },
+    );
+
+    quote!(
+        if !(#condition) {
+            #log_stmt;
+            defmt::export::panic()
+        }
+    )
+    .into()
+}
diff --git a/macros/src/function_like/assert_like/unwrap.rs b/macros/src/function_like/assert_like/unwrap.rs
new file mode 100644
index 0000000..eef6f91
--- /dev/null
+++ b/macros/src/function_like/assert_like/unwrap.rs
@@ -0,0 +1,46 @@
+use defmt_parser::Level;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, punctuated::Punctuated};
+
+use crate::{construct, function_like::log};
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let args = parse_macro_input!(args as super::Args);
+
+    let condition = args.condition;
+    let (format_string, formatting_args) = if let Some(log_args) = args.log_args {
+        let format_string = format!("panicked at '{}'", log_args.format_string.value());
+        (format_string, log_args.formatting_args)
+    } else {
+        let format_string = format!(
+            "panicked at 'unwrap failed: {}'\nerror: `{{:?}}`",
+            construct::escaped_expr_string(&condition)
+        );
+
+        let mut formatting_args = Punctuated::new();
+        formatting_args.push(construct::variable("_unwrap_err"));
+
+        (format_string, Some(formatting_args))
+    };
+
+    let format_string = construct::string_literal(&format_string);
+    let log_stmt = log::expand_parsed(
+        Level::Error,
+        log::Args {
+            format_string,
+            formatting_args,
+        },
+    );
+
+    quote!(
+        match defmt::export::into_result(#condition) {
+            ::core::result::Result::Ok(res) => res,
+            ::core::result::Result::Err(_unwrap_err) => {
+                #log_stmt;
+                defmt::export::panic()
+            }
+        }
+    )
+    .into()
+}
diff --git a/macros/src/function_like/dbg.rs b/macros/src/function_like/dbg.rs
new file mode 100644
index 0000000..4706725
--- /dev/null
+++ b/macros/src/function_like/dbg.rs
@@ -0,0 +1,40 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::construct;
+
+use self::args::Args;
+
+mod args;
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let args = parse_macro_input!(args as Args);
+    codegen(&args)
+}
+
+fn codegen(args: &Args) -> TokenStream {
+    let tuple_exprs = args
+        .exprs
+        .iter()
+        .map(|expr| {
+            let escaped_expr = construct::escaped_expr_string(&expr);
+            let format_string = format!("{} = {{}}", escaped_expr);
+
+            quote!(match #expr {
+                tmp => {
+                    defmt::trace!(#format_string, tmp);
+                    tmp
+                }
+            })
+        })
+        .collect::<Vec<_>>();
+
+    if tuple_exprs.is_empty() {
+        // for compatibility with `std::dbg!` we also emit a TRACE log in this case
+        quote!(defmt::trace!(""))
+    } else {
+        quote![ (#(#tuple_exprs),*) ]
+    }
+    .into()
+}
diff --git a/macros/src/function_like/dbg/args.rs b/macros/src/function_like/dbg/args.rs
new file mode 100644
index 0000000..19759b9
--- /dev/null
+++ b/macros/src/function_like/dbg/args.rs
@@ -0,0 +1,17 @@
+use syn::{
+    parse::{Parse, ParseStream},
+    punctuated::Punctuated,
+    Expr, Token,
+};
+
+pub(crate) struct Args {
+    pub(crate) exprs: Punctuated<Expr, Token![,]>,
+}
+
+impl Parse for Args {
+    fn parse(input: ParseStream) -> Result<Self, syn::Error> {
+        Ok(Self {
+            exprs: Punctuated::parse_terminated(input)?,
+        })
+    }
+}
diff --git a/macros/src/function_like/intern.rs b/macros/src/function_like/intern.rs
new file mode 100644
index 0000000..755b9b7
--- /dev/null
+++ b/macros/src/function_like/intern.rs
@@ -0,0 +1,9 @@
+use proc_macro::TokenStream;
+use syn::{parse_macro_input, LitStr};
+
+use crate::construct;
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let literal = parse_macro_input!(args as LitStr);
+    construct::interned_string(&literal.value(), "str", false).into()
+}
diff --git a/macros/src/function_like/internp.rs b/macros/src/function_like/internp.rs
new file mode 100644
index 0000000..c75a330
--- /dev/null
+++ b/macros/src/function_like/internp.rs
@@ -0,0 +1,31 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, LitStr};
+
+use crate::construct;
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let literal = parse_macro_input!(args as LitStr);
+    let sym_name = construct::mangled_symbol_name("prim", &literal.value());
+
+    let prefix = Some("prim");
+    let section = construct::linker_section(false, prefix, &sym_name);
+    let section_for_macos = construct::linker_section(true, prefix, &sym_name);
+
+    let var_addr = if cfg!(feature = "unstable-test") {
+        quote!({ defmt::export::fetch_add_string_index() as u16 })
+    } else {
+        quote!({
+            #[cfg_attr(target_os = "macos", link_section = #section_for_macos)]
+            #[cfg_attr(not(target_os = "macos"), link_section = #section)]
+            #[export_name = #sym_name]
+            static S: u8 = 0;
+            &S as *const u8 as u16
+        })
+    };
+
+    quote!({
+        defmt::export::make_istr(#var_addr)
+    })
+    .into()
+}
diff --git a/macros/src/function_like/log.rs b/macros/src/function_like/log.rs
new file mode 100644
index 0000000..6dd25c3
--- /dev/null
+++ b/macros/src/function_like/log.rs
@@ -0,0 +1,98 @@
+use defmt_parser::{Level, ParserMode};
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use proc_macro_error::abort;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::construct;
+
+pub(crate) use self::{args::Args, codegen::Codegen};
+
+mod args;
+mod codegen;
+
+pub(crate) fn expand(level: Level, args: TokenStream) -> TokenStream {
+    expand_parsed(level, parse_macro_input!(args as Args)).into()
+}
+
+pub(crate) fn expand_parsed(level: Level, args: Args) -> TokenStream2 {
+    let format_string = args.format_string.value();
+    let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
+        Ok(args) => args,
+        Err(e) => abort!(args.format_string, "{}", e),
+    };
+
+    let formatting_exprs = args
+        .formatting_args
+        .map(|punctuated| punctuated.into_iter().collect())
+        .unwrap_or_else(Vec::new);
+
+    let Codegen { patterns, exprs } = Codegen::new(
+        &fragments,
+        formatting_exprs.len(),
+        args.format_string.span(),
+    );
+
+    let header = construct::interned_string(&format_string, level.as_str(), true);
+    let logging_enabled = cfg_if_logging_enabled(level);
+    quote!({
+        #[cfg(#logging_enabled)] {
+            match (#(&(#formatting_exprs)),*) {
+                (#(#patterns),*) => {
+                    defmt::export::acquire();
+                    defmt::export::header(&#header);
+                    #(#exprs;)*
+                    defmt::export::release()
+                }
+            }
+        }
+        // if logging is disabled match args, so they are not unused
+        #[cfg(not(#logging_enabled))]
+        match (#(&(#formatting_exprs)),*) {
+            _ => {}
+        }
+    })
+}
+
+fn cfg_if_logging_enabled(level: Level) -> TokenStream2 {
+    let features_dev = necessary_features_for_level(level, true);
+    let features_release = necessary_features_for_level(level, false);
+
+    quote!(
+        any(
+            all(    debug_assertions,  any(#( feature = #features_dev     ),*)),
+            all(not(debug_assertions), any(#( feature = #features_release ),*))
+        )
+    )
+}
+
+/// Returns a list of features of which one has to be enabled for `level` to be active
+///
+/// * `debug_assertions == true` means that dev profile is enabled
+/// * `"defmt-default"` is enabled for dev & release profile so debug_assertions does not matter
+fn necessary_features_for_level(level: Level, debug_assertions: bool) -> &'static [&'static str] {
+    match level {
+        Level::Trace if debug_assertions => &["defmt-trace", "defmt-default"],
+        Level::Debug if debug_assertions => &["defmt-debug", "defmt-trace", "defmt-default"],
+
+        Level::Trace => &["defmt-trace"],
+        Level::Debug => &["defmt-debug", "defmt-trace"],
+        Level::Info => &["defmt-info", "defmt-debug", "defmt-trace", "defmt-default"],
+        Level::Warn => &[
+            "defmt-warn",
+            "defmt-info",
+            "defmt-debug",
+            "defmt-trace",
+            "defmt-default",
+        ],
+        Level::Error => &[
+            "defmt-error",
+            "defmt-warn",
+            "defmt-info",
+            "defmt-debug",
+            "defmt-trace",
+            "defmt-default",
+        ],
+    }
+}
diff --git a/macros/src/function_like/log/args.rs b/macros/src/function_like/log/args.rs
new file mode 100644
index 0000000..ee7493c
--- /dev/null
+++ b/macros/src/function_like/log/args.rs
@@ -0,0 +1,24 @@
+use syn::{
+    parse::{self, Parse, ParseStream},
+    punctuated::Punctuated,
+    Expr, LitStr, Token,
+};
+
+pub(crate) struct Args {
+    pub(crate) format_string: LitStr,
+    pub(crate) formatting_args: Option<Punctuated<Expr, Token![,]>>,
+}
+
+impl Parse for Args {
+    fn parse(input: ParseStream) -> parse::Result<Self> {
+        Ok(Self {
+            format_string: input.parse()?,
+            formatting_args: if input.is_empty() {
+                None
+            } else {
+                let _comma: Token![,] = input.parse()?;
+                Some(Punctuated::parse_terminated(input)?)
+            },
+        })
+    }
+}
diff --git a/macros/src/function_like/log/codegen.rs b/macros/src/function_like/log/codegen.rs
new file mode 100644
index 0000000..5c0a9e0
--- /dev/null
+++ b/macros/src/function_like/log/codegen.rs
@@ -0,0 +1,140 @@
+use defmt_parser::{Fragment, Parameter, Type};
+use proc_macro2::{Ident as Ident2, Span as Span2, TokenStream as TokenStream2};
+use proc_macro_error::abort;
+use quote::{format_ident, quote};
+
+pub(crate) struct Codegen {
+    pub(crate) exprs: Vec<TokenStream2>,
+    pub(crate) patterns: Vec<Ident2>,
+}
+
+impl Codegen {
+    pub(crate) fn new(fragments: &[Fragment<'_>], given_arg_count: usize, span: Span2) -> Self {
+        let params = fragments
+            .iter()
+            .filter_map(|frag| match frag {
+                Fragment::Parameter(param) => Some(param.clone()),
+                Fragment::Literal(_) => None,
+            })
+            .collect::<Vec<_>>();
+
+        let expected_arg_count = params
+            .iter()
+            .map(|param| param.index + 1)
+            .max()
+            .unwrap_or(0);
+
+        if given_arg_count != expected_arg_count {
+            let mut only = "";
+            if given_arg_count < expected_arg_count {
+                only = "only ";
+            }
+
+            abort!(
+                span,
+                "format string requires {} arguments but {}{} were provided",
+                expected_arg_count,
+                only,
+                given_arg_count
+            )
+        }
+
+        let mut exprs = vec![];
+        let mut patterns = vec![];
+
+        for arg_index in 0..expected_arg_count {
+            let arg_ident = format_ident!("arg{}", arg_index);
+            let matching_param = params
+                .iter()
+                .find(|param| param.index == arg_index)
+                .unwrap();
+
+            let expr = encode_arg(&matching_param.ty, &params, arg_index, &arg_ident);
+
+            exprs.push(expr);
+            patterns.push(arg_ident);
+        }
+
+        Codegen { exprs, patterns }
+    }
+}
+
+fn encode_arg(ty: &Type, params: &[Parameter], arg_index: usize, arg: &Ident2) -> TokenStream2 {
+    match ty {
+        Type::I8 => quote!(defmt::export::i8(#arg)),
+        Type::I16 => quote!(defmt::export::i16(#arg)),
+        Type::I32 => quote!(defmt::export::i32(#arg)),
+        Type::I64 => quote!(defmt::export::i64(#arg)),
+        Type::I128 => quote!(defmt::export::i128(#arg)),
+        Type::Isize => quote!(defmt::export::isize(#arg)),
+
+        Type::U8 => quote!(defmt::export::u8(#arg)),
+        Type::U16 => quote!(defmt::export::u16(#arg)),
+        Type::U32 => quote!(defmt::export::u32(#arg)),
+        Type::U64 => quote!(defmt::export::u64(#arg)),
+        Type::U128 => quote!(defmt::export::u128(#arg)),
+        Type::Usize => quote!(defmt::export::usize(#arg)),
+
+        Type::F32 => quote!(defmt::export::f32(#arg)),
+        Type::F64 => quote!(defmt::export::f64(#arg)),
+
+        Type::Bool => quote!(defmt::export::bool(#arg)),
+
+        Type::Str => quote!(defmt::export::str(#arg)),
+        Type::IStr => quote!(defmt::export::istr(#arg)),
+        Type::Char => quote!(defmt::export::char(#arg)),
+
+        Type::Format => quote!(defmt::export::fmt(#arg)),
+        Type::FormatSlice => quote!(defmt::export::fmt_slice(#arg)),
+        Type::FormatArray(len) => quote!(defmt::export::fmt_array({
+            let tmp: &[_; #len] = #arg;
+            tmp
+        })),
+
+        Type::Debug => quote!(defmt::export::debug(#arg)),
+        Type::Display => quote!(defmt::export::display(#arg)),
+        Type::FormatSequence => unreachable!(),
+
+        Type::U8Slice => quote!(defmt::export::slice(#arg)),
+
+        // We cast to the expected array type (which should be a no-op cast) to provoke
+        // a type mismatch error on mismatched lengths:
+        // ``Symbol’s value as variable is void: //
+        Type::U8Array(len) => quote!(defmt::export::u8_array({
+            let tmp: &[u8; #len] = #arg;
+            tmp
+        })),
+
+        Type::BitField(_) => {
+            let all_bitfields = params.iter().filter(|param| param.index == arg_index);
+            let (smallest_bit_index, largest_bit_index) =
+                defmt_parser::get_max_bitfield_range(all_bitfields).unwrap();
+
+            // indices of the lowest and the highest octet which contains bitfield-relevant data
+            let lowest_byte = smallest_bit_index / 8;
+            let highest_byte = (largest_bit_index - 1) / 8;
+            let truncated_sz = highest_byte - lowest_byte + 1; // in bytes
+
+            // shift away unneeded lower octet
+            // TODO: create helper for shifting because readability
+            match truncated_sz {
+                1 => {
+                    quote!(defmt::export::u8(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
+                }
+                2 => {
+                    quote!(defmt::export::u16(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
+                }
+                3..=4 => {
+                    quote!(defmt::export::u32(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
+                }
+                5..=8 => {
+                    quote!(defmt::export::u64(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
+                }
+                9..=16 => {
+                    quote!(defmt::export::u128(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))
+                }
+                _ => unreachable!(),
+            }
+        }
+    }
+}
diff --git a/macros/src/function_like/panic_like.rs b/macros/src/function_like/panic_like.rs
new file mode 100644
index 0000000..32fa75c
--- /dev/null
+++ b/macros/src/function_like/panic_like.rs
@@ -0,0 +1,42 @@
+use std::borrow::Cow;
+
+use defmt_parser::Level;
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::{construct, function_like::log};
+
+pub(crate) fn expand(
+    args: TokenStream,
+    zero_args_format_string: &str,
+    transform_format_string: impl FnOnce(&str) -> String,
+) -> TokenStream {
+    let (format_string, formatting_args) = if args.is_empty() {
+        // panic!() -> error!("panicked at 'explicit panic'")
+        (Cow::from(zero_args_format_string), None)
+    } else {
+        // panic!("a", b, c) -> error!("panicked at 'a'", b, c)
+        let log_args = parse_macro_input!(args as log::Args);
+        let format_string = transform_format_string(&log_args.format_string.value());
+
+        (Cow::from(format_string), log_args.formatting_args)
+    };
+
+    let format_string = construct::string_literal(&format_string);
+    let log_stmt = log::expand_parsed(
+        Level::Error,
+        log::Args {
+            format_string,
+            formatting_args,
+        },
+    );
+
+    quote!(
+        {
+            #log_stmt;
+            defmt::export::panic()
+        }
+    )
+    .into()
+}
diff --git a/macros/src/function_like/write.rs b/macros/src/function_like/write.rs
new file mode 100644
index 0000000..154e95b
--- /dev/null
+++ b/macros/src/function_like/write.rs
@@ -0,0 +1,48 @@
+use defmt_parser::ParserMode;
+use proc_macro::TokenStream;
+use proc_macro_error::abort;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::{construct, function_like::log};
+
+use self::args::Args;
+
+mod args;
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let Args {
+        formatter,
+        log_args,
+        ..
+    } = parse_macro_input!(args as Args);
+
+    let format_string = log_args.format_string.value();
+    let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
+        Ok(args) => args,
+        Err(e) => abort!(log_args.format_string, "{}", e),
+    };
+
+    let formatting_exprs: Vec<_> = log_args
+        .formatting_args
+        .map(|punctuated| punctuated.into_iter().collect())
+        .unwrap_or_default();
+
+    let log::Codegen { patterns, exprs } = log::Codegen::new(
+        &fragments,
+        formatting_exprs.len(),
+        log_args.format_string.span(),
+    );
+
+    let format_tag = construct::interned_string(&format_string, "write", false);
+    quote!({
+        let _typecheck_formatter: defmt::Formatter<'_> = #formatter;
+        match (#(&(#formatting_exprs)),*) {
+            (#(#patterns),*) => {
+                defmt::export::istr(&#format_tag);
+                #(#exprs;)*
+            }
+        }
+    })
+    .into()
+}
diff --git a/macros/src/function_like/write/args.rs b/macros/src/function_like/write/args.rs
new file mode 100644
index 0000000..5846c1e
--- /dev/null
+++ b/macros/src/function_like/write/args.rs
@@ -0,0 +1,22 @@
+use syn::{
+    parse::{self, Parse, ParseStream},
+    Expr, Token,
+};
+
+use crate::function_like::log;
+
+pub(crate) struct Args {
+    pub(crate) formatter: Expr,
+    _comma: Token![,],
+    pub(crate) log_args: log::Args,
+}
+
+impl Parse for Args {
+    fn parse(input: ParseStream) -> parse::Result<Self> {
+        Ok(Self {
+            formatter: input.parse()?,
+            _comma: input.parse()?,
+            log_args: input.parse()?,
+        })
+    }
+}
diff --git a/macros/src/items.rs b/macros/src/items.rs
new file mode 100644
index 0000000..5d0bac1
--- /dev/null
+++ b/macros/src/items.rs
@@ -0,0 +1,4 @@
+//! Procedural macros that expand to items
+
+pub(crate) mod bitflags;
+pub(crate) mod timestamp;
diff --git a/macros/src/items/bitflags.rs b/macros/src/items/bitflags.rs
new file mode 100644
index 0000000..a6445c3
--- /dev/null
+++ b/macros/src/items/bitflags.rs
@@ -0,0 +1,91 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{quote, ToTokens};
+use syn::parse_macro_input;
+
+use crate::{cargo, construct};
+
+use self::input::Input;
+
+mod input;
+
+pub(crate) fn expand(input: TokenStream) -> TokenStream {
+    let bitflags_input = TokenStream2::from(input.clone());
+    let input = parse_macro_input!(input as Input);
+
+    // Encode package and disambiguator to provide the decoder with all info it needs (even if
+    // technically redundant, since it's also stored in the symbol we create).
+    let format_string = format!(
+        "{{={}:__internal_bitflags_{}@{}@{}}}",
+        input.ty().to_token_stream(),
+        input.ident(),
+        cargo::package_name(),
+        construct::crate_local_disambiguator(),
+    );
+    let format_tag = construct::interned_string(&format_string, "bitflags", false);
+
+    let ident = input.ident();
+    let ty = input.ty();
+    let flag_statics = codegen_flag_statics(&input);
+    quote!(
+        const _: () = {
+            fn assert<T: defmt::export::UnsignedInt>() {}
+            assert::<#ty>;
+
+            #(#flag_statics)*
+        };
+
+        defmt::export::bitflags! {
+            #bitflags_input
+        }
+
+        impl defmt::Format for #ident {
+            fn format(&self, f: defmt::Formatter) {
+                unreachable!()
+            }
+
+            fn _format_tag() -> defmt::Str {
+                #format_tag
+            }
+
+            fn _format_data(&self) {
+                // There's a method available for every supported bitflags type.
+                defmt::export::#ty(&self.bits());
+            }
+        }
+
+    )
+    .into()
+}
+
+fn codegen_flag_statics(input: &Input) -> Vec<TokenStream2> {
+    input
+        .flags()
+        .map(|flag| {
+            let cfg_attrs = flag.cfg_attrs();
+            let var_name = &flag.ident();
+            let value = &flag.value();
+            let repr_ty = &input.ty();
+
+            let sym_name = construct::mangled_symbol_name(
+                "bitflags_value",
+                &format!("{}::{}", input.ident(), flag.ident()),
+            );
+
+            quote! {
+                #(#cfg_attrs)*
+                #[cfg_attr(target_os = "macos", link_section = ".defmt,end")]
+                #[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end")]
+                #[export_name = #sym_name]
+                static #var_name: u128 = {
+                    // NB: It might be tempting to just do `#value as u128` here, but that
+                    // causes a value such as `1 << 127` to be evaluated as an `i32`, which
+                    // overflows. So we instead coerce (but don't cast) it to the bitflags' raw
+                    // type, and then cast that to u128.
+                    let coerced_value: #repr_ty = #value;
+                    coerced_value as u128
+                };
+            }
+        })
+        .collect::<Vec<_>>()
+}
diff --git a/macros/src/items/bitflags/input.rs b/macros/src/items/bitflags/input.rs
new file mode 100644
index 0000000..8c277ba
--- /dev/null
+++ b/macros/src/items/bitflags/input.rs
@@ -0,0 +1,97 @@
+use syn::{
+    parse::{Parse, ParseStream},
+    punctuated::Punctuated,
+    token, Attribute, Expr, Ident, Token, Type, Visibility,
+};
+
+#[allow(dead_code)]
+pub(super) struct Input {
+    struct_attrs: Vec<Attribute>,
+    vis: Visibility,
+    struct_token: Token![struct],
+    ident: Ident,
+    colon_token: Token![:],
+    ty: Type,
+    brace_token: token::Brace,
+    flags: Punctuated<Flag, Token![;]>,
+}
+
+impl Parse for Input {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let flags;
+        Ok(Self {
+            struct_attrs: Attribute::parse_outer(input)?,
+            vis: input.parse()?,
+            struct_token: input.parse()?,
+            ident: input.parse()?,
+            colon_token: input.parse()?,
+            ty: input.parse()?,
+            brace_token: syn::braced!(flags in input),
+            flags: Punctuated::parse_terminated(&flags)?,
+        })
+    }
+}
+
+impl Input {
+    pub(super) fn flags(&self) -> impl Iterator<Item = &Flag> {
+        self.flags.iter()
+    }
+
+    pub(super) fn ident(&self) -> &Ident {
+        &self.ident
+    }
+
+    pub(super) fn ty(&self) -> &Type {
+        &self.ty
+    }
+}
+
+#[allow(dead_code)]
+pub(super) struct Flag {
+    cfg_attrs: Vec<Attribute>,
+    const_attrs: Vec<Attribute>,
+    const_token: Token![const],
+    ident: Ident,
+    eq_token: Token![=],
+    value: Expr,
+}
+
+impl Parse for Flag {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let const_attrs = Attribute::parse_outer(input)?;
+        Ok(Self {
+            cfg_attrs: extract_cfgs(&const_attrs),
+            const_attrs,
+            const_token: input.parse()?,
+            ident: input.parse()?,
+            eq_token: input.parse()?,
+            value: input.parse()?,
+        })
+    }
+}
+
+impl Flag {
+    pub(super) fn cfg_attrs(&self) -> &[Attribute] {
+        &self.cfg_attrs
+    }
+
+    pub(super) fn ident(&self) -> &Ident {
+        &self.ident
+    }
+
+    pub(super) fn value(&self) -> &Expr {
+        &self.value
+    }
+}
+
+fn extract_cfgs(attrs: &[Attribute]) -> Vec<Attribute> {
+    let mut cfgs = vec![];
+
+    for attr in attrs {
+        if attr.path.is_ident("cfg") {
+            cfgs.push(attr.clone());
+        }
+    }
+
+    cfgs
+}
diff --git a/macros/src/items/timestamp.rs b/macros/src/items/timestamp.rs
new file mode 100644
index 0000000..78c5bef
--- /dev/null
+++ b/macros/src/items/timestamp.rs
@@ -0,0 +1,58 @@
+use defmt_parser::ParserMode;
+use proc_macro::TokenStream;
+use proc_macro_error::abort;
+use quote::format_ident;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::{construct, function_like::log};
+
+pub(crate) fn expand(args: TokenStream) -> TokenStream {
+    let args = parse_macro_input!(args as log::Args);
+
+    let format_string = args.format_string.value();
+
+    let fragments = match defmt_parser::parse(&format_string, ParserMode::Strict) {
+        Ok(args) => args,
+        Err(e) => abort!(args.format_string, "{}", e),
+    };
+
+    let formatting_exprs: Vec<_> = args
+        .formatting_args
+        .map(|punctuated| punctuated.into_iter().collect())
+        .unwrap_or_default();
+
+    let log::Codegen { patterns, exprs } = log::Codegen::new(
+        &fragments,
+        formatting_exprs.len(),
+        args.format_string.span(),
+    );
+
+    let var_name = format_ident!("S");
+    let var_item = construct::static_variable(&var_name, &format_string, "timestamp");
+
+    quote!(
+        const _: () = {
+            #[export_name = "_defmt_timestamp"]
+            fn defmt_timestamp(fmt: ::defmt::Formatter<'_>) {
+                match (#(&(#formatting_exprs)),*) {
+                    (#(#patterns),*) => {
+                    // NOTE: No format string index, and no finalize call.
+                        #(#exprs;)*
+                    }
+                }
+            }
+
+            #var_item;
+
+            // Unique symbol name to prevent multiple `timestamp!` invocations in the crate graph.
+            // Uses `#var_name` to ensure it is not discarded by the linker.
+            // This symbol itself is retained via a `EXTERN` directive in the linker script.
+            #[no_mangle]
+            #[cfg_attr(target_os = "macos", link_section = ".defmt,end.timestamp")]
+            #[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end.timestamp")]
+            static __DEFMT_MARKER_TIMESTAMP_WAS_DEFINED: &u8 = &#var_name;
+        };
+    )
+    .into()
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index fdef2f9..2bf4778 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -2,775 +2,72 @@
 
 #![doc(html_logo_url = "https://knurling.ferrous-systems.com/knurling_logo_light_text.svg")]
 
-mod bitflags;
-mod symbol;
-
-use std::{
-    collections::hash_map::DefaultHasher,
-    convert::TryFrom,
-    fmt::Write as _,
-    hash::{Hash, Hasher},
-};
-
-use defmt_parser::{Fragment, Level, ParserMode};
+use defmt_parser::Level;
 use proc_macro::TokenStream;
-use proc_macro2::{Ident as Ident2, Span as Span2, TokenStream as TokenStream2};
-use quote::{format_ident, quote};
-use syn::{
-    parse::{self, Parse, ParseStream},
-    parse_macro_input,
-    punctuated::Punctuated,
-    spanned::Spanned as _,
-    Attribute, Data, DeriveInput, Expr, ExprPath, Fields, FieldsNamed, FieldsUnnamed, GenericParam,
-    ItemFn, ItemStruct, LitStr, Path, PathArguments, PathSegment, ReturnType, Token, Type,
-    WhereClause, WherePredicate,
-};
-
-/// Checks if any attribute in `attrs_to_check` is in `reject_list` and returns a compiler error if there's a match
-///
-/// The compiler error will indicate that the attribute conflicts with `attr_name`
-fn check_attribute_conflicts(
-    attr_name: &str,
-    attrs_to_check: &[Attribute],
-    reject_list: &[&str],
-) -> parse::Result<()> {
-    for attr in attrs_to_check {
-        if let Some(ident) = attr.path.get_ident() {
-            let ident = ident.to_string();
-            if reject_list.contains(&&*ident) {
-                let message = format!(
-                    "`#[{}]` attribute cannot be used together with `#[{}]`",
-                    attr_name, ident
-                );
-                return Err(parse::Error::new(attr.span(), message));
-            }
-        }
-    }
-    Ok(())
-}
-
+use proc_macro2::TokenStream as TokenStream2;
+use proc_macro_error::proc_macro_error;
+use quote::quote;
+
+mod attributes;
+mod cargo;
+mod construct;
+mod consts;
+mod derives;
+mod function_like;
+mod items;
+
+// NOTE some proc-macro functions have an `_` (underscore) suffix. This is intentional.
+// If left unsuffixed the procedural macros would shadow macros defined in `std` (like `assert`)
+// within the context of this entire crate, which leads to lots of confusion.
+
+/* # Attributes */
 #[proc_macro_attribute]
-pub fn global_logger(args: TokenStream, input: TokenStream) -> TokenStream {
-    if !args.is_empty() {
-        return parse::Error::new(
-            Span2::call_site(),
-            "`#[global_logger]` attribute takes no arguments",
-        )
-        .to_compile_error()
-        .into();
-    }
-    let s = parse_macro_input!(input as ItemStruct);
-    let ident = &s.ident;
-    let is_unit = matches!(s.fields, Fields::Unit);
-    if !s.generics.params.is_empty() || s.generics.where_clause.is_some() || !is_unit {
-        return parse::Error::new(
-            ident.span(),
-            "struct must be a non-generic unit struct (e.g. `struct S;`)",
-        )
-        .to_compile_error()
-        .into();
-    }
-
-    let attrs = &s.attrs;
-    let vis = &s.vis;
-    quote!(
-        #(#attrs)*
-        #vis struct #ident;
-
-        #[no_mangle]
-        unsafe fn _defmt_acquire()  {
-            <#ident as defmt::Logger>::acquire()
-        }
-
-        #[no_mangle]
-        unsafe fn _defmt_release()  {
-            <#ident as defmt::Logger>::release()
-        }
-
-        #[no_mangle]
-        unsafe fn _defmt_write(bytes: &[u8])  {
-            <#ident as defmt::Logger>::write(bytes)
-        }
-    )
-    .into()
+#[proc_macro_error]
+pub fn global_logger(args: TokenStream, item: TokenStream) -> TokenStream {
+    attributes::global_logger::expand(args, item)
 }
 
 #[proc_macro_attribute]
-pub fn panic_handler(args: TokenStream, input: TokenStream) -> TokenStream {
-    if !args.is_empty() {
-        return parse::Error::new(
-            Span2::call_site(),
-            "`#[defmt::panic_handler]` attribute takes no arguments",
-        )
-        .to_compile_error()
-        .into();
-    }
-    let f = parse_macro_input!(input as ItemFn);
-
-    let rety_is_ok = match &f.sig.output {
-        ReturnType::Default => false,
-        ReturnType::Type(_, ty) => matches!(&**ty, Type::Never(_)),
-    };
-
-    let ident = &f.sig.ident;
-    if f.sig.constness.is_some()
-        || f.sig.asyncness.is_some()
-        || f.sig.unsafety.is_some()
-        || f.sig.abi.is_some()
-        || !f.sig.generics.params.is_empty()
-        || f.sig.generics.where_clause.is_some()
-        || f.sig.variadic.is_some()
-        || !f.sig.inputs.is_empty()
-        || !rety_is_ok
-    {
-        return parse::Error::new(ident.span(), "function must have signature `fn() -> !`")
-            .to_compile_error()
-            .into();
-    }
-
-    let attrs = &f.attrs;
-    if let Err(e) = check_attribute_conflicts("panic_handler", attrs, &["export_name", "no_mangle"])
-    {
-        return e.to_compile_error().into();
-    }
-    let block = &f.block;
-    quote!(
-        #(#attrs)*
-        #[export_name = "_defmt_panic"]
-        fn #ident() -> ! {
-            #block
-        }
-    )
-    .into()
-}
-
-#[proc_macro]
-pub fn timestamp(ts: TokenStream) -> TokenStream {
-    let f = parse_macro_input!(ts as FormatArgs);
-
-    let ls = f.litstr.value();
-
-    let symname = Ident2::new("S", Span2::call_site());
-    let sym = mkstatic(symname.clone(), &ls, "timestamp");
-
-    let fragments = match defmt_parser::parse(&ls, ParserMode::Strict) {
-        Ok(args) => args,
-        Err(e) => {
-            return parse::Error::new(f.litstr.span(), e)
-                .to_compile_error()
-                .into()
-        }
-    };
-    let args: Vec<_> = f
-        .rest
-        .map(|(_, exprs)| exprs.into_iter().collect())
-        .unwrap_or_default();
-    let (pats, exprs) = match Codegen::new(&fragments, args.len(), f.litstr.span()) {
-        Ok(cg) => (cg.pats, cg.exprs),
-        Err(e) => return e.to_compile_error().into(),
-    };
-
-    quote!(
-        const _: () = {
-            #[export_name = "_defmt_timestamp"]
-            fn defmt_timestamp(fmt: ::defmt::Formatter<'_>) {
-                match (#(&(#args)),*) {
-                    (#(#pats),*) => {
-                    // NOTE: No format string index, and no finalize call.
-                        #(#exprs;)*
-                    }
-                }
-            }
-
-            #sym;
-
-            // Unique symbol name to prevent multiple `timestamp!` invocations in the crate graph.
-            // Uses `#symname` to ensure it is not discarded by the linker.
-            // This symbol itself is retained via a `EXTERN` directive in the linker script.
-            #[no_mangle]
-            #[cfg_attr(target_os = "macos", link_section = ".defmt,end.timestamp")]
-            #[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end.timestamp")]
-            static __DEFMT_MARKER_TIMESTAMP_WAS_DEFINED: &u8 = &#symname;
-        };
-    )
-    .into()
+#[proc_macro_error]
+pub fn panic_handler(args: TokenStream, item: TokenStream) -> TokenStream {
+    attributes::panic_handler::expand(args, item)
 }
 
-#[proc_macro]
-pub fn bitflags(ts: TokenStream) -> TokenStream {
-    bitflags::expand(ts)
-}
-
-/// Returns a list of features of which one has to be enabled for `level` to be active
-///
-/// * `debug_assertions == true` means that dev profile is enabled
-/// * `"defmt-default"` is enabled for dev & release profile so debug_assertions does not matter
-fn necessary_features_for_level(level: Level, debug_assertions: bool) -> &'static [&'static str] {
-    match level {
-        Level::Trace if debug_assertions => &["defmt-trace", "defmt-default"],
-        Level::Debug if debug_assertions => &["defmt-debug", "defmt-trace", "defmt-default"],
-
-        Level::Trace => &["defmt-trace"],
-        Level::Debug => &["defmt-debug", "defmt-trace"],
-        Level::Info => &["defmt-info", "defmt-debug", "defmt-trace", "defmt-default"],
-        Level::Warn => &[
-            "defmt-warn",
-            "defmt-info",
-            "defmt-debug",
-            "defmt-trace",
-            "defmt-default",
-        ],
-        Level::Error => &[
-            "defmt-error",
-            "defmt-warn",
-            "defmt-info",
-            "defmt-debug",
-            "defmt-trace",
-            "defmt-default",
-        ],
-    }
-}
-
-// `#[derive(Format)]`
+/* # Derives */
 #[proc_macro_derive(Format)]
-pub fn format(ts: TokenStream) -> TokenStream {
-    let mut input = parse_macro_input!(ts as DeriveInput);
-    let span = input.span();
-
-    let ident = input.ident;
-    let mut fs = String::new();
-    let mut field_types = vec![];
-    let mut exprs = vec![];
-    let sym: TokenStream2;
-    match input.data {
-        Data::Enum(de) => {
-            if de.variants.is_empty() {
-                // For zero-variant enums, this is unreachable code.
-                sym = mksym("!", "derived", false);
-                exprs.push(quote!(match *self {}));
-            } else {
-                let mut arms = vec![];
-                let mut first = true;
-                for (i, var) in de.variants.iter().enumerate() {
-                    let vident = &var.ident;
-
-                    if first {
-                        first = false;
-                    } else {
-                        fs.push('|');
-                    }
-                    fs.push_str(&vident.to_string());
-
-                    let mut pats = vec![];
-                    let exprs = fields(&var.fields, &mut fs, &mut field_types, &mut pats);
-                    let pats = quote!( { #(#pats),* } );
-
-                    let len = de.variants.len();
-                    let encode_discriminant = if len == 1 {
-                        // For single-variant enums, there is no need to encode the discriminant.
-                        quote!()
-                    } else if let (Ok(_), Ok(i)) = (u8::try_from(len), u8::try_from(i)) {
-                        quote!(
-                            defmt::export::u8(&#i);
-                        )
-                    } else if let (Ok(_), Ok(i)) = (u16::try_from(len), u16::try_from(i)) {
-                        quote!(
-                            defmt::export::u16(&#i);
-                        )
-                    } else if let (Ok(_), Ok(i)) = (u32::try_from(len), u32::try_from(i)) {
-                        quote!(
-                            defmt::export::u32(&#i);
-                        )
-                    } else if let (Ok(_), Ok(i)) = (u64::try_from(len), u64::try_from(i)) {
-                        quote!(
-                            defmt::export::u64(&#i);
-                        )
-                    } else {
-                        // u128 case is omitted with the assumption, that usize is never greater than u64
-                        return parse::Error::new(
-                            span,
-                            format!("`#[derive(Format)]` does not support enums with more than {} variants", u64::MAX),
-                        )
-                        .to_compile_error()
-                        .into();
-                    };
-
-                    arms.push(quote!(
-                        #ident::#vident #pats => {
-                            #encode_discriminant
-                            #(#exprs;)*
-                        }
-                    ))
-                }
-
-                sym = mksym(&fs, "derived", false);
-                exprs.push(quote!(match self {
-                    #(#arms)*
-                }));
-            }
-        }
-
-        Data::Struct(ds) => {
-            fs = ident.to_string();
-            let mut pats = vec![];
-            let args = fields(&ds.fields, &mut fs, &mut field_types, &mut pats);
-
-            sym = mksym(&fs, "derived", false);
-            exprs.push(quote!(match self {
-                Self { #(#pats),* } => {
-                    #(#args;)*
-                }
-            }));
-        }
-
-        Data::Union(..) => {
-            return parse::Error::new(span, "`#[derive(Format)]` does not support unions")
-                .to_compile_error()
-                .into();
-        }
-    }
-
-    let where_clause = input.generics.make_where_clause();
-    let mut where_clause: WhereClause = where_clause.clone();
-    let (impl_generics, type_generics, _) = input.generics.split_for_impl();
-
-    // Extend where-clause with `Format` bounds for type parameters.
-    for param in &input.generics.params {
-        if let GenericParam::Type(ty) = param {
-            let ident = &ty.ident;
-            where_clause
-                .predicates
-                .push(syn::parse::<WherePredicate>(quote!(#ident: defmt::Format).into()).unwrap());
-        }
-    }
-
-    quote!(
-        impl #impl_generics defmt::Format for #ident #type_generics #where_clause {
-            fn format(&self, f: defmt::Formatter) {
-                unreachable!()
-            }
-            fn _format_tag() -> defmt::Str {
-                #sym
-            }
-            fn _format_data(&self) {
-                #(#exprs)*
-            }
-        }
-    )
-    .into()
-}
-
-fn fields(
-    fields: &Fields,
-    format: &mut String,
-    // collect all *non-native* types that appear as fields
-    field_types: &mut Vec<Type>,
-    pats: &mut Vec<TokenStream2>,
-) -> Vec<TokenStream2> {
-    let mut list = vec![];
-    match fields {
-        Fields::Named(FieldsNamed { named: fs, .. })
-        | Fields::Unnamed(FieldsUnnamed { unnamed: fs, .. }) => {
-            let named = match fields {
-                Fields::Named(..) => true,
-                Fields::Unnamed(..) => false,
-                _ => unreachable!(),
-            };
-
-            if !fs.is_empty() {
-                if named {
-                    format.push_str(" {{ ");
-                } else {
-                    format.push('(');
-                }
-                let mut first = true;
-                for (i, f) in fs.iter().enumerate() {
-                    if first {
-                        first = false;
-                    } else {
-                        format.push_str(", ");
-                    }
-                    let ty = as_native_type(&f.ty).unwrap_or_else(|| {
-                        field_types.push(f.ty.clone());
-                        "?".to_string()
-                    });
-                    if let Some(ident) = f.ident.as_ref() {
-                        core::write!(format, "{}: {{={}:?}}", ident, ty).ok();
-
-                        if ty == "?" {
-                            list.push(quote!(defmt::export::fmt(#ident)));
-                        } else {
-                            let method = format_ident!("{}", ty);
-                            list.push(quote!(defmt::export::#method(#ident)));
-                        }
-                        pats.push(quote!( #ident ));
-                    } else {
-                        // Unnamed (tuple) field.
-
-                        core::write!(format, "{{={}}}", ty).ok();
-
-                        let ident = format_ident!("arg{}", i);
-                        if ty == "?" {
-                            list.push(quote!(defmt::export::fmt(#ident)));
-                        } else {
-                            let method = format_ident!("{}", ty);
-                            list.push(quote!(defmt::export::#method(#ident)));
-                        }
-
-                        let i = syn::Index::from(i);
-                        pats.push(quote!( #i: #ident ));
-                    }
-                }
-                if named {
-                    format.push_str(" }}");
-                } else {
-                    format.push(')');
-                }
-            }
-        }
-
-        Fields::Unit => {}
-    }
-
-    list
-}
-
-/// Returns `true` if `ty_name` refers to a builtin Rust type that has native support from defmt
-/// and does not have to go through the `Format` trait.
-///
-/// This should return `true` for all types that can be used as `{:type}`.
-///
-/// Note: This is technically incorrect, since builtin types can be shadowed. However the efficiency
-/// gains are too big to pass up, so we expect user code to not do that.
-fn as_native_type(ty: &Type) -> Option<String> {
-    match ty {
-        Type::Path(p) => match p.path.get_ident() {
-            Some(ident) => {
-                let s = ident.to_string();
-                match &*s {
-                    "u8" | "u16" | "u32" | "usize" | "i8" | "i16" | "i32" | "isize" | "f32"
-                    | "f64" | "bool" | "str" => Some(s),
-                    _ => None,
-                }
-            }
-            None => None,
-        },
-        Type::Reference(tref) => as_native_type(&*tref.elem),
-        _ => None,
-    }
-}
-
-fn cfg_if_logging_enabled(level: Level) -> TokenStream2 {
-    let features_dev = necessary_features_for_level(level, true);
-    let features_release = necessary_features_for_level(level, false);
-
-    quote!(
-        any(
-            all(    debug_assertions,  any(#( feature = #features_dev     ),*)),
-            all(not(debug_assertions), any(#( feature = #features_release ),*))
-        )
-    )
-}
-
-fn log_ts(level: Level, ts: TokenStream) -> TokenStream {
-    log(level, parse_macro_input!(ts as FormatArgs)).into()
-}
-
-fn log(level: Level, log: FormatArgs) -> TokenStream2 {
-    let ls = log.litstr.value();
-    let fragments = match defmt_parser::parse(&ls, ParserMode::Strict) {
-        Ok(args) => args,
-        Err(e) => return parse::Error::new(log.litstr.span(), e).to_compile_error(),
-    };
-
-    let args = log
-        .rest
-        .map(|(_, exprs)| exprs.into_iter().collect())
-        .unwrap_or_else(Vec::new);
-
-    let (pats, exprs) = match Codegen::new(&fragments, args.len(), log.litstr.span()) {
-        Ok(cg) => (cg.pats, cg.exprs),
-        Err(e) => return e.to_compile_error(),
-    };
-
-    let sym = mksym(&ls, level.as_str(), true);
-    let logging_enabled = cfg_if_logging_enabled(level);
-    quote!({
-        #[cfg(#logging_enabled)] {
-            match (#(&(#args)),*) {
-                (#(#pats),*) => {
-                    defmt::export::acquire();
-                    defmt::export::header(&#sym);
-                    #(#exprs;)*
-                    defmt::export::release()
-                }
-            }
-        }
-        // if logging is disabled match args, so they are not unused
-        #[cfg(not(#logging_enabled))]
-        match (#(&(#args)),*) {
-            _ => {}
-        }
-    })
-}
-
-struct DbgArgs {
-    exprs: Punctuated<Expr, Token![,]>,
-}
-
-impl Parse for DbgArgs {
-    fn parse(input: ParseStream) -> Result<Self, syn::Error> {
-        Ok(Self {
-            exprs: Punctuated::parse_terminated(input)?,
-        })
-    }
-}
-
-#[proc_macro]
-pub fn dbg(input: TokenStream) -> TokenStream {
-    let inputs = parse_macro_input!(input as DbgArgs).exprs;
-
-    let outputs = inputs
-        .into_iter()
-        .map(|expr| {
-            let escaped_expr = escape_expr(&expr);
-            let format_string = format!("{} = {{}}", escaped_expr);
-
-            quote!(match #expr {
-            tmp => {
-                defmt::trace!(#format_string, tmp);
-                tmp
-            }
-            })
-        })
-        .collect::<Vec<_>>();
-
-    if outputs.is_empty() {
-        // for compatibility with `std::dbg!` we also emit a TRACE log in this case
-        quote!(defmt::trace!(""))
-    } else {
-        quote!((#(#outputs),*))
-    }
-    .into()
-}
-
-#[proc_macro]
-pub fn trace(ts: TokenStream) -> TokenStream {
-    log_ts(Level::Trace, ts)
-}
-
-#[proc_macro]
-pub fn debug(ts: TokenStream) -> TokenStream {
-    log_ts(Level::Debug, ts)
-}
-
-#[proc_macro]
-pub fn info(ts: TokenStream) -> TokenStream {
-    log_ts(Level::Info, ts)
-}
-
-#[proc_macro]
-pub fn warn(ts: TokenStream) -> TokenStream {
-    log_ts(Level::Warn, ts)
-}
-
-#[proc_macro]
-pub fn error(ts: TokenStream) -> TokenStream {
-    log_ts(Level::Error, ts)
-}
-
-fn panic(
-    ts: TokenStream,
-    zero_args_string: &str,
-    string_transform: impl FnOnce(&str) -> String,
-) -> TokenStream {
-    let log_stmt = if ts.is_empty() {
-        // panic!() -> error!("panicked at 'explicit panic'")
-        let litstr = LitStr::new(zero_args_string, Span2::call_site());
-        log(Level::Error, FormatArgs { litstr, rest: None })
-    } else {
-        // panic!("a", b, c) -> error!("panicked at 'a'", b, c)
-        let args = parse_macro_input!(ts as FormatArgs);
-        let litstr = LitStr::new(&string_transform(&args.litstr.value()), Span2::call_site());
-        let rest = args.rest;
-        log(Level::Error, FormatArgs { litstr, rest })
-    };
-
-    quote!(
-        {
-            #log_stmt;
-            defmt::export::panic()
-        }
-    )
-    .into()
-}
-
-// not naming this `panic` to avoid shadowing `core::panic` in this scope
-#[proc_macro]
-pub fn panic_(ts: TokenStream) -> TokenStream {
-    panic(ts, "panicked at 'explicit panic'", |format_string| {
-        format!("panicked at '{}'", format_string)
-    })
+#[proc_macro_error]
+pub fn format(input: TokenStream) -> TokenStream {
+    derives::format::expand(input)
 }
 
-// not naming this `todo` to avoid shadowing `core::todo` in this scope
+/* # Function-like */
 #[proc_macro]
-pub fn todo_(ts: TokenStream) -> TokenStream {
-    panic(ts, "panicked at 'not yet implemented'", |format_string| {
-        format!("panicked at 'not yet implemented: {}'", format_string)
-    })
+#[proc_macro_error]
+pub fn assert_(args: TokenStream) -> TokenStream {
+    function_like::assert_like::assert::expand(args)
 }
 
-// not naming this `unreachable` to avoid shadowing `core::unreachable` in this scope
 #[proc_macro]
-pub fn unreachable_(ts: TokenStream) -> TokenStream {
-    panic(
-        ts,
-        "panicked at 'internal error: entered unreachable code'",
-        |format_string| {
-            format!(
-                "panicked at 'internal error: entered unreachable code: {}'",
-                format_string
-            )
-        },
-    )
+#[proc_macro_error]
+pub fn assert_eq_(args: TokenStream) -> TokenStream {
+    function_like::assert_binop::eq(args)
 }
 
-// not naming this `assert` to avoid shadowing `core::assert` in this scope
 #[proc_macro]
-pub fn assert_(ts: TokenStream) -> TokenStream {
-    let assert = parse_macro_input!(ts as Assert);
-
-    let condition = assert.condition;
-    let log_stmt = if let Some(args) = assert.args {
-        let litstr = LitStr::new(
-            &format!("panicked at '{}'", args.litstr.value()),
-            Span2::call_site(),
-        );
-        let rest = args.rest;
-        log(Level::Error, FormatArgs { litstr, rest })
-    } else {
-        let value = &format!(
-            "panicked at 'assertion failed: {}'",
-            escape_expr(&condition)
-        );
-        let litstr = LitStr::new(value, Span2::call_site());
-        log(Level::Error, FormatArgs { litstr, rest: None })
-    };
-
-    quote!(
-        if !(#condition) {
-            #log_stmt;
-            defmt::export::panic()
-        }
-    )
-    .into()
-}
-
-#[derive(PartialEq)]
-enum BinOp {
-    Eq,
-    Ne,
-}
-
-// not naming this `assert_eq` to avoid shadowing `core::assert_eq` in this scope
-#[proc_macro]
-pub fn assert_eq_(ts: TokenStream) -> TokenStream {
-    assert_binop(ts, BinOp::Eq)
-}
-
-// not naming this `assert_ne` to avoid shadowing `core::assert_ne` in this scope
-#[proc_macro]
-pub fn assert_ne_(ts: TokenStream) -> TokenStream {
-    assert_binop(ts, BinOp::Ne)
-}
-
-// not naming this `assert_eq` to avoid shadowing `core::assert_eq` in this scope
-fn assert_binop(ts: TokenStream, binop: BinOp) -> TokenStream {
-    let assert = parse_macro_input!(ts as AssertEq);
-
-    let left = assert.left;
-    let right = assert.right;
-
-    let mut log_args = Punctuated::new();
-
-    let extra_string = if let Some(args) = assert.args {
-        if let Some(rest) = args.rest {
-            log_args.extend(rest.1);
-        }
-        format!(": {}", args.litstr.value())
-    } else {
-        String::new()
-    };
-
-    let vals = match binop {
-        BinOp::Eq => &["left_val", "right_val"][..],
-        BinOp::Ne => &["left_val"][..],
-    };
-
-    for val in vals {
-        log_args.push(ident_expr(*val));
-    }
-
-    let log_stmt = match binop {
-        BinOp::Eq => log(
-            Level::Error,
-            FormatArgs {
-                litstr: LitStr::new(
-                    &format!(
-                        "panicked at 'assertion failed: `(left == right)`{}'
- left: `{{:?}}`
-right: `{{:?}}`",
-                        extra_string
-                    ),
-                    Span2::call_site(),
-                ),
-                rest: Some((syn::token::Comma::default(), log_args)),
-            },
-        ),
-        BinOp::Ne => log(
-            Level::Error,
-            FormatArgs {
-                litstr: LitStr::new(
-                    &format!(
-                        "panicked at 'assertion failed: `(left != right)`{}'
-left/right: `{{:?}}`",
-                        extra_string
-                    ),
-                    Span2::call_site(),
-                ),
-                rest: Some((syn::token::Comma::default(), log_args)),
-            },
-        ),
-    };
-
-    let mut cond = quote!(*left_val == *right_val);
-    if binop == BinOp::Eq {
-        cond = quote!(!(#cond));
-    }
-
-    quote!(
-        // evaluate arguments first
-        match (&(#left), &(#right)) {
-            (left_val, right_val) => {
-                // following `core::assert_eq!`
-                if #cond {
-                    #log_stmt;
-                    defmt::export::panic()
-                }
-            }
-        }
-    )
-    .into()
+#[proc_macro_error]
+pub fn assert_ne_(args: TokenStream) -> TokenStream {
+    function_like::assert_binop::ne(args)
 }
 
+/* ## `debug_` variants */
 // NOTE these `debug_*` macros can be written using `macro_rules!` (that'd be simpler) but that
 // results in an incorrect source code location being reported: the location of the `macro_rules!`
 // statement is reported. Using a proc-macro results in the call site being reported, which is what
 // we want
 #[proc_macro]
-pub fn debug_assert_(ts: TokenStream) -> TokenStream {
-    let assert = TokenStream2::from(assert_(ts));
+#[proc_macro_error]
+pub fn debug_assert_(input: TokenStream) -> TokenStream {
+    let assert = TokenStream2::from(assert_(input));
     quote!(if cfg!(debug_assertions) {
         #assert
     })
@@ -778,8 +75,9 @@ pub fn debug_assert_(ts: TokenStream) -> TokenStream {
 }
 
 #[proc_macro]
-pub fn debug_assert_eq_(ts: TokenStream) -> TokenStream {
-    let assert = TokenStream2::from(assert_eq_(ts));
+#[proc_macro_error]
+pub fn debug_assert_eq_(input: TokenStream) -> TokenStream {
+    let assert = TokenStream2::from(assert_eq_(input));
     quote!(if cfg!(debug_assertions) {
         #assert
     })
@@ -787,438 +85,118 @@ pub fn debug_assert_eq_(ts: TokenStream) -> TokenStream {
 }
 
 #[proc_macro]
-pub fn debug_assert_ne_(ts: TokenStream) -> TokenStream {
-    let assert = TokenStream2::from(assert_ne_(ts));
+#[proc_macro_error]
+pub fn debug_assert_ne_(input: TokenStream) -> TokenStream {
+    let assert = TokenStream2::from(assert_ne_(input));
     quote!(if cfg!(debug_assertions) {
         #assert
     })
     .into()
 }
+/* ## end of `debug_` variants */
 
 #[proc_macro]
-pub fn unwrap(ts: TokenStream) -> TokenStream {
-    let assert = parse_macro_input!(ts as Assert);
-
-    let condition = assert.condition;
-    let log_stmt = if let Some(args) = assert.args {
-        let litstr = LitStr::new(
-            &format!("panicked at '{}'", args.litstr.value()),
-            Span2::call_site(),
-        );
-        let rest = args.rest;
-        log(Level::Error, FormatArgs { litstr, rest })
-    } else {
-        let mut log_args = Punctuated::new();
-        log_args.push(ident_expr("_unwrap_err"));
-
-        let litstr = LitStr::new(
-            &format!(
-                "panicked at 'unwrap failed: {}'\nerror: `{{:?}}`",
-                escape_expr(&condition)
-            ),
-            Span2::call_site(),
-        );
-        let rest = Some((syn::token::Comma::default(), log_args));
-        log(Level::Error, FormatArgs { litstr, rest })
-    };
-
-    quote!(
-        match defmt::export::into_result(#condition) {
-            ::core::result::Result::Ok(res) => res,
-            ::core::result::Result::Err(_unwrap_err) => {
-                #log_stmt;
-                defmt::export::panic()
-            }
-        }
-    )
-    .into()
+#[proc_macro_error]
+pub fn dbg(args: TokenStream) -> TokenStream {
+    function_like::dbg::expand(args)
 }
 
-fn ident_expr(name: &str) -> Expr {
-    let mut segments = Punctuated::new();
-    segments.push(PathSegment {
-        ident: Ident2::new(name, Span2::call_site()),
-        arguments: PathArguments::None,
-    });
-
-    Expr::Path(ExprPath {
-        attrs: vec![],
-        qself: None,
-        path: Path {
-            leading_colon: None,
-            segments,
-        },
-    })
-}
-
-fn escape_expr(expr: &Expr) -> String {
-    let q = quote!(#expr);
-    q.to_string().replace("{", "{{").replace("}", "}}")
-}
-
-struct Assert {
-    condition: Expr,
-    args: Option<FormatArgs>,
+#[proc_macro]
+#[proc_macro_error]
+pub fn intern(args: TokenStream) -> TokenStream {
+    function_like::intern::expand(args)
 }
 
-impl Parse for Assert {
-    fn parse(input: ParseStream) -> parse::Result<Self> {
-        let condition = input.parse()?;
-        if input.is_empty() {
-            // assert!(a)
-            return Ok(Assert {
-                condition,
-                args: None,
-            });
-        }
-
-        let _comma: Token![,] = input.parse()?;
-
-        if input.is_empty() {
-            // assert!(a,)
-            Ok(Assert {
-                condition,
-                args: None,
-            })
-        } else {
-            // assert!(a, "b", c)
-            Ok(Assert {
-                condition,
-                args: Some(input.parse()?),
-            })
-        }
-    }
+#[proc_macro]
+#[proc_macro_error]
+pub fn internp(args: TokenStream) -> TokenStream {
+    function_like::internp::expand(args)
 }
 
-struct AssertEq {
-    left: Expr,
-    right: Expr,
-    args: Option<FormatArgs>,
+/* ## Logging macros */
+#[proc_macro]
+#[proc_macro_error]
+pub fn trace(args: TokenStream) -> TokenStream {
+    function_like::log::expand(Level::Trace, args)
 }
 
-impl Parse for AssertEq {
-    fn parse(input: ParseStream) -> parse::Result<Self> {
-        let left = input.parse()?;
-        let _comma: Token![,] = input.parse()?;
-        let right = input.parse()?;
-
-        if input.is_empty() {
-            // assert_eq!(a, b)
-            return Ok(AssertEq {
-                left,
-                right,
-                args: None,
-            });
-        }
-
-        let _comma: Token![,] = input.parse()?;
-
-        if input.is_empty() {
-            // assert_eq!(a, b,)
-            Ok(AssertEq {
-                left,
-                right,
-                args: None,
-            })
-        } else {
-            // assert_eq!(a, b, "c", d)
-            Ok(AssertEq {
-                left,
-                right,
-                args: Some(input.parse()?),
-            })
-        }
-    }
+#[proc_macro]
+#[proc_macro_error]
+pub fn debug(args: TokenStream) -> TokenStream {
+    function_like::log::expand(Level::Debug, args)
 }
 
-struct FormatArgs {
-    litstr: LitStr,
-    rest: Option<(Token![,], Punctuated<Expr, Token![,]>)>,
+#[proc_macro]
+#[proc_macro_error]
+pub fn info(args: TokenStream) -> TokenStream {
+    function_like::log::expand(Level::Info, args)
 }
 
-impl Parse for FormatArgs {
-    fn parse(input: ParseStream) -> parse::Result<Self> {
-        Ok(Self {
-            litstr: input.parse()?,
-            rest: if input.is_empty() {
-                None
-            } else {
-                Some((input.parse()?, Punctuated::parse_terminated(input)?))
-            },
-        })
-    }
+#[proc_macro]
+#[proc_macro_error]
+pub fn warn(args: TokenStream) -> TokenStream {
+    function_like::log::expand(Level::Warn, args)
 }
 
 #[proc_macro]
-pub fn intern(ts: TokenStream) -> TokenStream {
-    let lit = parse_macro_input!(ts as LitStr);
-    let ls = lit.value();
-
-    mksym(&ls, "str", false).into()
+#[proc_macro_error]
+pub fn error(args: TokenStream) -> TokenStream {
+    function_like::log::expand(Level::Error, args)
 }
+/* ## end of logging macros */
 
 #[proc_macro]
-pub fn internp(ts: TokenStream) -> TokenStream {
-    let lit = parse_macro_input!(ts as LitStr);
-    let ls = lit.value();
-
-    let sym = symbol::Symbol::new("prim", &ls).mangle();
-
-    let section = mksection(false, "prim.", &sym);
-    let section_macos = mksection(true, "prim.", &sym);
-
-    let sym = if cfg!(feature = "unstable-test") {
-        quote!({ defmt::export::fetch_add_string_index() as u16 })
-    } else {
-        quote!({
-            #[cfg_attr(target_os = "macos", link_section = #section_macos)]
-            #[cfg_attr(not(target_os = "macos"), link_section = #section)]
-            #[export_name = #sym]
-            static S: u8 = 0;
-            &S as *const u8 as u16
-        })
-    };
-
-    quote!({
-        defmt::export::make_istr(#sym)
+#[proc_macro_error]
+pub fn panic_(args: TokenStream) -> TokenStream {
+    function_like::panic_like::expand(args, "panicked at 'explicit panic'", |format_string| {
+        format!("panicked at '{}'", format_string)
     })
-    .into()
 }
 
 #[proc_macro]
-pub fn write(ts: TokenStream) -> TokenStream {
-    let write = parse_macro_input!(ts as Write);
-    let ls = write.litstr.value();
-    let fragments = match defmt_parser::parse(&ls, ParserMode::Strict) {
-        Ok(args) => args,
-        Err(e) => {
-            return parse::Error::new(write.litstr.span(), e)
-                .to_compile_error()
-                .into()
-        }
-    };
-
-    let args: Vec<_> = write
-        .rest
-        .map(|(_, exprs)| exprs.into_iter().collect())
-        .unwrap_or_default();
-
-    let (pats, exprs) = match Codegen::new(&fragments, args.len(), write.litstr.span()) {
-        Ok(cg) => (cg.pats, cg.exprs),
-        Err(e) => return e.to_compile_error().into(),
-    };
-
-    let fmt = &write.fmt;
-    let sym = mksym(&ls, "write", false);
-    quote!({
-        let fmt: defmt::Formatter<'_> = #fmt;
-        match (#(&(#args)),*) {
-            (#(#pats),*) => {
-                defmt::export::istr(&#sym);
-                #(#exprs;)*
-            }
-        }
+#[proc_macro_error]
+pub fn todo_(args: TokenStream) -> TokenStream {
+    function_like::panic_like::expand(args, "panicked at 'not yet implemented'", |format_string| {
+        format!("panicked at 'not yet implemented: {}'", format_string)
     })
-    .into()
 }
 
-/// work around restrictions on length and allowed characters imposed by macos linker
-/// returns (note the comma character for macos):
-///   under macos: ".defmt," + 16 character hex digest of symbol's hash
-///   otherwise:   ".defmt." + prefix + symbol
-fn mksection(macos: bool, prefix: &str, symbol: &str) -> String {
-    let mut sub_section = format!(".{}{}", prefix, symbol);
-
-    if macos {
-        let mut hasher = DefaultHasher::new();
-        sub_section.hash(&mut hasher);
-        sub_section = format!(",{:x}", hasher.finish());
-    }
-
-    format!(".defmt{}", sub_section)
-}
-
-fn mkstatic(varname: Ident2, string: &str, tag: &str) -> TokenStream2 {
-    let sym = symbol::Symbol::new(tag, string).mangle();
-    let section = mksection(false, "", &sym);
-    let section_macos = mksection(true, "", &sym);
-
-    quote!(
-        #[cfg_attr(target_os = "macos", link_section = #section_macos)]
-        #[cfg_attr(not(target_os = "macos"), link_section = #section)]
-        #[export_name = #sym]
-        static #varname: u8 = 0;
+#[proc_macro]
+#[proc_macro_error]
+pub fn unreachable_(args: TokenStream) -> TokenStream {
+    function_like::panic_like::expand(
+        args,
+        "panicked at 'internal error: entered unreachable code'",
+        |format_string| {
+            format!(
+                "panicked at 'internal error: entered unreachable code: {}'",
+                format_string
+            )
+        },
     )
 }
 
-fn mksym(string: &str, tag: &str, is_log_statement: bool) -> TokenStream2 {
-    // NOTE we rely on this variable name when extracting file location information from the DWARF
-    // without it we have no other mean to differentiate static variables produced by `info!` vs
-    // produced by `intern!` (or `internp`)
-    let varname = if is_log_statement {
-        format_ident!("DEFMT_LOG_STATEMENT")
-    } else {
-        format_ident!("S")
-    };
-    let sym = if cfg!(feature = "unstable-test") {
-        quote!({ defmt::export::fetch_add_string_index() })
-    } else {
-        let statik = mkstatic(varname.clone(), string, tag);
-        quote!({
-            #statik
-            &#varname as *const u8 as u16
-        })
-    };
-
-    quote!({
-        defmt::export::make_istr(#sym)
-    })
-}
-
-struct Write {
-    fmt: Expr,
-    _comma: Token![,],
-    litstr: LitStr,
-    rest: Option<(Token![,], Punctuated<Expr, Token![,]>)>,
+#[proc_macro]
+#[proc_macro_error]
+pub fn unwrap(args: TokenStream) -> TokenStream {
+    function_like::assert_like::unwrap::expand(args)
 }
 
-impl Parse for Write {
-    fn parse(input: ParseStream) -> parse::Result<Self> {
-        Ok(Self {
-            fmt: input.parse()?,
-            _comma: input.parse()?,
-            litstr: input.parse()?,
-            rest: if input.is_empty() {
-                None
-            } else {
-                Some((input.parse()?, Punctuated::parse_terminated(input)?))
-            },
-        })
-    }
+#[proc_macro]
+#[proc_macro_error]
+pub fn write(args: TokenStream) -> TokenStream {
+    function_like::write::expand(args)
 }
 
-struct Codegen {
-    pats: Vec<Ident2>,
-    exprs: Vec<TokenStream2>,
+/* # Items */
+#[proc_macro]
+#[proc_macro_error]
+pub fn bitflags(ts: TokenStream) -> TokenStream {
+    items::bitflags::expand(ts)
 }
 
-impl Codegen {
-    fn new(fragments: &[Fragment<'_>], num_args: usize, span: Span2) -> parse::Result<Self> {
-        let parsed_params = fragments
-            .iter()
-            .filter_map(|frag| match frag {
-                Fragment::Parameter(param) => Some(param.clone()),
-                Fragment::Literal(_) => None,
-            })
-            .collect::<Vec<_>>();
-
-        let actual_argument_count = parsed_params
-            .iter()
-            .map(|param| param.index + 1)
-            .max()
-            .unwrap_or(0);
-
-        let mut exprs = vec![];
-        let mut pats = vec![];
-
-        for i in 0..actual_argument_count {
-            let arg = format_ident!("arg{}", i);
-            // find first use of this argument and return its type
-            let param = parsed_params.iter().find(|param| param.index == i).unwrap();
-            match param.ty {
-                defmt_parser::Type::I8 => exprs.push(quote!(defmt::export::i8(#arg))),
-                defmt_parser::Type::I16 => exprs.push(quote!(defmt::export::i16(#arg))),
-                defmt_parser::Type::I32 => exprs.push(quote!(defmt::export::i32(#arg))),
-                defmt_parser::Type::I64 => exprs.push(quote!(defmt::export::i64(#arg))),
-                defmt_parser::Type::I128 => exprs.push(quote!(defmt::export::i128(#arg))),
-                defmt_parser::Type::Isize => exprs.push(quote!(defmt::export::isize(#arg))),
-
-                defmt_parser::Type::U8 => exprs.push(quote!(defmt::export::u8(#arg))),
-                defmt_parser::Type::U16 => exprs.push(quote!(defmt::export::u16(#arg))),
-                defmt_parser::Type::U32 => exprs.push(quote!(defmt::export::u32(#arg))),
-                defmt_parser::Type::U64 => exprs.push(quote!(defmt::export::u64(#arg))),
-                defmt_parser::Type::U128 => exprs.push(quote!(defmt::export::u128(#arg))),
-                defmt_parser::Type::Usize => exprs.push(quote!(defmt::export::usize(#arg))),
-
-                defmt_parser::Type::F32 => exprs.push(quote!(defmt::export::f32(#arg))),
-                defmt_parser::Type::F64 => exprs.push(quote!(defmt::export::f64(#arg))),
-
-                defmt_parser::Type::Bool => exprs.push(quote!(defmt::export::bool(#arg))),
-
-                defmt_parser::Type::Str => exprs.push(quote!(defmt::export::str(#arg))),
-                defmt_parser::Type::IStr => exprs.push(quote!(defmt::export::istr(#arg))),
-                defmt_parser::Type::Char => exprs.push(quote!(defmt::export::char(#arg))),
-
-                defmt_parser::Type::Format => exprs.push(quote!(defmt::export::fmt(#arg))),
-                defmt_parser::Type::FormatSlice => {
-                    exprs.push(quote!(defmt::export::fmt_slice(#arg)))
-                }
-                defmt_parser::Type::FormatArray(len) => {
-                    exprs.push(quote!(defmt::export::fmt_array({
-                        let tmp: &[_; #len] = #arg;
-                        tmp
-                    })))
-                }
-
-                defmt_parser::Type::Debug => exprs.push(quote!(defmt::export::debug(#arg))),
-                defmt_parser::Type::Display => exprs.push(quote!(defmt::export::display(#arg))),
-                defmt_parser::Type::FormatSequence => unreachable!(),
-
-                defmt_parser::Type::U8Slice => exprs.push(quote!(defmt::export::slice(#arg))),
-                // We cast to the expected array type (which should be a no-op cast) to provoke
-                // a type mismatch error on mismatched lengths:
-                // ```
-                // error[E0308]: mismatched types
-                //   --> src/bin/log.rs:20:5
-                //    |
-                // 20 |     defmt::info!("🍕 array {:[u8; 3]}", [3, 14]);
-                //    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-                //    |     |
-                //    |     expected an array with a fixed size of 3 elements, found one with 2 elements
-                //    |     expected due to this
-                // ```
-                defmt_parser::Type::U8Array(len) => exprs.push(quote!(defmt::export::u8_array({
-                    let tmp: &[u8; #len] = #arg;
-                    tmp
-                }))),
-                defmt_parser::Type::BitField(_) => {
-                    let all_bitfields = parsed_params.iter().filter(|param| param.index == i);
-                    let (smallest_bit_index, largest_bit_index) =
-                        defmt_parser::get_max_bitfield_range(all_bitfields).unwrap();
-
-                    // indices of the lowest and the highest octet which contains bitfield-relevant data
-                    let lowest_byte = smallest_bit_index / 8;
-                    let highest_byte = (largest_bit_index - 1) / 8;
-                    let truncated_sz = highest_byte - lowest_byte + 1; // in bytes
-
-                    // shift away unneeded lower octet
-                    // TODO: create helper for shifting because readability
-                    match truncated_sz {
-                        1 => exprs.push(quote!(defmt::export::u8(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))),
-                        2 => exprs.push(quote!(defmt::export::u16(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))),
-                        3..=4 => exprs.push(quote!(defmt::export::u32(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))),
-                        5..=8 => exprs.push(quote!(defmt::export::u64(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))),
-                        9..=16 => exprs.push(quote!(defmt::export::u128(&defmt::export::truncate((*#arg) >> (#lowest_byte * 8))))),
-                        _ => unreachable!(),
-                    }
-                }
-            }
-            pats.push(arg);
-        }
-
-        if num_args != actual_argument_count {
-            let mut only = "";
-            if num_args < actual_argument_count {
-                only = "only ";
-            }
-
-            let message = format!(
-                "format string requires {} arguments but {}{} were provided",
-                actual_argument_count, only, num_args
-            );
-            return Err(parse::Error::new(span, message));
-        }
-
-        Ok(Codegen { pats, exprs })
-    }
+#[proc_macro]
+#[proc_macro_error]
+pub fn timestamp(args: TokenStream) -> TokenStream {
+    items::timestamp::expand(args)
 }


Test patch:
diff --git a/tests/ui/panic-handler-export-name.stderr b/tests/ui/panic-handler-export-name.stderr
index 67c39c7..b276f9b 100644
--- a/tests/ui/panic-handler-export-name.stderr
+++ b/tests/ui/panic-handler-export-name.stderr
@@ -2,4 +2,4 @@ error: `#[panic_handler]` attribute cannot be used together with `#[export_name]
  --> $DIR/panic-handler-export-name.rs:5:1
   |
 5 | #[export_name = "hello"]
-  | ^
+  | ^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/tests/ui/panic-handler-no-mangle.stderr b/tests/ui/panic-handler-no-mangle.stderr
index 63cdcaf..2563973 100644
--- a/tests/ui/panic-handler-no-mangle.stderr
+++ b/tests/ui/panic-handler-no-mangle.stderr
@@ -2,4 +2,4 @@ error: `#[panic_handler]` attribute cannot be used together with `#[no_mangle]`
  --> $DIR/panic-handler-no-mangle.rs:5:1
   |
 5 | #[no_mangle]
-  | ^
+  | ^^^^^^^^^^^^