7 releases
0.1.6 | Sep 8, 2024 |
---|---|
0.1.5 | Jan 29, 2024 |
0.1.2 | Oct 15, 2023 |
0.1.1 | Apr 2, 2023 |
#74 in Procedural macros
200KB
3.5K
SLoC
do-with-in
A template language for Rust metaprogramming using partial staging.
This crate lets you run code at compile time to produce the tokens other proc_macros will consume. It is also useful for ad-hoc in-place code generation, and for making templates (like Jinja or Mustache) for your code. It uses its own simple inner language based on handlers, variables, and a user-chosen sigil. The stage separation model of Rust does impose some limitations on what this crate can do.
Table of Contents
Background
This package was conceived to allow for a specific kind of refactoring in my fantasy cpu emulator needed to make the code fold more and be easier to do experiments with; that project makes use of a fairly complex proc macro to do quick system emulator generation for experimentation with different system designs in a type safe and integrated way, replacing a huge amount of non-type safe and non-integrated ad hoc gnarly perl scripts and build system code generation. But refactoring things in the use site of giant proc macros is not something that Rust had a good story for, so I ended up having to write that story myself, leading to this crate.
And while this can be used for some gnarly stuff like that, it turns out that it can also be used for simple code templating in ways that declarative macros struggle with too.
Limitations
We don't have true unquote-splicing, as that is more or less impossible in Rust due to the nature of Rust's stage separation.
Install
cargo add do-with-in
Usage
In its simplest form, you can wrap the invocation of some other proc_macro with do_with_in!
in order to do metaprogramming during the invocation of the thing - that is, in the input itself - using a fairly basic language.
#[macro_use]
extern crate do_with_in;
use do_with_in::*;
// ...
fn main() {
do_with_in!{
sigil: ~ // One of $, %, #, or ~
do // Separates front matter from body
// Define a handler called `header` that wraps its argument
~(mk header <head><title> ~(run ~1) </title></head>)
html!{
<html>
~(header {"My title"})
</html>
}
}
// ...
}
There are examples of the use of do_with_in!
for use site metaprogramming in the examples/
directory; an example showing off generation of compile time length safe access functions is at examples/mem.rs
. The various handlers are documented on the docs dot rs site; the tests at do_with_in_internal_macros/tests/do_with_in_test.rs
are a lot of simple cases exercising the functionality of the do_with_in!
macro.
API
do_with_in!()
This is the proc_macro most users of this crate will use.
There is front matter, which can define the sigil; the default is $
if no sigil is defined. Then do
, then after that is where the metaprogramming can happen.
In the metaprogramming section, variables are identifiers with a sigil prepended. You can create and assign to them with let
and var
handlers. Numbers with a sigil prepended are special variables that can be set inside a handler; you cannot assign to them with let or var. Brackets with a sigil prepended start a handler invocation; the handler invoked will be the first token inside the brackets, which must be an identifier. Everything else passes through the macro unchanged.
The Environment
Handlers
Default Handlers
If you are using do_with_in!
directly, the environment will be prepopulated with a set of default handlers to make it a 'batteries included' experience. See fn.genericDefaultHandlers
for the list of documented default handlers.
let
& var
Create and assign variables, identifiers with a prepended sigil. The difference between the two is that let
does not interpolate during either variable definition or use, whereas var
interpolates both.
The value assigned to a variable defined with let
will remain unchanged before it is used:
%(let bar = {let y = "bar"; })
%bar
// y == "bar"
Whereas a variable set with var
can make reference to other metaprogramming variables:
%(let foo = { 5; })
%(var bar = { %foo })
x = %bar
// x == 5
run
block
Evaluate block of code in place. Useful for pass-through arguments when building handlers, or to evaluate an unquoted array:
~(let thing = {~(quote ~x + ~y)})
let z = ~(run ~(unquote ~thing));
quote
& unquote
Similar to the LISP concept, quote
renders its arguments inert, and unquote
makes active a quoted argument.
This allows constructing and passing arround of an expression without premature evaluation.
~(let thing = {~(quote ~x + ~y)})
~(let
x = {3}
y = {4}
)
let z = ~(run ~(unquote ~thing)); // z == 7
mk
identifier block
A simple way to define new handlers at the use site. Allows the use of positional numbered parameters e.g. ~1
.
// mk defines the handler..
~(mk embolden <b> ~(run ~1) </b>)
// .. which can then be called with arguments
~(embolden {"World"})
if
testBlock trueBlock falseBlock
Conditional control flow. Test must return either true
or false
. Only one branch is expanded and executed.
let x = $(if {true} {4} {5}); // x == 4
// Test could be a $logic handler or set variable
let y = $(if {$(logic true & false)} {1} {2}); // y = 2
concat
params*
Concatenates its arguments into a string.
let x = $(concat 1 "abc" 2);
// x == "1abc2"
arithmetic
& logic
Provides basic numeric arithmetic operations and logic comparisons respectively.
// Return type must be specified for arithmetic:
let x = $(arithmetic u64 1 + 1 + 1); // x == 3
let y = $(logic false | ($N < $M))
Full documentation can be found on fn.arithmeticHandler
and fn.logicHandler
.
withSigil
newSigil params
Redefine which sigil to use for the scope of the handler.
$(let a = {"foo"})
let a = $(withSigil # #(concat #a "bar"));
marker
& runMarkers
Markers are a way to "pass" data from one invocation of do_with_in!
to another, allowing patterns such as sharing common
handler definitions and variables between do_with_in!
blocks.
The marker
handler embeds data in one invocation of do_with_in!
in a way that can be loaded by later invocations using runMarkers
.
$(marker "optional_name" =>
$(let x = { 3 })
$(mk foo
let $1 = $x * $2;))
This can then be invoked in another do_with_in!
block (potentially in the same file) like so:
$(runMarkers Base "path" "to" "file.rs" => "optional_name")
$(foo g 2) // $g == 12
Further documentation can be found on fn.markerHandler
and fn.runMarkersHandler
.
import
pathSegment*
Basic file inclusion. Path is specified by quoted segments; special unquoted identier Base
is used for the crate root.
$(import Base "src" "import.$")
A file:
hint can be included in the front matter to allow import to use a relative path.
do_with_in!{
file: "src/importable.rs"
sigil: $
do
$(import "import.$")
}
License
SEE LICENSE IN LICENSE file.
Dependencies
~0.3–8MB
~64K SLoC