Writing Complex Macros in Rust: Reverse Polish Notation
blog.cloudflare.com
blog.cloudflare.com
The other threads talk a bit about general metaprogramming, so let's talk about the other bit. On the generics side, here's an illustrative example, taken from http://en.cppreference.com/w/cpp/language/function_template
#include <iostream>
template<typename T>
void f(T s)
{
std::cout << s << '\n';
}
int main()
{
f(7);
}
Here, the function f will take any type T, and print it out. in this case, 7 is an int, so T is int.In Rust, here's the same function:
fn f<T>(s: T) {
println!("{}", s);
}
fn main() {
f(7);
}
here, 7 is an i32, so T is i32. But, we get a compilation error in Rust: error[E0277]: the trait bound `T: std::fmt::Display` is not satisfied
--> src/main.rs:2:22
|
2 | println!("{}", s);
| ^ `T` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
|
= help: the trait `std::fmt::Display` is not implemented for `T`
= help: consider adding a `where T: std::fmt::Display` bound
= note: required by `std::fmt::Display::fmt`
error: aborting due to previous error
This illuminates a core difference between Rust's generics and C++'s templates: in the C++ case, the code is typechecked after the code is expanded. So it works. However, we don't even need that `main` to cause a compilation error in Rust; it only checks the body of the function. In this case, Rust says "Hey, you accept any T, but you try to print it out. What if a type isn't printable? That wouldn't work!"The fix is:
use std::fmt::Display;
fn f<T: Display>(s: T)
// or in near-future Rust, a slightly nicer syntax, still needs the 'use' line too:
fn f(s: impl Display)
This changes the signature to say "we take any type T that implements the Display trait." Now we know that the body of the function works properly.There are advantages and disadvantages to both approaches. In some senses, the tradeoffs are the same as any duck typed vs not situation: the C++ approach is far more flexible, but the Rust approach has stronger guarantees. We also chose the style we did because we prefer the nicer errors; C++ template errors are notoriously complex, though many say they get used to how to read them over time.
Additionally, neither languages' story is complete here. C++20 has a new feature called "Concepts" that gives you some degree of similar features here, but it's complex and I haven't read the version that was merged into the draft of C++20, so maybe someone else can elaborate on it. Rust is also adding a 'const fn' feature that gives you access to similar tricks to constexpr in C++, as well as more features for our bounded generics.
#include <iostream>
#include <string>
#include <type_traits>
class Display {
virtual std::string fmt() = 0;
};
template<typename T>
void f(T s)
{
static_assert(std::is_base_of<Display, T>::value, "Error: T must implement Display interface");
std::cout << s << '\n';
}
int main()
{
f(7);
}
Gives the following error with clang, <source>:12:7: error: static_assert failed "Error: T must implement Display interface"
static_assert(std::is_base_of<Display, T>::value, "Error: T must implement Display interface");
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:19:7: note: in instantiation of function template specialization 'f<int>' requested here
f(7);
^
1 error generated.
Compiler returned: 1
Of course the downside is having the library author spending extra effort to write those checks.Beyond that as well, you won't get an error if you don't use f(7), that is, if your main is empty, which would affect library authors.
use std::fmt::Display;
fn f<T: Display>(s: T) {
println!("{}", s);
}
fn main() {
f(7);
}
whereas there would be no way to get `f(7)` to compile in your C++ version since you can't make `int` inherit from `Display`. _A_ downside is that the library author has to add these sorts of checks, but for me a bigger downside is that anyone who then wants to use the library has to inherit from the library's types.Which means the templates themselves can still impose additional requirements based only on their internals, leading to the same bad error messages for users and lack of checking for library authors that we have without concepts.
And, nobody really steps through code anymore. So a lot of the difficulty argument is moot.
I don't follow. What other technique is in, compared to code stepping?
Sometimes I put a breakpoint in and step thru code to help me figure if something is working properly or maybe live-inspect the return value of something that comes from the outside world. Maybe I'm missing some new fundamental technique to dev/debug?
Thanks in advance. (genuine question)
Rust as a project is investing heavily in working towards excellent debugger support, because people do use it.
It even works remotely attached to bare metal embedded projects on stm32 microcontrollers!
Unfortunately debugging isn't available with intellij-rust in the free IDEA community edition.
I don't actually have a lot of experience with the rust world, but the main complaint with macros is typically that you can't step through them easily.
With my getting downvoted, I'm assuming I'm wrong and that step debuggers are more common in rust than I was assuming. I think that makes me happy. If anyone has a good video demonstrating this level of tooling, I'd be interested.
Some of it may be situational. With how many microservices I'm dealing with, "stepping into" a call really needs to jump to another process quite often.
Anyone have numbers, by chance, on how many folks actually use a lot of these features?
Usually the problema is that many developers never learned what a visual debugger is capable of, and the typical command line interface in UNIX leaves little room for exploration if people aren't curious to learn about them.
For example, many aren't aware that gdb has a TUI that eases the experience.
But, really, this is something I've never actually done. Reasoning about how the system should behave has been enough to get me by.
When some condition took place, the problematic process would break into the debugger, while the others would be parked waiting for me to attach to them.
However, I believe you can't do that with C++ templates. C++ template parameters can be types or primitive values, but not expressions. And the body of a template has to be some sort of declaration, e.g. a function definition or struct definition, but not an expression or statement.
They require someone with a great deal of energy, knowledge and will to win in order to tease anything useful out of them. This, I submit, is a feature. It dissuades the programmer who believes they are better than they are from being too clever by half.
There are worse things in the programming sphere than crap meta-programming but it does have its own circle in hell.
I appreciate that the Rust team are working hard to spoil my happy idyll but happy I remain for now.
The C preprocessor and C++ templates are not difficult to use. But they're both difficult to use well. So they get used. A lot. Badly.
C++ tried to add a type system for templates with "concepts" but it was scrapped.
Rust instead has properly typed generics instead using the trait system, resulting in proper static checking and proper error messages (although without higher kinded types and const generics currently, that C++ instead supports).
Rust also has a powerful macro system, which is unrelated to templates, although you can use them to have kinds of abstractions that the language doesn't support directly (like higher kinded types, const generic or abstracting over mutability), at the cost of having to explicitly perform instantiation, not having inference and having a higher cognitive burder.
concepts was merged into the C++2020 draft recently, so not entirely scrapped! I forget if it was full-blown concepts or "concepts lite" though.
Besides that, it is already possible to use typed templates in C++, at least with any C++14 compiler and even better on C++17, even if it requires a bit more of effort for the implementer of the generic code.
Basically by making use of type traits, static asserts, constexpr if and enable_if.
Yes, it isn't as clean as Rust but as shown by Andrei Alexandrescu with D, it opens the door for very powerful designs.
suppose you have a concept Hashable that is only enabled for type with a hash() function. But you can still in the code of your HashMap do a comparison with the < operator, which the Hashable concept don't ensure. In that case you woud only have an error at instentiation type for Hashable types that don't have '<'
In Rust, the compiler make sure that your generic function only use operations that exits in the traits declared in the function signature.
I thought it was possible to do something like this? (disclaimer: I don't use c++)
class Hashable { abstract char *hash() = 0; } template<typename T: Hashable, typename U> class Hashmap { vector<U> buckets; // etc. }
Since you get some kind of compile-time diagnostic, then effectively, the type is checked. Just, within static time, not as early as you'd like, with not as a relevant a diagnostic.
ISO C++ doesn't dictate the wording of diagnostics; a C++ implementation could work backward from that error, or do whatever else, in order to phrase the diagnostic in terms of a problem in the template.
C++ templates being checked only for instantiated types makes them flexible. E.g. a template that accesses type::foo or instance.foo will work with anything that has a member named foo. That template argument doesn't have to have a declared relationship to any type which has foo, which would be an annoying restriction for the users.
Types that aren't instantiated don't exist. They don't correspond to anything in the executable image. Worrying about them is like a recording studio fussing over how much reverb to add to a one-handed clap.
It's fine to not worry about types that don't get instantiated if you are the only person ever instantiating a template, but that's obviously not always the case. If my template doesn't typecheck for a type that someone else wrote, I don't find out about it until they send me a bug report. With typechecking before instantiating, I find out about it as soon as I write it.
> A type error that doesn't execute doesn't really exist either.
Nope; that depends on what you mean by "doesn't execute".
Provably doesn't execute? As in: it is removable, dead code?
Or: doesn't execute because it's not covered by a test case? So that it could execute if a suitable input is found?
A template-generated type that is not instantiated doesn't execute because it doesn't exist. It's not in the image. There is no possible input to the program which can exercise it.
The code is basically not written. There is no need to diagnose bugs in code that isn't written.
Making users derive template arguments from a specific type is just stupidly "blubby". That C++ has typeless template parameters is one of the few sane things about the way it supports generic programming.
> With typechecking before instantiating, I find out about it as soon as I write it.
With type checking, that same user who would have run into a problem with the "typeless template" will simply find your template useless, and roll their own. In other words, same situation as before. The type error will still be there.
If you have a template that is intennded with foo-like arguments, and the user uses them with a bar-like object, that's going to be diagnosed one way or another.
If the generic arguments are weak, it will show up as some constraint violation when the template-generate code uses the the bar-like thing as if it were foo-like.
If the generic arguments are strong, the user instead gets an error that the bar-like thing is not a suitable argument to the template, which expects a foo-like.
It's the same either way; it's all static checking at the same static time and it doesn't change the circumstances under which errors occur. It just changes the amount of discipline needed to use the generic, and the accuracy/granularity of the diagnostic ("this thing is not a foo" versus "this thing here coming from a template parameter can't be used in this way in this piece of template generated code").
It's easier for the user to adjust the bar object to make it sufficiently foo-like for a given template, than to make it completely conform to the foo type.
Unless you have a bug in your template that causes it to have a type error for a type that you didn't intend for it to have a type error for.
You might intend (without actually testing) that the template works with some bar also, not related to foo; the user tries it and finds otherwise, due to a bug.
With the typed generic, you just declare that it's for foo, and it refuses to work with bar (whether or not it has a bug which would actually prevent that in the absence of the declaration).
All the declaring, checking, instantiation, and the rest of it are all happening in the same time: static time. There is no difference like with static versus dynamic. Static versus dynamic is due to different times: what is done before running the program, versus during.
There is no verification difference between declaring that a template parameter may be of types foo | bar, and writing a typeless template which a test module then instantiates for foo and bar. Both are declarative mechanisms at work, played out statically. Just one way is more open-ended; it's not "sealed" against new instantiations.
= note: expanding `rpn! { @ op [ 4 , 3 + 7 , 2 ] * }`
= note: to `rpn ! ( [ 3 + 7 * 4 , 2 ] )`
Is that not, like, operator precedence gone really wrong? Or is this the rust stringifier in trace_macros messing up without anything going wrong in the actual AST?folding * over 4, 3+7, 2
*/ 4, 3+7, 2
*/ 3+7 * 4, 2
*/ 2 * 3+7 * 4
=> 2 * 3 + 7 * 4
so basically, beware ...
= note: to `rpn ! ( [ 5 , (3 + 7) * 4 , 2 ] )`
= note: expanding `rpn! { [ 5 , (3 + 7) * 4 , 2 ] }`
...Trying rpn-compile at the REPL:
$ txr -i rpn.tl
1> (rpn-compile '(3))
(let* () 3)
2> (rpn-compile '(3 4 +))
(let* ((#:g0266 4)
(#:g0267 3)
(#:g0268 (+ #:g0266 #:g0267)))
#:g0268)
3> (rpn-compile '(3 4 + 5 6 + *))
(let* ((#:g0269 4)
(#:g0270 3)
(#:g0271 (+ #:g0269 #:g0270))
(#:g0272 6)
(#:g0273 5)
(#:g0274 (+ #:g0272 #:g0273))
(#:g0275 (* #:g0274 #:g0271)))
#:g0275)
4> (rpn-compile '(3 4 + dup *))
(let* ((#:g0276 4)
(#:g0277 3)
(#:g0278 (+ #:g0276 #:g0277))
(#:g0279 (* #:g0278 #:g0278)))
#:g0279)
Code in rpn.tl: (defstruct rpn-compile-time nil
temps
lets
stack)
(defun allocate-temp (rct)
(push (gensym) rct.temps)
(first rct.temps))
(defun compile-expr (rct expr)
(if (member expr rct.temps)
expr
(let ((temp (allocate-temp rct)))
(push ^(,temp ,expr) rct.lets)
temp)))
(defun compile-dup (rct)
(let ((top (compile-expr rct (pop rct.stack))))
(push top rct.stack)
(push top rct.stack)))
(defun compile-swap (rct)
(swap (first rct.stack) (second rct.stack)))
(defun compile-binop (op rct)
(let* ((left (compile-expr rct (pop rct.stack)))
(right (compile-expr rct (pop rct.stack)))
(sum (compile-expr rct ^(,op ,left ,right))))
(push sum rct.stack)))
(defvar *compile-table*
^#H(() (dup ,(fun compile-dup))
(swap ,(fun compile-swap))
(+ ,(op compile-binop '+))
(- ,(op compile-binop '-))
(* ,(op compile-binop '*))
(/ ,(op compile-binop '-))))
(defun rpn-compile (exprs)
(let ((rct (new rpn-compile-time)))
(each ((word exprs))
(iflet ((fun [*compile-table* word]))
[fun rct]
(push word rct.stack)))
^(let* ,(reverse rct.lets) ,(first rct.stack))))
Having this function, making the macro is trivial. At the REPL again: 5> (defmacro rpn (. exprs) (rpn-compile exprs))
rpn
6> (rpn 2 2 +)
4If you have a compiler which can reduce (let ((x 3)) ((y 4)) (+ x y)) to 5, why replicate that in a macro.
If you do not have one (as is the case here), then it's better to work on getting one than compensating for it in macros.
Yet, with the following small change to compile-expr, whereby we avoid generating temps for constant expressions, we can at least get some nicer-looking output:
(defun compile-expr (rct expr)
(cond
((member expr rct.temps) expr)
((constantp expr) expr)
(t (let ((temp (allocate-temp rct)))
(push ^(,temp ,expr) rct.lets)
temp))))
1> (rpn-compile '(3 4 + 5 6 + *))
(let* ((#:g0266 (+ 4 3))
(#:g0267 (+ 6 5))
(#:g0268 (* #:g0267 #:g0266)))
#:g0268)I find the syntax ok, and you do get used to the snake_case.
I've used the "Native Debug" extension with VS: Code in the past.
I've heard that if you have a CLion license, it (with the Rust plugin) is pretty solid at debugging, but I don't own one of those.
What do you mean by "Native Debug" extension for VSCode? The normal Rust plugin built on top of Racer often shows Racer as crashed, and doesn't underline errors while typing. It also doesn't really seem to know the type of a variable most of the time. I'm writing in too many languages at once. Autocompletion usually gives me enough of a hint that I remember what to type.
VSCode being my favorite editor, so I'm glad that I don't need to install another IDE right now.
EDIT: I just checked it out, and apparently you can install the Rust plugin with IntelliJ Community Edition. Which is free.
I meant this extension: https://marketplace.visualstudio.com/items?itemName=webfreak...
> The normal Rust plugin
Which one are you talking about? There are three of them: one is abandoned.
> you can install the Rust plugin with IntelliJ Community Edition. Which is free.
My understanding is that you can install it, but it does not include debugging support, that's what you have to pay for.
>it does not include debugging support
That's a bit sad, but I haven't had debugging support in VSCode until now. I'd pick good code completion and error hints over debugging. Both would be nice though.
How do you configure that Native Debug plugin for Rust?
> How do you configure that Native Debug plugin for Rust?
exactly the same way that it says on the page; choose gdb or lldb, change target to point to the executable, and you're good to go.
o/
Stuff like this:
fn map<T, U, F>(vector: &[T], function: F) -> Vec<U>
where F: for<'a> Fn(&'a T) -> U {
...
It's not too hard to read once you're in the ecosystem, but there's a lot of non-alphanumerics in there, and you know each one of them is super important to getting the thing working.Why not something more akin to ML?
fn map<T, U, 'a>(vector: &[T], function: &'a T -> U) -> Vec<U>So I don't really know lisp macros but I guess it's still closer than to C.
If you want a short introduction to what Rust's macros actually are then try this: https://danielkeep.github.io/practical-intro-to-macros.html#...
Honu is a language with Javascript-like syntax, but has support for syntax-case style macros.
by comparison to gp's article Facotr macros are easy because instead of knowing exactly where and what the arguments are in Factor the reader doesn't have to care.
Homoiconic means that procedures are stored in representation in which they are written, or more or less.
A Lisp that compiles everything entered at the REPL isn't homoiconic. The input looks like (lambda (...) ...), but the storage is some block of x86 machine code with an environment vector.
POSIX shells are homoiconic. When you type set, you see your functions, in source code form (modulo reformatting).
The thing you want for metaprogramming is a smart internal representation of program text (not same as the external one).
[0]: https://doc.rust-lang.org/proc_macro/index.html [1]: https://github.com/dtolnay/syn [2]: https://github.com/dtolnay/quote