Expand description
§mynt 🍬 - a refreshing error handling crate for proc macros
mynt provides a straightforward way to handle errors
based on the nightly diagnostic interface.
It takes a different approach compared to manyhow.
Instead of returning results, we return token streams and instead emit errors externally and continue, only quitting if the error is fatal.
There is no passing around emitters or writing to dummy streams if an error occurs.
It is inspired by proc-macro-error3,
but with some changes:
- Uses declarative macros instead of procedural macros for lower compile times
- All dependencies are optional
- Removes dummy streams (instead opting for the developer to emit as much as possible)
- Support for
synandproc-macro2, as well as support forvenialanddarling - Macros for emitting diagnostics, bailing, quitting, and writing assertions
- Terminal colors for the stable channel fallback with
yansi
§API
The following are the utilities mynt provides to make error handling easier with proc-macros:
§Entry points
Macros need to be wrapped in an entry point to use mynt’s features (needed for quit + fallback support).
mynt!{}: allows wrapping an inline/existing proc macros in lib.rsmynt_macro!(name => name_impl);: declares a function-like proc macromynt_macro_attribute!(name => name_impl);: declares an attribute proc macromynt_macro_derive!(name for Trait(attributes(attr))? => name_impl);: declares a derive proc macro
§Helpers
Helpers make emitting diagnostics easy. They can be called in two ways:
helper!(item);for emitting a diagnostic from an item that implementsEmittable(like strings or error types).helper!(spans => message);for emitting a diagnostic with a custom span and message.
You can also call helper!("message"); to use the call site span.
emit!(): Emit a diagnostic for a givenLevelhelp!(): Emit a help message (written to stderr on stable)note!(): Emit a note (written to stderr on stable)warn!(): Emit a warning (written to stderr on stable)error!(): Emit an errorbail!(): Emit an error and return with the default valuefatal!(): Emit an error andquit
§Assertions
mynt provides equivalents to assert_*! macros that instead
call fatal! instead of panic! for cleaner error output.
mynt_assert!(): Ensures an expression istruemynt_assert_eq!(): Ensures two expressions are equalmynt_assert_ne!(): Ensures two expressions are not equal
§Low-level API
mynt exposes some of its internals just in case.
Diagnostic: Manually write diagnosticsLevel: The level of diagnostic (Error/Warning/Note/Help)quit(): Quit the proc-macro and let mynt clean-up
§Feature Flags
default:proc-macro2,syndarling: support for darling error conversionnightly: support for nightly Rust’sproc_macro_diagnosticfeatureproc-macro2: support for proc-macro2 span conversionsyn: support for syn error conversionvenial: support for venial error conversionyansi: support for fallback (stable Rust) terminal coloring via yansi
§Example
This example (/examples/attribute/src/lib.rs) demonstrates the outer macro pattern,
a technique where we want to share information between invocations of macros
(like when we want to get information about items in a database schema),
which is not currently possible with macros (without risking determinism).
Instead, we can wrap macros in an outer macro, which can then find those macros, collect data from them, and then proceed with their implementations.
This pattern shows how mynt can shine, by allowing inner macros to produce output, even if other inner macros run into errors.
use std::collections::HashMap;
use mynt::*;
use ordered_multimap::ListOrderedMultimap;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote, quote_spanned};
// declare the attribute macro
// (we could also wrap the macro with mynt!, but this allows for defining the macro in another file)
mynt_macro_attribute!(schema => schema_impl);
fn schema_impl(attr: TokenStream, input: TokenStream) -> TokenStream {
mynt_assert!(attr.is_empty()); // ensure the outer macro doesn't have any arguments
let syn::ItemMod {
attrs,
vis,
unsafety,
mod_token,
ident,
content,
semi,
} = syn::parse2(input).unwrap_or_quit(); // parse with syn or quit with an error
let Some((_, items)) = content else {
fatal!(ident.span() => "not a inline module"); // quit if the module is to a file
};
let mut item_store = SchemaStore::new();
let mut output = HashMap::new();
for item in items {
let syn::Item::Struct(mut item_struct) = item else {
item_store.insert(None, ModItem::Not(item.to_token_stream()));
continue;
};
let attr: InnerAttr = match try_extract_attributes(&mut item_struct) {
Ok(Some(attr)) => attr,
Ok(None) => continue,
Err(err) => {
error!(err); // emit an error, but then continue with the rest
continue;
}
};
let ident = item_struct.ident.clone();
let schema_item = SchemaItem { attr, item_struct };
item_store.insert(Some(ident), ModItem::Schema(schema_item));
}
for item in &item_store {
let (Some(ident), ModItem::Schema(schema_item)) = item else {
continue;
};
output.insert(
ident.clone(),
match &schema_item.attr {
InnerAttr::Row => TokenStream::new(),
InnerAttr::Table(args) => table_impl(args, &schema_item.item_struct, &item_store),
},
);
}
let new_module: TokenStream = item_store
.into_iter()
.map(|(_, v)| match v {
ModItem::Schema(schema_item) => {
let original = schema_item.item_struct;
let output = output.get(&original.ident);
quote! {
#original
#output
}
}
ModItem::Not(token_stream) => token_stream,
})
.collect();
quote! {
#(#attrs)*
#vis
#unsafety
#mod_token
#ident
{
#new_module
}
#semi
}
}
// in a production proc macro, it would probably be better to use a custom implementation for compile time
type SchemaStore = ListOrderedMultimap<Option<syn::Ident>, ModItem>;
fn try_extract_attributes<T: deluxe::HasAttributes, R: deluxe::ExtractAttributes<T>>(
obj: &mut T,
) -> deluxe::Result<Option<R>> {
if obj.attrs().iter().any(|a| R::path_matches(a.path())) {
return R::extract_attributes(obj).map(Some);
}
Ok(None)
}
enum ModItem {
Schema(SchemaItem),
Not(TokenStream),
}
struct SchemaItem {
attr: InnerAttr,
item_struct: syn::ItemStruct,
}
#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes(schema))]
enum InnerAttr {
Row,
#[deluxe(transparent)]
Table(TableArgs),
}
#[derive(deluxe::ParseMetaItem)]
struct TableArgs {
row: syn::Ident,
pk: syn::Ident,
}
fn table_impl(args: &TableArgs, item_struct: &syn::ItemStruct, store: &SchemaStore) -> TokenStream {
let ident = item_struct.ident.clone();
let span = ident.span();
let TableArgs { row, pk } = args;
let getter_ident = format_ident!("find_by_{pk}");
let Some(ModItem::Schema(row_item)) = store.get(&Some(row.clone())) else {
bail!(row.span() => "row type not found in schema scope"); // emit an error and return the default
};
let syn::Fields::Named(row_fields) = &row_item.item_struct.fields else {
bail!(row.span() => "row struct needs named fields"); // in this function, the default is an empty stream
};
let Some(pk_field) = row_fields
.named
.iter()
.find(|f| f.ident.as_ref() == Some(pk))
else {
bail!(pk.span() => "primary key not found in row struct");
};
let getter_ty = &pk_field.ty;
quote_spanned! {span=>
impl #ident {
#[allow(unused_variables)]
pub fn #getter_ident(#pk: #getter_ty) -> #row {
#row {
#pk: 32,
..Default::default()
}
}
}
}
}Check out /examples on the repository to see how this macro is used and other examples.
Modules§
- fallback
- Fallback implementation of the diagnostic interface for the stable channel.
Macros§
- bail
- Emit an error diagnostic and then exit the current function with the
Default::defaultvalue. - emit
- Helper for emitting a diagnostic.
- error
- Helper for emitting a
Level::Errordiagnostic. - fatal
- Emit an error diagnostic and then
quit. - help
- Helper for emitting a
Level::Helpdiagnostic. - mynt
- General use entrypoint that wraps any proc-macro.
- mynt_
assert - Asserts that a boolean expression is
trueat runtime. - mynt_
assert_ eq - Asserts that two expressions are equal to each other (using
std::cmp::PartialEq). - mynt_
assert_ ne - Asserts that two expressions are not equal to each other (using
std::cmp::PartialEq). - mynt_
macro - Declares a function-like proc-macro entrypoint.
- mynt_
macro_ attribute - Declares an attribute proc-macro entrypoint.
- mynt_
macro_ derive - Declares a derive proc-macro entrypoint.
- note
- Helper for emitting a
Level::Notediagnostic. - warn
- Helper for emitting a
Level::Warningdiagnostic.
Traits§
- Emittable
- Trait implemented by types that can be emitted through diagnostics.
- Mynt
Result Ext - Extension trait for
Result. - ToSpans
- Helper trait implemented by types that can be converted to a multispan.
Functions§
- quit
- Panics with a marker that allows mynt to safely emit any errors accumulated before returning an empty token stream.
Type Aliases§
- Diagnostic
- Type alias to the available diagnostic struct.
- Level
- Type alias to the available diagnostic level enum.