Ok, let’s talk about your concrete example than. First we need to know what does the file actually contain and what are we trying to move to a previous stage. I’d say that using a Vec<libc::tm>
is bad design anyway because as you said it is platform dependent.
So our stages here are:
- read the file and de-serialize its contents from some known format.
- create a const expression that contains that contents.
macro.rs (in pseudo-code):
macro read_from_file(file_name: Path) -> Expr {
let vec: Vec<String> = read_file(file_name);
let expr = arrayExpression::new();
for e in vec.lines() {
expr.add(e.to_expr());
}
expr
}
my_module.rs:
const Known = read_from_file!(".");
what I’m aiming for here is that the macro generates the initialization expression. So that the above will be transformed to something like:
const Known = [ libc::tm {year: 1999, month: 1, day: 1, ...}, ... ];
This is portable and works with cross-compilation. There are a few points of note here:
-
What is the type of Known? I’d argue that a Vec is the wrong type since it allocates on the heap. The macro should generate an
[T; n]
-
The macro seems complex, right? Well there are two parts to this - there is re-use of regular “run-time” code to read the file for example but the other part I wrote with some API is to generate the expression. In Nemerle there is built in splicing syntax that is very ergonomic for this latter part:
macro (…) { <[ print (“first”); ]> print (“second”); }
Everything inside <[ ]> is output tokens (the return type of the macro, an Expr in our example) and everything outside is executed regularly. Using such quasi-quoting syntax makes the above macro much shorter and much simpler to understand.
3. What if I want a different data-structure, say a map? Rust will need to have something like the new c++ initialization syntax which allows to write:
std::vector<int> v1{1, 2, 3};
std::map<int, int> v2{{1, 2}, {2, 3}};
Or in Rust-ish :
let v: std::Vec<Foo> = [ {..}, ..]; // notice there is no vec! macro call here
let v: map<K, V> = {{k1, v1}, {k2, v2}, ..};
- The macro itself can execute itself any code, in our example it does I/O, memory allocation, etc… but there is a strict separation of concerns, it cannot allocate the memory to be used by the “run-time” code which runs in a separate stage. Instead, it returns the expression so that the next stage can have a static value it can manipulate. It generates new source code.
Most of the above exists in some form already in Rust. We just need to fill some holes to make it ergonomic and fun.