gb_bank/model/warp.rs
1//! [`Warp`]: a deferred banked call, the [`Future`] analog of this crate.
2//!
3//! A `#[bank]` function does not run when you name it with arguments; like an
4//! `async fn`, calling it just *captures* the work and hands back a value
5//! implementing [`Warp`]. [`Warp::drive`] is the `.await`: it performs the bank
6//! switch, runs the function, and restores the caller's bank.
7//!
8//! The concrete value returned is a [`BankedWarp`] (the analog of an `async fn`'s
9//! anonymous state machine), but callers only ever see `impl Warp<Output = R>`.
10
11use core::marker::{PhantomData, Tuple};
12
13use super::{Bank, DynFar, Far, Group, GroupZero, switch_run, switch_run_far};
14
15pub use gb_bank_safe::BankSafe;
16
17// Far pointers are the sanctioned way to carry a banked function out of its bank:
18// calling one switches banks first, so a `Far` / `DynFar` is `BankSafe` regardless of
19// what it points at. (This also stops the auto-derivation from looking through the
20// inner `*const T` at a banked `fn`.)
21unsafe impl<T: ?Sized, G: Group> BankSafe for Far<T, G> {}
22unsafe impl<T: ?Sized> BankSafe for DynFar<T> {}
23
24/// A deferred banked call: a function applied to its arguments, not yet run.
25///
26/// This is the [`Future`] of the banking world. A `#[bank]` function returns
27/// `impl Warp<Output = R>`; [`drive`](Warp::drive) is the `.await` that drives it,
28/// performing the bank switch, running the function, and restoring the caller. The
29/// concrete types are built by the macros and never named.
30///
31/// A `#[bank::zero]` function returns one too, and that one switches nothing: its
32/// body is in bank 0 and always reachable, so it threads the caller's token
33/// straight through, and any banked call inside switches `C` to its target and back
34/// to `C`. That is what makes a bank-0 helper safe to call from *any* bank, not
35/// only from bank 0.
36///
37/// The `#[must_use]` sits on the trait rather than on the concrete types because a
38/// banked function returns `impl Warp`, and that lint reads the trait.
39#[must_use = "a Warp does nothing until `.drive()`"]
40pub trait Warp {
41 /// The value the call produces. It must be [`BankSafe`]: a banked call's result
42 /// crosses the switch back to the caller, so it cannot embed a pointer to the
43 /// callee's (now unmapped) banked code.
44 type Output: BankSafe;
45
46 /// The bank group this call targets (where its function lives).
47 type Group: Group;
48
49 /// Run the call in its own bank, given a token already there: the same-bank
50 /// "near call". No switch happens, `here` proves the bank is mapped.
51 ///
52 /// This is the primitive; [`drive`](Warp::drive) is `near` wrapped in a
53 /// [`scope`](crate::scope). Because it takes a `Bank<Self::Group>`, it pins the token's
54 /// group: a run of `near` calls share one [`scope`](crate::scope), and `scope` over them
55 /// infers the token's group with no annotation. (For data, see
56 /// [`local`](crate::Far::local).)
57 fn near(self, here: &mut Bank<Self::Group>) -> Self::Output;
58
59 /// Run the call from any bank, leaving the caller's bank `C` as it was: the
60 /// cross-bank counterpart of [`near`](Warp::near). A banked call switches into its
61 /// bank and back (elided when `C` is already the target, or the target is
62 /// [`GroupZero`]); a bank-0 helper instead forwards `C` with no switch.
63 ///
64 /// A resident caller inlines the switch, which lets the `Warp` collapse into a
65 /// plain call. A banked caller cannot: inlined switch code would unmap itself, so
66 /// it goes through a bank-0 trampoline instead.
67 #[inline]
68 fn drive<C: Group>(self, outer: &mut Bank<C>) -> Self::Output
69 where
70 Self: Sized,
71 {
72 let run = |b: &mut Bank<Self::Group>| self.near(b);
73 if C::FIXED {
74 switch_run(outer, run)
75 } else {
76 switch_run_far(outer, run)
77 }
78 }
79}
80
81/// The concrete deferred call: a function pointer with its arguments captured.
82///
83/// The analog of the anonymous state machine an `async fn` returns. You normally
84/// see it only as `impl Warp`; the `#[bank]` macro constructs it.
85///
86/// Like a [`Future`], it is inert if dropped without being run (`#[must_use]`).
87#[must_use = "a Warp does nothing until `.drive()`"]
88pub struct BankedWarp<F, G, Args> {
89 f: F,
90 args: Args,
91 _g: PhantomData<G>,
92}
93
94impl<F, G, Args> BankedWarp<F, G, Args> {
95 /// Capture a function and its arguments. Emitted by the `#[bank]` macro.
96 ///
97 /// # Safety
98 ///
99 /// `f` must be a function living in group `G`'s bank, so that the switch
100 /// performed by [`Warp::drive`] maps the bank its code actually resides in.
101 #[doc(hidden)]
102 #[inline(always)]
103 pub const unsafe fn new(f: F, args: Args) -> Self {
104 BankedWarp { f, args, _g: PhantomData }
105 }
106}
107
108impl<F: Fn<Args>, G: Group, Args: Tuple + BankSafe> Warp for BankedWarp<F, G, Args>
109where
110 F::Output: BankSafe,
111{
112 type Output = F::Output;
113 type Group = G;
114 #[inline]
115 fn near(self, _here: &mut Bank<G>) -> F::Output {
116 // `_here` proves G is mapped, so there is no switch to make.
117 self.f.call(self.args)
118 }
119}
120
121
122/// The body of a bank-0 (`#[bank::zero]`) function, generic over the *caller's*
123/// bank group `C`.
124///
125/// A bank-0 function lives in the always-mapped region, so reaching it needs no
126/// bank switch. But when it drives banked calls of its own, each one must switch
127/// back to whatever bank the *caller* was in, otherwise the caller resumes with
128/// the wrong bank mapped. So the body has to be parametric over the caller's
129/// group `C`, threading a [`Bank<C>`](Bank) token through its `.drive()`s.
130///
131/// A function pointer cannot be generic over a *type* (`for<C: Group> fn(..)` is
132/// not expressible), so the body is encoded as this trait instead, implemented on
133/// a per-function marker type by the `#[bank::zero]` macro. The generic lives on
134/// the [`run`](FixedFn::run) *method*, which is allowed. [`FixedWarp`] is the
135/// [`Warp`] that invokes it.
136pub trait FixedFn<Args: BankSafe> {
137 /// The value the call produces. [`BankSafe`] for the same reason as
138 /// [`Warp::Output`]: a bank-0 helper's result is handed back to the caller.
139 type Output: BankSafe;
140
141 /// Run the body in the caller's bank `C`, threading its `bank` token so that
142 /// any banked call inside restores `C` on the way out.
143 fn run<C: Group>(args: Args, bank: &mut Bank<C>) -> Self::Output;
144}
145
146/// The concrete [`Warp`] a bank-0 (`#[bank::zero]`) function returns.
147///
148/// Where a [`BankedWarp`] switches into a *fixed* group `G`, a `FixedWarp` performs
149/// no switch at all: its body is in bank 0 and always reachable, so it just
150/// threads the caller's token straight through. Any banked call inside the body
151/// then switches `C -> target -> C` and leaves the caller's bank exactly as it
152/// found it. That is what makes a bank-0 helper safe to call from *any* bank,
153/// not only from bank 0.
154///
155/// `M` is a per-function marker carrying the body via its [`FixedFn`] impl; the
156/// macro builds it. Callers only ever see `impl Warp<Output = R>` and drive it
157/// with `helper(args).drive()`. Like any [`Warp`], it is inert until run
158/// (`#[must_use]`).
159#[must_use = "a Warp does nothing until `.drive()`"]
160pub struct FixedWarp<M, Args> {
161 args: Args,
162 _m: PhantomData<M>,
163}
164
165impl<M, Args> FixedWarp<M, Args> {
166 /// Capture the arguments. Emitted by the `#[bank::zero]` macro.
167 ///
168 /// Safe: the [`Warp`] impl below requires `M: FixedFn<Args>`, so a marker and
169 /// its argument tuple can never be mismatched.
170 #[doc(hidden)]
171 #[inline(always)]
172 pub const fn new(args: Args) -> Self {
173 FixedWarp { args, _m: PhantomData }
174 }
175}
176
177impl<M: FixedFn<Args>, Args: BankSafe> Warp for FixedWarp<M, Args> {
178 type Output = M::Output;
179 // A bank-0 helper is caller-generic; anchor it at bank 0 so the
180 // `Warp` contract is satisfied. Resident helpers are normally run with `drive`.
181 type Group = GroupZero;
182 #[inline]
183 fn near(self, here: &mut Bank<GroupZero>) -> M::Output {
184 M::run(self.args, here)
185 }
186 // Override the default `drive`: a bank-0 helper forwards the *actual* caller
187 // `C` unchanged (so its inner calls restore `C`) rather than scoping to
188 // GroupZero. Resident code is always mapped, so there is no switch to make.
189 #[inline]
190 fn drive<C: Group>(self, outer: &mut Bank<C>) -> M::Output {
191 M::run(self.args, outer)
192 }
193}