I'm searching for hacks that will let me emulate typestates in some macro-generated code. Since the only typestate information Rust keeps track of is whether a value is dropped or borrowed I figure it might be possible to leverage that somehow to encode more useful information for controlling type-checking.
To that end, are there any scenarios in which whether a value has been dropped can effect the meaning of code?
Borrowchecking does not actually influence the generated code — it can only reject invalid code. See e.g. mrustc which is a reimplementation that doesn't do borrowchecking, but is still able to compile rust code.
As for dropping, it impacts code in the sense that dropped values run a destructor, and that borrowchecking ensure that you don't use it afterwards.
Also, borrow checking happens at a later stage of compilation than type checking, and it acts on the Mid-level Intermediate Representation (MIR), where all types are already made fully explicit, so there isn't a way for the borrow checker to influence the type checking or inference.
If you want to do typestate in Rust, it's probably easiest to encode the types yourself directly, rather than trying to get the compiler into doing it for you. This should be doable from within a procedural macro at least. We might be able to help more if you explain what it is you want to do with typestates.
is not merely an implementation detail, but baked into the basic rules of borrow checking: you can’t do borrow checking (at least, not the “use of moved value” part) correctly without knowing which types implement Copy, since that determines whether it’s legal to use a moved-from value.
You also need types to do autoref, autoderef, deref coercions, &mut->& coercions, etc correctly, all of which affect what the borrow checker is supposed to be checking.
The only technique I’m aware of that resembles typestate at all is having some types representing states and functions which consume one and return another. I agree that we need to hear more about your use case to offer any better help.
My use case is that I'm trying to implement a crate for session-typed generators. For example, with my hypothetical crate you could define a generator which is guaranteed to yield a u32, then a string, then exit:
The problem is expanding the session! macro such that the Protocol can be inferred and that the body of the session is guaranteed to follow that protocol.
One way to do it would be to have every invocation of send! take and return an object representing the current state of the session, and return that object at the end. eg:
let my_session: impl Session<Protocol = Send<u32, Send<&'static str>, Finish>>
= session!(|token| {
let token = send!(token, 123u32);
let token = send!(token, "wow");
token
)}
However this gets pretty noisy so I was hoping there's some clever way to avoid it. Since I'm implementing both the session! and send! macros I have a lot of leeway in how to do that, assuming that it's at all possible.
Another option would be for the session! macro implementation to search the session body for any instances of send! and thread through all the usages and re-bindings of token itself. But that would be fragile since it wouldn't work reliably if there are other macro invocations in the body which implement their own control-flow or which expand to a usage of send!. I would have to either ban usages of third-party macros inside the session body entirely (which would be very restrictive) or somehow expand those sub-macros while expanding the session! macro (which doesn't seem possible).
So what I really need here is typestate. But as pointed out, thanks to Copy, typechecking has to happen before liveness checking. And that kills all the half-baked ideas I had for MacGyvering typestate out of dropped-ness
I don't suppose anyone can see another way to do this?
I feel like I'm missing something huge because my first thought is... can't you literally just do exactly this? Except hide the |token|, let token = s and token under the macros?
You can't because send! might be called inside a block. eg.
session! {
{
send!(23);
}
send!("wow");
}
would expand to:
move || {
let token = ... ;
{
let token = send_impl!(token, 23);
}
// This uses the wrong `token`
let token = send_impl!(token, "wow");
}
The macro would have to fold over all the blocks, loops, etc. looking for invocations of send! and insert the code to propagate token outwards. But there's no way it can look inside of macros that might expand to a send!, so it can't be made to work reliably. It could work if there was a way for proc macros to manually invoke sub-macros though.