4.7 KiB
We make a distinction between positional and prioritized macros.
Named macro
macro (
rule match ...$expr { ...$body } => \recurse. '(
fn::pass (...$expr) \match::value. ..$(
fn::pass (quote::split body ';) \cases.
fn::pass (list::map cases \x. (
fn::pass (quote::split_once x '=>) \pair.
tuple::destr pair 2 \req. \handler.
fn::pass (recurse '(match::request (...$key))) \match_res.
quote::match '(match::response $decoder (...$bindings)) match_res \match_res_match.
fn::pass (option::expect match_res_match "Invalid pattern ${key}") \res_parts.
fn::pass (map::get_unwrap res_parts "decoder") \decoder.
fn::pass (map::get_unwrap res_parts "bindings") \bindings.
fn::pass (quote::to_list bindings) \binding_names.
fn::pass (list::rfold handler \tail. \name. '( \ $name . $tail )) \success.
'( $decoder $success )
)) \case_fns.
list::append case_fns '( panic "No cases match" )
)
)
)
Named macro patterns must start with a name token. They are always evaluated first. If they don't end with a vectorial placeholder, macro evaluation continues after them so that they can become first arguments to infix operators.
Prioritized macro
macro 3 (
...$lhs + ...$rhs:1 => \recurse. '(add (..$(recurse lhs)) (..$(recurse rhs)))
)
Prioritised macro patterns must start and end with a vectorial placeholder. They represent infix operators.
Algorithm
Macros are checked from the outermost block inwards.
- For every name token, test all named macros starting with that name
- If the tail is implicit, continue iterating
- Test all prioritized macros
- Take the first rule that matches in the highest prioritized block
Test all in a set of macros
- Take the first rule that matches in each block
- If there are multiple matches across blocks, raise an ambiguity error
- If the single match is in the recursion stack, raise a recursion error
- Add the matching rule to the recursion stack, then execute the body.
Considerations
Maxims for the location of macros
- Macro patterns are held in the host, they don't contain atoms, and atoms are never considered equal, so the matcher doesn't have to call an extension.
- The body of macros may be defined in Rust. If it isn't, the entire interpreter will recurse on the macro to calculate the output.
On recursion, the following errors can be detected
- if the rule body uses the same macro, fail with the rule
- if the rule explicitly recursively invokes the same macro, fail with the first match
Elements of the extension
Recursion has to happen through the interpreter itself, so the macro system is defined in terms of atoms just like an extension
-
atom
MacTreedepicts a single token.MacTree(tpl)is also aMacTreebut it can contain the independently unrepresentable templated slot node -
lexer
'followed by any single token always generatesMacTree. If it contains placeholders which are tokens prefixed with$or..$, it generates a call toinstantiate_tplwith a preparedMacTree(tpl)as the first argument and the placeholder values after.MacTree(tpl)only exists as an internal subresult routed directly toinstantiate_tpl. -
line parser
macroparses a macro with the existing logic -
atom
MacRecurStateholds the recursion state -
function
resolve_recurfinds all matches on a MacTree- type:
MacRecurState -> MacTree -> MacTree - use all relevant macros to find all matches in the tree
- since macros must contain a locally defined token, it can be assumed that at the point that a constant is evaluated and all imports in the parent module have been resolved, necessarily all relevant macro rules must have been loaded
- for each match
- check for recursion violations
- wrap the body in iife-s corresponding to the named values in the match state
- emit a recursive call to process and run the body, and pass the same recursive call as argument for the macro to use
(\recur. lower (recur $body) recur) (resolve_recur $mac_recur_state) - emit a single call to
instantiate_tplwhich receives all of these
- type:
-
function
instantiate_tplinsertsMacTreevalues into aMacTree(tpl)- type:
MacTree(tpl) [-> MacTree] -> MacTree
this function deduces the number of arguments from the first argument. This combines poorly with autocurry, but it's an easy way to avoid representing standalone tree lists - walks the tree to find max template slot number, reads and type checks as many template values
- returns the populated tree
- type:
-
function
resolveis the main entry point of the code- type:
MacTree -> MacTree - invokes
resolve_recurwith an emptyMacRecurState
- type:
-
function
loweris the main exit point of the code- type:
MacTree -> any - Lowers
MacTreeinto the equivalentExpr.
- type: