Skip to content

Instantly share code, notes, and snippets.

@kurtlawrence
Created May 16, 2022 06:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kurtlawrence/a653c1d215ae8ce5041f43d684423991 to your computer and use it in GitHub Desktop.
Save kurtlawrence/a653c1d215ae8ce5041f43d684423991 to your computer and use it in GitHub Desktop.
`fit_kserd!` macro implementation used in daedalus
// # Fitting kserd macro.
//
// The macro logic is hidden from users to avoid having the long list of pattern matching exposed.
// It also helps control the pattern state.
//
// Fitting is split into four aspects:
// 1. Parsing
// 2. Flattening
// 3. Documentation
// 4. Code generation
//
// ## Parsing
// Parsing is the first phase. The intention is to take the macro input tokens and transform them
// into an intermediary representation, validating the input along the way.
// The intermediary format is designed for a pull based reading system. At the highest level are
// the _cases_, these are a stream of pattern (or _case_) tokens linked to a body expression.
//
// A _case_ format consists of a tree of tokens, but in a streamed, depth-first, format, with tags
// denoting starts and ends of nesting. For example, take the case pattern:
//
// ```
// Cntr {
// field1 = Bool(x),
// field2 = Cntr {
// field3 = Num(y)
// }
// field4 = Str(z)
// }
// ```
//
// The goal of this pattern is to have 3 local variables, x, y, z, which would be a bool, f64, and
// &str respectively. That is if the Kserd matches that data form. It was found that the most simple
// way of making the codegen work was to convert the input into a list of _paths_ from the root
// Kserd to the local variable assignment. To achieve this the intermediary format constructs a
// stream that flags nesting (indentation and new lines are added for clarity):
// ```
// @cntr_start
// @req(field1) "Bool" bool @local x
// @req(field2) @cntr_start
// @req(field3) "Num" float @local y
// @cntr_end
// @req(field4) "Str" str @local z
// @cntr_end
// ```
//
// When read in a stream the @cntr tags are used to define the start and end of the nesting. Local
// assigments carry some metadata, like the local identifier, the function identifier used to get
// that function, and the id literal (this is used for error handling).
//
// Parsing also handles fitting types (denoted with the `field: Type` syntax), and optional fields
// (denoted with `[field]` syntax). Optional fields change the @req to @opt, while fitting fields
// use a specific `@fit(Type)` syntax.
//
// It is important to note that the parsed stream is still in a _nested_ format.
//
// ## Documentation
// After the parsing phase, documenting and flattening are forked. This is done because documenting
// works better with a nested structure, while the flattening alters the stream into what the
// codegen phase can work with.
//
// Documenting is fairly simple, it consumes the parsed token stream and concatenates an expression
// together. This expression (which will be of literals) is stored until written to a `#[doc =
// expr]` attribute on the trait impl.
//
// ## Flattening
// Flattening takes the parsed stream of tokens and _flattens_ the nested structure into one which
// has a **defined path from the root Kserd** for each local assignment.
//
// While simple in scope the implementation is complex. In a high level view a prefix of tokens is
// maintained, acting like a stack where path components are popped when consumed. However, as the
// macro pattern matching will only match tokens that _precede_ a tt stream (otherwise it would be
// ambiguous), the prefix is **maintained in reverse order**. This also makes the whole case
// matching stream be constructed in a reverse manner and has to be reversed before being sent to
// the codegen.
//
// > Note that it is only the _case_ that is reversed, so this is operating inside each case and
// > the body expression is store alongside each pattern.
//
// These implementation complexities make flattening the most complex phase and care must be taken
// in this area if added syntax is wanted.
//
// ## Code generation
// Codegen imagines the cases token stream structure to be in a flatten format, where each local
// assignment carries a path before it from the root Kserd.
//
// The algorithm used will consume case matching and slowly build up the code. Nested structures
// use temporary variables to get to the final getting function from a Kserd. The code is built in
// a way that handles optionality in a general manner. It makes the code messier but saves on
// duplicating macro code which would be far more maintenance. The concept is that even without
// optional fields, the Kserd is lumped into a Option, and the next get function is then applied on
// that. Optionality is transitive, if a field is optional, all further matches are optional as
// well. It is important to note that the fields **before** the optional field are **not** optional
// and must return an error. To handle this there is a variable identifier that is carried through,
// starting as required and flipping to optional on the first optional field that is encountered.
//
// Code generation also had to handle the multiple cases. The way the fitting is intended to work
// is that there might be multiple different patterns of a Kserd that could match. Each of these
// patterns (case) should have fast fail attributes (making use of the ? operator), but chaining
// each one together was difficult. Instead a bespoke function is defined for each case. There is a
// limit of 32 of these cases that can be defined which should be more than enough. Each function
// has a signature that handles the Result, and then chaining these functions together to carry the
// failure messages is quite simple.
#[doc(hidden)]
#[macro_export]
macro_rules! fit_kserd_internal { // INTEREST -- using an internal call and making it doc hidden
// #### PARSE ##############################################################
// No more cases: Finished parsing.
(@parse[$doc:expr, $($args:tt)*]
{}
()
()
($($cases:tt)*)
) => {
// compile_error!(concat!("Finished Parsing:\n", stringify!($($cases)*))); // turn on to
// get some debugging
$crate::fit_kserd_internal! { @flatten[$crate::fit_kserd_internal!(@doc-init, $doc, $($cases)*), $($args)*] $($cases)* }
};
// No more input but last case hadn't been finished with expr
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
()
($($case:tt)*)
($($cases:tt)*)
) => {
compile_error!(concat!("expecting an expression associated with: ", stringify!($($case)*)));
};
// Recognise a case documentation.
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(#[doc = $doc:literal] $($tail:tt)*)
()
($($cases:tt)*)
) => {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* $doc }
($($tail)*)
()
($($cases)*)
}
};
// ---- Variant(ident) pattern ---------------------------------------------
// Match a Bool variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Bool($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Bool" bool @local $local)
($($cases)*)
}
};
// Match a Num variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Num($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Num" float @local $local)
($($cases)*)
}
};
// Match a Str variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Str($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Str" str @local $local)
($($cases)*)
}
};
// Match a Str variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Barr($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Barr" barr @local $local)
($($cases)*)
}
};
// Match a Tuple variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Tuple($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Tuple" tuple @local $local)
($($cases)*)
}
};
// Match a Cntr variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Cntr($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Cntr" cntr @local $local)
($($cases)*)
}
};
// Match a Seq variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Seq($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Seq" seq @local $local)
($($cases)*)
}
};
// Match a Tuple|Seq variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Tuple|Seq($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Tuple|Seq" tuple_or_seq @local $local)
($($cases)*)
}
};
// Match a Map variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Map($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Map" map @local $local)
($($cases)*)
}
};
// Match a Kserd variant
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Kserd($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* "Kserd" SPECIAL_KSERD @local $local)
($($cases)*)
}
};
// It looks to match a `Variant(ident)` pattern but hasn't matched, print error
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
($variant:ident($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
compile_error!(concat!("unknown variant '", stringify!($variant), "'."));
};
// It looks to match a `Variant|Variant(ident)` pattern but hasn't matched, print error
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
($variant:ident|$variant2:ident($local:ident) $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
compile_error!(concat!("unknown variant '", stringify!($variant), "|", stringify!($variant2), "'. Did you mean Tuple|Seq?"));
};
// ---- Variant { } pattern ------------------------------------------------
// Match a nested Cntr variant.
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(Cntr { $($nested:tt)+ } $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($nested)* @cntr_end $($tail)*)
($($case)* @cntr_start)
($($cases)*)
}
};
// It looks to match a `Variant { }` pattern but hasn't matched, print error
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
($variant:ident { $($nested:tt)+ } $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
compile_error!(concat!("unknown nested variant '", stringify!($variant), "'."));
};
// Match an _optional_ `[ident] = ` pattern
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
([$field:ident] = $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* @opt($field))
($($cases)*)
}
};
// Match an `ident = ` pattern
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
($field:ident = $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* @req($field))
($($cases)*)
}
};
// Match an _optional_ `ident: FitTy` pattern
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
([$field:ident]: $fit:ty, $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* @opt($field) @fit($fit))
($($cases)*)
}
};
// Match an `ident: FitTy` pattern
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
($field:ident: $fit:ty, $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* @req($field) @fit($fit))
($($cases)*)
}
};
// Print error if used a : but not with trailing comma
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
([$field:ident]: $e:tt $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
compile_error!(concat!("Expecting a trailing comma after a `Fit` type ascription> [", stringify!($field), "]: ", stringify!($e)));
};
// Print error if used a : but not with trailing comma
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
($field:ident: $e:tt $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
compile_error!(concat!("Expecting a trailing comma after a `Fit` type ascription> ", stringify!($field), ": ", stringify!($e)));
};
// End a Cntr
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(@cntr_end $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)* @cntr_end)
($($cases)*)
}
};
// Skip trailing commas in fields
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(, $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
)=> {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{ $($casedocs)* }
($($tail)*)
($($case)*)
($($cases)*)
}
};
// ---- Body ---------------------------------------------------------------
// Case separated by comma
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(=> $body:expr, $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
) => {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{}
($($tail)*)
()
($($cases)* ({ $($casedocs)* | $body } $($case)*),)
}
};
// Print error if a semi colon was used
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(=> $body:expr; $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
) => {
compile_error!("found a semi-colon at the end of a body, did you mean to use a comma?");
};
// Don't require comma after proper block
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(=> $body:block $($tail:tt)*)
($($case:tt)*)
($($cases:tt)*)
) => {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{}
($($tail)*)
()
($($cases)* ({ $($casedocs)* | $body } $($case)*),)
}
};
// No more tail, finish it off
(@parse[$($args:tt)*]
{ $($casedocs:literal)* }
(=> $body:expr)
($($case:tt)*)
($($cases:tt)*)
) => {
$crate::fit_kserd_internal! {
@parse[$($args)*]
{}
()
()
($($cases)* ({ $($casedocs)* | $body } $($case)*),)
}
};
// #### FLATTEN ############################################################
// Flattening uses a prefix stack. Fields are _always_ pushed onto the stack.
// They are popped when the field is 'consumed' is some way, that can be a local assigner, the
// cntr end, or the fitting ascription.
// ---- Case movements -----------------------------------------------------
// No more cases to flatten.
(@flatten_next[$($args:tt)*]
($($completed:tt)*)
()
) => {
$crate::fit_kserd_internal! { @gen-init[$($args)*] $($completed)* }
};
// Matched a case, start next case cycle.
(@flatten_next[$($args:tt)*]
($($completed:tt)*)
(({ $($body:tt)* } $($case:tt)*), $($tail:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ { $($body)* } $($completed)* ]
()
($($case)* $($tail)*)
()
}
};
// Flattening entry point.
(@flatten[$($args:tt)*] $($cases:tt)*) => {
$crate::fit_kserd_internal! {
@flatten_next[$($args)*]
()
($($cases)*)
}
};
// ---- Case building ------------------------------------------------------
// Recognised that the next sequence is the next case. Run the reversing logic.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
($( ({ $($body:tt)* } $($tmp:tt)*), )*)
($($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_rev[$($args)*] [ $($store)* ] [ $( ({ $($body)* } $($tmp)*), )* ]
($($case)*)
()
}
};
// Recognise the beginning of a cntr.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
(@cntr_start $($tail:tt)*)
($($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
($($case)*)
($($tail)*)
(cntr@ $($prefix)*)
}
};
// Recognise the end of a cntr. Consumes the last field assigner as well.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
(@cntr_end $($tail:tt)*)
(cntr@ ($field:ident)$f:ident@ $($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
($($case)*)
($($tail)*)
($($prefix)*)
}
};
// Recognise the end of a cntr. The prefix just has a cntr (this is the _last_ cntr_end)
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
(@cntr_end $($tail:tt)*)
(cntr@ $($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
($($case)*)
($($tail)*)
($($prefix)*)
}
};
// Add the fitting to a field.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
(@fit($fit:ty) $($tail:tt)*)
(($field:ident)$f:ident@ $($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
(($field, $fit)fit@ ($field)$f@ $($prefix)* $($case)*)
($($tail)*)
($($prefix)*)
}
};
// Recognise a NESTED local assignment. Eg f1 = Bool(x). Here the field is in the prefix.
// Notice the field prefix is consumed.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
($id:tt $get:ident @local $local:ident $($tail:tt)*)
(($field:ident)$f:ident@ $($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
($local $get $id ($field)$f@ $($prefix)* $($case)*)
($($tail)*)
($($prefix)*)
}
};
// Recognise a ROOT local assignment. Eg Bool(x) => "Bool" bool @local x
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
($id:tt $get:ident @local $local:ident $($tail:tt)*)
($($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
($local $get $id $($prefix)* $($case)*)
($($tail)*)
($($prefix)*)
}
};
// A field, this gets pushed onto the prefix stack.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
(@$f:ident($field:ident) $($tail:tt)*)
($($prefix:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_case[$($args)*] [ $($store)* ]
($($case)*)
($($tail)*)
(($field)$f@ $($prefix)*)
}
};
// Print error if not recognised.
(@flatten_case[$($args:tt)*] [ $($store:tt)* ]
($($case:tt)*)
($($tokens:tt)*)
($($prefix:tt)*)
) => {
compile_error!(
concat!("internal error: unrecognised flatten_case:\n",
stringify!($($tokens)*),
"\nPrefix:\n",
stringify!($($prefix)*)
)
);
};
// ---- Reversing ----------------------------------------------------------
// Finished reversing; Rebuild case and add to completed. Then begin the next seq.
(@flatten_rev[$($args:tt)*] [ { $($body:tt)* } $($completed:tt)* ] [$($nextcase:tt)*]
()
($($rev:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_next[$($args)*]
($($completed)* ({ $($body)* } $($rev)*),)
($($nextcase)*)
}
};
// Start/continue the flattening, pop a single token and push it back.
// This consumes many recusrion cycles, it is common to have to increase recursion limit.
(@flatten_rev[$($args:tt)*] [ $($store:tt)* ] [ $($nextcase:tt)* ]
($token:tt $($tail:tt)*)
($($rev:tt)*)
) => {
$crate::fit_kserd_internal! {
@flatten_rev[$($args)*] [ $($store)* ] [ $($nextcase)* ]
($($tail)*)
($token $($rev)*)
}
};
// #### CODE GENERATION ####################################################
// Generate the conditional invocation code. This invokes a case function, prepends the
// previous error. Only invokes a case function if the previous one returned an error.
(@gen[$out:ty, $kserd:ident, $cx:ident, $cxty:ty]
()
($($rem:tt)*)
($first:ident, $($fns:ident,)*)
) => {
#[allow(unused_mut)]
let mut r = $first($kserd, $cx);
$(
if let Err(a) = r {
r = $fns($kserd, $cx).map_err(|b| a.append(b));
}
)*
r
};
// Generate each internal function. This means the use of ? can be used, significantly
// simplifying control flow.
(@gen[$out:ty, $kserd:ident, $cx:ident, $cxty:ty]
(({ $($discard:literal)* | $body:expr } $($locals:tt)* ), $($cases:tt)*)
($name:ident, $($fns:ident,)*)
($($named:tt)*)
) => {
fn $name(__kserd: &$crate::Kserd, $cx: $cxty)
-> ::std::result::Result<$out, $crate::fit::FitError>
{
$crate::fit_kserd_internal!(@locals[Some(__kserd), __kserd, $cx] (@req) $($locals)*);
$body
}
$crate::fit_kserd_internal! {
@gen[$out, $kserd, $cx, $cxty]
($($cases)*)
($($fns,)*)
($($named)* $name,)
}
};
// Print error if cases does not match expected front.
(@gen[$($args:tt)*]
($($cases:tt)*)
($($fns:tt)*)
($($named:tt)*)
) => {
compile_error!(concat!("internal codegen error: unrecognised case representation: ", stringify!($($cases)*)))
};
// Print error if run out of fn names
(@gen[$($args:tt)*]
($($cases:tt)*)
()
($($named:tt)*)
) => {
compile_error!("reached maxiumum 32 cases supported.")
};
// Generate code for each case. Initialisation vector.
(@gen-init[$docs:expr, $on:ty, $out:ty, $cx:ident, $cxty:ty] $($cases:tt)*) => {
#[doc = $docs]
impl $crate::fit::Fit<&$crate::Kserd<'_>, $cxty, $out> for $on {
fn fit(__kserd: &$crate::Kserd, $cx: $cxty)
-> ::std::result::Result<$out, $crate::fit::FitError>
{
if __kserd.id().map(|id| !id.eq_ignore_ascii_case(stringify!($on))).unwrap_or(false) {
return Err($crate::fit::FitError::lazy(||
$crate::fit::FitErrorEntry {
desc: concat!("Expecting an id of `", stringify!($on), "`.").into(),
body: None
}));
}
// INTEREST
// since concatenating tokens is impossible (without `paste`), just specifies 32
// potential function names
$crate::fit_kserd_internal! {
@gen[$out, __kserd, $cx, $cxty]
($($cases)*)
(
_oper00, _oper01, _oper02, _oper03, _oper04, _oper05, _oper06, _oper07,
_oper08, _oper09, _oper10, _oper11, _oper12, _oper13, _oper14, _oper15,
_oper16, _oper17, _oper18, _oper19, _oper20, _oper21, _oper22, _oper23,
_oper24, _oper25, _oper26, _oper27, _oper28, _oper29, _oper30, _oper31,
)
()
}
}
}
};
// Print error if args don't match expected pattern; internal repr error.
(@gen-init[$($args:tt)*] $($cases:tt)*) => {
compile_error!(concat!("internal codegen error: did not recognise arguments: ", stringify!($($args)*)));
};
// Recognise that the field is optional and flip the optional flag.
(@locals[$($args:tt)*]
(@req) @cntr @opt $($tail:tt)*
) => {
$crate::fit_kserd_internal! {
@locals[$($args)*] (@opt) @cntr @opt $($tail)*
}
};
// Start the next kserd match cycle.
(@locals[$kserdopt:expr, $($args:tt)*]
(@$req:ident) @cntr @$ignore:ident($field:ident) $($tail:tt)*
) => {
let _tmp_kserd = match $kserdopt {
Some(_x) => $crate::fit_kserd_internal!(@locals @$req, _x, $field),
None => None,
};
$crate::fit_kserd_internal! { @locals[_tmp_kserd, $($args)*] (@$req) $($tail)* }
};
// Local assignment. Not optional so unwraps the kserdopt.
// Specialised Kserd variant, notice the get function.
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*]
(@req) $id:literal SPECIAL_KSERD $local:ident $($tail:tt)*
) => {
let $local = $kserdopt.expect("internal fit_kserd! macro error: req case is always Some");
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* }
};
// Local assignment. Not optional so unwraps the kserdopt.
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*]
(@req) $id:literal $get:ident $local:ident $($tail:tt)*
) => {
let _tmp_kserd = $kserdopt.expect("internal fit_kserd! macro error: req case is always Some");
let $local = _tmp_kserd.$get().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty($id, _tmp_kserd))?;
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* }
};
// Local assignment. Optional. Specialised Kserd variant, notice the get function.
// Always succeeds as just getting the Kserd
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*]
(@opt) $id:literal SPECIAL_KSERD $local:ident $($tail:tt)*
) => {
let $local = $kserdopt;
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* }
};
// Local assignment. Optional.
// The type of the field is non-optional and will throw an error if not matching
(@locals[$kserdopt:expr, $kserd:ident, $($args:tt)*]
(@opt) $id:literal $get:ident $local:ident $($tail:tt)*
) => {
let $local = match $kserdopt {
Some(_x) => Some(_x.$get().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty($id, _x))?),
None => None,
};
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $($args)*] (@req) $($tail)* }
};
// Fitting assignment. Not optional so unwraps the kserdopt.
(@locals[$kserdopt:expr, $kserd:ident, $cx:ident]
(@req) @fit($local:ident, $fit:ty) $($tail:tt)*
) => {
let $local = <$fit>::fit($kserdopt.expect("internal fit_kserd! macro error: req case is always Some"), $cx).nest($crate::fit::FitError::kserd_field_ty(stringify!($local), stringify!($fit)))?;
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $cx] (@req) $($tail)* }
};
// Fitting assignment. Optional.
// Follows similar logic to local assignment, a field existed with that name, then fitting
// fails propogate the error, NOT silently letting it through.
(@locals[$kserdopt:expr, $kserd:ident, $cx:ident]
(@opt) @fit($local:ident, $fit:ty) $($tail:tt)*
) => {
let $local = match $kserdopt {
Some(_kserd) => Some(<$fit>::fit(_kserd, $cx).nest($crate::fit::FitError::kserd_field_ty(stringify!($local), stringify!($fit)))?),
None => None
};
$crate::fit_kserd_internal! { @locals[Some($kserd), $kserd, $cx] (@req) $($tail)* }
};
// Inner kserd matching logic. Not optional so any failure makes use of the ? operator which
// will break out of the match sequence and return early.
(@locals @req, $kserd:ident, $field:ident) => {{
let _cntr = $kserd.cntr().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty("Cntr", $kserd))?;
let _kserd = _cntr.get(stringify!($field)).ok_or_else(|| $crate::fit::FitError::kserd_exp_field(stringify!($field), $kserd))?;
Some(_kserd)
}};
// Inner kserd matching logic. Optional so just propogates any non found values.
// Container types are NOT optional, only if the field does not exist.
(@locals @opt, $kserd:ident, $field:ident) => {{
let _cntr = $kserd.cntr().ok_or_else(|| $crate::fit::FitError::kserd_unexp_ty("Cntr", $kserd))?;
_cntr.get(stringify!($field))
}};
// No locals remain, do nothing.
(@locals[$($args:tt)*] (@$discard:ident)) => { };
// Print error if the @case pattern match fails
(@locals[$($args:tt)*]
$($tokens:tt)*
) => {
compile_error!(concat!("internal codegen error, unrecognised locals representation:\n", stringify!($($tokens)*)));
};
// #### DOCUMENTATION ######################################################
// Add the case documentation.
(@doc, $doc:expr, $( ({ $($casedocs:literal)* | $body:expr } $($case:tt)* ), )*) => {
concat!($doc,
$(
$( "//", $casedocs, "\n", )*
$crate::fit_kserd_internal!(@doc
("")
()
($($case)*)
),
)*
)
};
// No more docs.
(@doc ($doc:expr) ($($prefix:literal)*)
()
) => { $doc };
// Start of a container. Increment indent.
(@doc ($doc:expr) ($($prefix:literal)*)
(@cntr_start $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, "Cntr {\n"))
($($prefix)* " ")
($($tail)*)
)
};
// End of a container. New line after.
(@doc ($doc:expr) ($discard:literal $($prefix:literal)*)
(@cntr_end $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, $($prefix,)* "}\n"))
($($prefix)*)
($($tail)*)
)
};
// The variant id, eg Bool, Str, etc. Always new line after.
(@doc ($doc:expr) ($($prefix:literal)*)
($id:literal $get:ident @local $local:ident $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, $id, "\n"))
($($prefix)*)
($($tail)*)
)
};
// Required fitting: `field: Type`
(@doc ($doc:expr) ($($prefix:literal)*)
(@req($field:ident) @fit($fit:ty) $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, $($prefix,)* stringify!($field), ": ", stringify!($fit), "\n"))
($($prefix)*)
($($tail)*)
)
};
// Optional fitting: `[field]: Type`
(@doc ($doc:expr) ($($prefix:literal)*)
(@opt($field:ident) @fit($fit:ty) $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, $($prefix,)* "[", stringify!($field), "]: ", stringify!($fit), "\n"))
($($prefix)*)
($($tail)*)
)
};
// Required field assigner: `field = `
(@doc ($doc:expr) ($($prefix:literal)*)
(@req($field:ident) $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, $($prefix,)* stringify!($field), " = "))
($($prefix)*)
($($tail)*)
)
};
// Optional field assigner: `[field] = `
(@doc ($doc:expr) ($($prefix:literal)*)
(@opt($field:ident) $($tail:tt)*)
) => {
$crate::fit_kserd_internal!(@doc
(concat!($doc, $($prefix,)* "[", stringify!($field), "] = "))
($($prefix)*)
($($tail)*)
)
};
// Entry point. Prefixes the supplied docs with a # Grammar section.
(@doc-init, $doc:expr, $($tokens:tt)*) => {
$crate::fit_kserd_internal!(@doc, concat!($doc, "\n\n# Grammar\n\n```text\n"), $($tokens)*)
};
// #### ENTRY POINTS #######################################################
($docs:literal, $on:ty, $out:ty, $cx:ident, $cxty:ty >>) => { compile_error!("empty grammar"); };
($docs:literal, $on:ty, $out:ty, $cx:ident, $cxty:ty >> $($grammar:tt)*) => {
$crate::fit_kserd_internal! {
@parse[$docs, $on, $out, $cx, $cxty] {} ($($grammar)*) () ()
}
};
}
/// Macro to define [`Fit`] implementations using a grammar like pattern matching mechanism.
///
/// To increase the visibility of a [`Fit`] implementation (that is fitting a [`Kserd`]), the fit
/// macro accepts a pattern grammar that closely matches the variants of [`kserd::Value`]. The
/// macro parses this grammar and constructs the required rust code to assign the pattern locals.
/// It is the recommended way of fitting [`Kserd`] structures as it standardises the documentation,
/// errors, and matching behaviour.
///
/// # Example usage
/// The macro is used on `daedalus` defined items. As an example, to define a fit to a Table one
/// might use the macro like so:
/// ```rust
/// # use repr::*;
/// struct Table; // dummy struct
/// fit_kserd!("Some documentation on the fit", Table, cx is &str >>
/// /// Documentation for case
/// Tuple|Seq(rows) => {
/// // here `rows` will be &Vec<Kserd>
/// todo!()
/// },
/// Cntr {
/// [header] = Tuple(header),
/// data = Seq(rows)
/// } => {
/// // `header` would be optional: Option<&Vec<Kserd>>,
/// // `data` would be &Vec<Kserd>
/// todo!()
/// }
/// );
/// ```
///
/// Each pattern has a body expression associated with it. The body must return a `Result`, the ok
/// type will be defined either as the input structure, or another type if the alternate overload
/// is used. The error type is [`FitError`].
///
/// # Usage Notes
/// ## Recursion Limits
/// The macro sometimes reaches recursion limits, especially in nested patterns. Increasing the
/// crate defined `#![recursion_limit="256"]` usually assuages the issue.
///
/// ## `?` Operator
/// The `?` operator can (and should) be used in body expressions. This can make using [`FitError`]
/// more ergonomic.
///
/// ## Optionality
/// Nested fields can be optional. The syntax to use is `[field_name]`.
///
/// ## Identifiers
/// Fields and variable identifiers have to be valid rust identifiers, this means that `field-name`
/// is illegal.
///
/// ## Fitting
/// Using the syntax `field: Type,` tells the macro to invoke a fitting mechanism on the kserd
/// data at that field. This is used to compose fitting together. Fits can also be optional with
/// `[field]: Type`. `Type` needs to support fitting by implementing [`Fit`].
///
/// ## Overloads
/// The macro has 4 variants. The most basic signature is `T >> /* patterns */`. This applies the
/// [`Fit`] trait to `T` and the result is also of type `T`. If the result type needs to be
/// something other than `T` that be specified with `T, U >> /* patterns */`. Both of these
/// signatures can also have a literal doc string as the first argument. The doc string gets
/// prepended to the grammar docs.
///
/// ## Documentation
/// Along with a preliminary doc string, each case can be documented using the `///` syntax _above
/// the case_.
///
/// ## Context and working directory
/// The action context and working directory are defined _before_ the grammar, using identifiers
/// specified bracketed by paranthese: `(cx, wd)`. These identifiers are arbitary and enable access
/// to the context and working directory defined on [`Fit`].
///
/// # Grammar Syntax
/// Most variants except `Unit` are supported. There is support for `Tuple|Seq` where it will match
/// on a tuple _or_ a sequence. `Cntr` variants have nesting syntax with curly braces, each field
/// defined with the variant or type it will match against.
///
/// ## All Variants
/// Required:
/// ```rust
/// #![recursion_limit="256"]
/// # use repr::*;
/// use std::collections::BTreeMap;
/// use std::path::Path;
///
/// struct A;
/// fit_kserd! { A, cx is () >>
/// Cntr {
/// a = Bool(a),
/// b = Num(b),
/// c = Str(c),
/// d = Barr(d),
/// e = Tuple(e),
/// f = Cntr(f),
/// g = Seq(g),
/// h = Tuple|Seq(h),
/// i = Map(i),
/// } => {
/// let a: bool = a;
/// let b: f64 = b;
/// let c: &str = c;
/// let d: &[u8] = d;
/// let e: &Vec<Kserd> = e;
/// // f is an Accessor here: see kserd docs for more info
/// let g: &Vec<Kserd> = g;
/// let h: &Vec<Kserd> = h;
/// let i: &BTreeMap<Kserd, Kserd> = i;
///
/// // access the context
/// let cx: () = cx;
///
/// Ok(A)
/// }
/// }
/// ```
///
/// Optional:
/// ```rust
/// #![recursion_limit="256"]
/// # use repr::*;
/// use std::collections::BTreeMap;
///
/// struct A;
/// fit_kserd! { A, cx is () >>
/// Cntr {
/// [a] = Bool(a),
/// [b] = Num(b),
/// [c] = Str(c),
/// [d] = Barr(d),
/// [e] = Tuple(e),
/// [f] = Cntr(f),
/// [g] = Seq(g),
/// [h] = Tuple|Seq(h),
/// [i] = Map(i),
/// } => {
/// let a: Option<bool> = a;
/// let b: Option<f64> = b;
/// let c: Option<&str> = c;
/// let d: Option<&[u8]> = d;
/// let e: Option<&Vec<Kserd>> = e;
/// // f is an Accessor here: see kserd docs for more info
/// let g: Option<&Vec<Kserd>> = g;
/// let h: Option<&Vec<Kserd>> = h;
/// let i: Option<&BTreeMap<Kserd, Kserd>> = i;
///
/// Ok(A)
/// }
/// }
/// ```
///
/// [`Fit`]: crate::fit::Fit
/// [`FitError`]: crate::fit::FitError
/// [`Kserd`]: ::kserd::Kserd
/// [`kserd::Value`]: ::kserd::Value
#[macro_export]
macro_rules! fit_kserd {
($docs:literal, $on:ty, $out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => {
$crate::fit_kserd_internal! { $docs, $on, $out, $cx, $cxty >> $($grammar)* }
};
($on:ty, $out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => {
$crate::fit_kserd!("", $on, $out, $cx is $cxty >> $($grammar)*);
};
($docs:literal, $out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => {
$crate::fit_kserd!($docs, $out, $out, $cx is $cxty >> $($grammar)*);
};
($out:ty, $cx:ident is $cxty:ty >> $($grammar:tt)*) => {
$crate::fit_kserd!("", $out, $out, $cx is $cxty >> $($grammar)*);
};
}
#[cfg(test)]
mod tests {
use kserd::{ds::Accessor, Kserd, Kstr};
use std::collections::BTreeMap;
// Tests the 4 entry points and all variants for compilation.
#[test]
fn fit_macro_compilation_test1() {
struct A;
fit_kserd!(A, _cx is () >> Bool(_x) => { Ok(A) });
}
#[test]
fn fit_macro_compilation_test2() {
struct A;
fit_kserd!("Doc", A, _cx is () >> Num(_x) => Ok(A));
}
#[test]
fn fit_macro_compilation_test3() {
struct A;
struct B;
fit_kserd!(A, B, _cx is () >> Str(_x) => { Ok(B) });
(A, ()).1;
}
#[test]
fn fit_macro_compilation_test4() {
struct A;
struct B;
fit_kserd!("Doc", A, B, _cx is () >> Barr(_x) => Ok(B));
(A, ()).1;
}
#[test]
fn fit_macro_compilation_every_variant() {
struct A;
fit_kserd! { A, cx is () >>
Cntr {
a = Bool(a),
b = Num(b),
c = Str(c),
d = Barr(d),
e = Tuple(e),
f = Cntr(f),
g = Seq(g),
h = Tuple|Seq(h),
i = Map(i),
j = Kserd(j),
} => {
let _a: bool = a;
let _b: f64 = b;
let _c: &str = c;
let _d: &[u8] = d;
let _e: &Vec<Kserd> = e;
let _f: Accessor<&BTreeMap<Kstr, Kserd>> = f;
let _g: &Vec<Kserd> = g;
let _h: &Vec<Kserd> = h;
let _i: &BTreeMap<Kserd, Kserd> = i;
let _j: &Kserd = j;
let _cx: () = cx;
Ok(A)
}
}
}
#[test]
fn fit_macro_compilation_every_variant_opt() {
struct A;
fit_kserd! { A, _cx is () >>
Cntr {
[a] = Bool(a),
[b] = Num(b),
[c] = Str(c),
[d] = Barr(d),
[e] = Tuple(e),
[f] = Cntr(f),
[g] = Seq(g),
[h] = Tuple|Seq(h),
[i] = Map(i),
[j] = Kserd(j),
} => {
let _a: Option<bool> = a;
let _b: Option<f64> = b;
let _c: Option<&str> = c;
let _d: Option<&[u8]> = d;
let _e: Option<&Vec<Kserd>> = e;
let _f: Option<Accessor<&BTreeMap<Kstr, Kserd>>> = f;
let _g: Option<&Vec<Kserd>> = g;
let _h: Option<&Vec<Kserd>> = h;
let _i: Option<&BTreeMap<Kserd, Kserd>> = i;
let _j: Option<&Kserd> = j;
Ok(A)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment