5 unstable releases
Uses old Rust 2015
0.3.1 | Jan 20, 2017 |
---|---|
0.3.0 | Aug 13, 2016 |
0.2.0 | May 21, 2016 |
0.1.1 | Feb 23, 2016 |
0.1.0 | Feb 23, 2016 |
#988 in Rust patterns
23KB
262 lines
What it is
This is a small Rust crate that provides a new control-flow primitive by means of a horrible macro.
RFC 243 (the ?
/catch
RFC) proposed a feature called "early exit from any block". It generalizes break
to take an expression, as well as a lifetime, and to work inside all {}
blocks, not just loops. break LIFE EXPR
breaks out of the block/loop identified by the lifetime, and returns the given expression from the loop. Of course the expression must have the same type as the value that the block normally returns when it ends.
We can specify the desired feature by a source-to-source transformation (I doubt this is how it would be done if the feature were added to the language, but it does show that no new language features or control-flow primitives are truly required):
Input:
let x = 'a: {
break 'a 0; // *
1 // **
};
Output (asterisks show corresponding lines):
let x = {
let ret;
'a: loop {
ret = {
ret = 0; // *
break 'a; // *
1 // **
};
break 'a;
}
ret
};
Well, that was tedious to write (and read). Let's not do that again.
#[macro_use] extern crate named_block;
#[macro_use] extern crate static_cond; // not needed on nightly
let x = block!('a: {
break 'a 0;
1
});
Fixed!
How to use it
First, add "named-block" as a dependency in Cargo.toml
. Then, add #[macro_use] extern crate named_block;
at the top of your crate root.
If you are on nightly Rust, you can enable the "nightly" Cargo feature and skip the second step. Otherwise, you need to add "static-cond" in Cargo.toml
and #[macro_use] extern crate static_cond;
as well. (Check this crate's Cargo.toml
to see which version of "static-cond" to use.)
How it works
The block!
macro uses (a lot of) recursion to walk through your code and perform the source-to-source translation described above. The ret
variable is gensymmed using hygiene and cannot collide with other variable names or even nested calls to block!
. Neat macro tricks include using a "parsing stack" to descend into token trees, and generating new macros on the fly to do comparisons. See the commented macro source for more details.
Limitations
-
The macro recurses. A lot. This means it will slow down compilation proportional to the length of the code in the block. You may need to increase the recursion limit (stick
#![recursion_limit = "1000"]
at the crate root, playing with the number as necessary). -
break LIFE EXPR
will be transformed nearly anywhere it appears.-
Even if it's within the call to another macro, like
block!('a: { foo!(break 'a 42) })
. In principle,foo!
could be intending to transform the syntax in some other way, andblock!
will screw it up. But it seems more likely that you do want the code in macro calls to be transformed. -
Even if it's inside a closure. This is the one that could cause problems, in rare cases. If (a) you have a closure inside a
block!
call, and (b) there is ablock!
call inside the closure, and (c) the block labels are the same... then you will get some screwy error messages and/or behavior. -
The macro is smart enough to ignore items. So blocks within local
fn
s,impl
s, etc are safe. This should speed up parsing a bit too -- as soon as the macro sees e.g. the keywordimpl
it can skip an entire item without copying over every token or descending into token trees. -
For closures, strange macros or other undiscovered bugs in the macro, there is a special escape hatch in the form of an attribute. Any token tree annotated with
#[block(ignore)]
will be ignored by the macro (this does not require#![feature(stmt_expr_attributes)]
because the attribute is parsed by the macro itself).Example:
block!('a: { enum Foo { Bar(i32) } let closure = #[block(ignore)] { move |Foo::Bar(x): Foo| -> i32 { x + block!('a: { break 'a 41; }) } }; closure(Foo::Bar(1)) });
This block evaluates to
42
.
-
-
Bare
break
/continue
statements (lacking a specific lifetime) are not allowed withinblock!
calls. This is because the macro expansion itself generates a hidden loop, so the results of these statements will be confusing and unintended (type errors, infinite loops, etc). For the same reason, you can'tcontinue 'a
where'a
is the label given toblock!
. The macro will catch all of these cases during expansion and produce a compile error.