I know most of you are aware of this, but I'll post it nonetheless for reference, especially since it's an implementation which already offers a possible answer to most of the questions regarding panic and whatnot.
With "branding type constructors" (type constructors which manage to yield an instance of a unique type per instantiation), one can use the type-state pattern to ensure that a non-diverging input has properly called the desired destructor (or constructor! This can be used to structurally prove that an out-pointer to a structure is initialized by initializing each and every field).
The root point of a brand, in Rust, is an "anonymous" / unnameable lifetime, either because macro-generated, or because higher-order (callback style):
mod lib {
type PhantomInvariant<'lt> =
::core::marker::PhantomData<fn(&'lt ()) -> &'lt ()>
;
pub
struct Id<'brand> /* = */ (
PhantomInvariant<'brand>,
);
pub
struct Foo<'brand> {
// …
_brand: Id<'brand>,
}
impl<'brand> Foo<'brand> {
pub
fn consume (self: Foo<'brand>)
-> ProofOfConsumption<'brand>
{
// …
ProofOfConsumption(self._brand)
}
}
// where
pub
struct ProofOfConsumption<'brand>(Id<'brand>);
/// Here is where the magic happens: "must consume"-yielding constructor!
impl Foo<'_> {
pub(self) // private
fn new (/* … */)
-> Self
{
Self {
// …
_brand: Id(<_>::default()),
}
}
pub
fn with_new<R> (
/* …, */
scope: impl for<'brand> FnOnce(Foo<'brand>) -> (ProofOfConsumption<'brand>, R),
) -> R
{
scope(Self::new(/* … */)).1
}
}
}
The key is thus that
for</* any */ 'brand> …(Foo<'brand>) -> ProofOfConsumption<'brand> …
required callback: it needs to be a closure whose function body is lifetime-agnostic (otherwise it wouldn't meet the desired higher-order signature), and while doing so, it needs, for each / any input 'brand
in Foo
, to be able to yield a ProofOfConsumption<'brand>
.
This will only be possible if the logic of the given scope / callback, for every possible branch inside it, either .consume()
s Foo()
, or diverges.
Then, one could always write a Drop
impl of Foo<'_>
, which would only be called in the panic!
-king case: impl Drop for Foo<'_>
and impl Foo<'_> { fn consume(self)
would thus be the two function bodies handling both cases, with consume
being able to fail or whatnot, but the Drop
not (and one could even panic!
within the Drop
impl to abort if the scope
panics, should that consume
call be otherwise mandatory).
This does have the issue of requiring callbacks, hence not playing super nicely with try
blocks, async
, or early break
, continue
, or return
s.
This also answers the question of structural combination of such entities, or even that of storing those in a collection: the unique lifetime for each makes the latter impossible (), and the former possible but quite cumbersome:
struct Baz<'foo, 'bar>(Foo<'foo>, Bar<'bar>);
impl Baz {
fn with_new<R> (
scope: impl for<'f, 'b> FnOnce(Baz<'f, 'b>) -> (ConsumedBaz<'f, 'b>, R),
) -> R
{
Foo::with_new(|foo| {
Bar::with_new(|bar| {
let baz = Baz(foo, bar);
let (baz_token, ret) = scope(baz);
let (foo_token, bar_token) = baz_token.into_parts();
(bar_token, (foo_token, ret))
})
})
}
}