As I mentioned in the last
part,
Stateful
has some challenges it needs to overcome in order to add new and
exciting control flow mechanisms to Rust. While we don’t get access to any of
the cool analysis passes inside the Rust compiler, Stateful
is able to sneak
around their necessity in many cases since it really only needs to support a
subset of Rust. Here are some of the techniques it exploits, err, uses.
Variables
First off, let’s talk about variables. One of the primary things Stateful
needs to do is manage the process of state flowing through the machine. However,
consider a statement like this:
1
|
|
“Obviously it’s a variable, right?” Actually you can’t be sure. What if someone did:
1 2 3 4 |
|
Well then the compiler would helpfully report:
1 2 |
|
But that warning only works for simple let
statements. Consider what happens
with matches. Consider:
1 2 3 4 |
|
Is x
or y
a variable, or a variant? There’s no way to know unless you
perform name resolution, otherwise known as the resolve pass in the compiler.
Unfortunately though, there’s no way for Stateful
to run that analysis. As
Sméagol said, “There is another way. More secret, and dark way.”. This leads
us to Cheat Number One: Stateful assumes that all lowercase identifiers are
variables, and uppercase ones are enum variants. Sure, Rust supports
lowercase variants, but there’s no reason why Stateful
has to use them. It
makes our lives much easier.
Types
The next problem is typing. Sure, Rust is nice and all that you can write a
local variable like let x = ...
and it’ll infer the type for you. All Rust
asks for is that the user explicitly specify the type of a value that enters or
leaves the bounds of a function. Our problem is that one of the main tasks of
Stateful
is to lift variables into some State
structure so that their
available when the function is re-entered. So in effect, all variables inside
Stateful
must be typed. Consider the example from last week:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
This State
enumeration is what I’m talking about. It gets passed into and
out of the advance
function. It needs to be some concrete type, which looks
something like this:
1 2 3 4 5 6 7 8 9 |
|
The problem is that we want to write code like this:
1 2 3 4 5 6 7 8 |
|
So how can we resolve this? Well first, we could wait for RFC 105 or RFC 1305 to get implemented, but that’s not happening any time soon. Until then, there is cheat number two: Hide state variables in a boxed trait. This one is from Eduard Burtescu. Instead of the nice well typed example from the last post, we actually generate some code that hides the types with an over abundance of generic types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
|
All for the cost of a boxed variable. It’s not ideal, but it does let us keep experimenting. However, if we do want to avoid this allocation, we can just require that all variables that survive across a yield point have their type specified. So our previous example would be written as:
1 2 3 4 5 6 7 8 |
|
It’s not so bad here, but it’d get obnoxious if we had a generator like:
1 2 3 4 5 6 7 8 |
|
The type of iter
, by the way, is impossible to write because there is
currently no way to specify the type of the closure. Instead, it needs to be
rewritten to use a free function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
If we want to support closures though, we need to use the Box<Iterator<...>
trick.
References
This one’s a doozy. Here’s an example of the problem. Consider:
1 2 3 4 5 6 7 |
|
This would look something like this (which also demonstrates how match
statements are represented):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Zero in on this block:
1 2 3 4 5 6 7 8 9 |
|
The type of opt
is Option<&'a mut usize>
, and value
is &'a mut usize
.
So we’ve got two outstanding mutable borrows, which is illegal. The real
problem is that Stateful
without Resolve and the Borrow Checker pass, it
cannot know if a use of the variable is a copy or move in all cases. So we now
have cheat number 3: Use pseudo-macros to hint to Stateful if a type is
copyable or movable. This is the same technique we use to implement the
pseudo-macro yield_!(...)
, where we would add move_!(...)
and copy_!(...)
to inform Stateful
when something has been, well, moved or copied. Our
previous example would then be written as:
1 2 3 4 5 6 7 |
|
Which would then give Stateful
enough information to generate something like
this, which would then know that the match consumed the option:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
I’m also considering some default rules, that can be overridden with these macros:
- If a value is known to be copyable (it’s a primitive type, or it’s a
&T
type), then it’s always copied. All other types are assumed to not be copyable. - Non-copyable types are moved when passed into a function argument, unless
wrapped in a
copy_!(...)
hint. - Non-copyable type method calls are by reference, unless explicitly wrapped in
a
move_!(...)
hint. - Non-copyable types are moved in
match
statement, unless one of the match arms usesref
orref mut
.
Hopefully this will enable a vast majority of code to work without
copy_!(...)
or move_!(...)
.
Conclusion
Those are our major cheats! I’m sure there will be plenty more in the future. In the meantime, I want to show off some some actual working code! Check this puppy out!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Produces:
1 2 3 |
|
Isn’t it beautiful? We got generics, mutable variables, loops, matches, breaks, and a whole host of ignored warnings!