Skip to main content

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}