From 33946a4c181c8aa066c47ed873f234c94a0f5aa2 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Fri, 24 Jun 2022 00:41:43 +0200 Subject: [PATCH] :sparkles: :pencil: UpdateContext::keep and document UpdateContext --- stackline/src/context.rs | 161 ++++++++++++++++++++++++++++++++++----- stackline/src/pane.rs | 53 ++++++++++++- 2 files changed, 192 insertions(+), 22 deletions(-) diff --git a/stackline/src/context.rs b/stackline/src/context.rs index e371653..22f2daa 100644 --- a/stackline/src/context.rs +++ b/stackline/src/context.rs @@ -1,23 +1,32 @@ use super::*; +use std::ptr::NonNull; /** Provides an interface between a [`Tile`] and its parent [`Pane`] during [`Tile::update`]. All actions performed through `UpdateContext` will be executed *after* all the tiles have updated. - ## Design + # Design - There are several factors that came into the design of [`UpdateContext`]: + There are several factors that came into the design of `UpdateContext`: - - all of its methods are considered hot-path code, which means that allocations must be kept at a minimum - - all of the actions must be performed after all the tiles were updated - - we need mutable access to the current tile, so that it can update its internal state + - All of its methods are considered hot-path code, which means that allocations must be kept at a minimum + - All of the actions must be performed after all the tiles were updated + - We need mutable access to the current tile, so that it can update its internal state - ## Example + As a result, there are a few oddities to take note of: + + - If a [`Signal`] was in the updated tile, then it will be moved into the `UpdateContext`. + If you wish to put the signal back into its tile, + then you will need to call [`keep`](UpdateContext::keep) or [`send`](UpdateContext::send). + See [`take_signal`](UpdateContext::take_signal) for more information. + - Most methods that + + # Example Here is how you would implement a simple "counter" tile: ``` # use stackline::{*, tile::*, context::*}; - + # #[derive(Clone, Debug)] pub struct CounterTile(usize); @@ -33,7 +42,8 @@ use super::*; // Update the internal state self.0 += 1; - // Send the signal along: first, get the offset (Δx, Δy) associated with its direction and the tile at (x+Δx,y+Δy) + // Send the signal along: first, get the offset (Δx, Δy) associated with its direction and the tile at (x+Δx,y+Δy). + // Note that the next three lines can be shortened to `ctx.accepts_direction(signal.direction())` if let Some((pos, tile)) = ctx.get_offset(signal.direction().into_offset()) { // Then, check that `tile` accepts signals if tile.accepts_signal(signal.direction()) { @@ -47,20 +57,20 @@ use super::*; ``` - ## Safety + # Safety Because [`Tile::update`] requires a `&mut self` reference, the current [`Tile`] cannot be accessed through [`UpdateContext::get`]. This structure stores the [`State`] and [`Signal`] of the [`FullTile`] containing the current tile, so these can be accessed nonetheless, and it is still possible and safe to call [`UpdateContext::send`] on the current position. **/ pub struct UpdateContext<'a> { position: (usize, usize), - pane: &'a Pane, + pane: NonNull, state: State, signal: Option, commit: &'a mut UpdateCommit, } -// SAFETY: self.pane.tiles[self.position] may not be accessed by any method of UpdateContext +// SAFETY: self.pane.tiles[self.position].cell may not be accessed by any method of UpdateContext impl<'a> UpdateContext<'a> { /// Creates a new UpdateContext /// Returns `None` if the tile was already updated or is empty @@ -82,7 +92,9 @@ impl<'a> UpdateContext<'a> { position, state: tile.state(), signal: tile.take_signal(), - pane, + pane: unsafe { + NonNull::new_unchecked(&mut *pane) + }, commit, }; @@ -93,7 +105,7 @@ impl<'a> UpdateContext<'a> { /// Returns the position of the currently updated tile. /// - /// ## Example + /// # Example /// /// ``` /// # use stackline::prelude::*; @@ -123,7 +135,32 @@ impl<'a> UpdateContext<'a> { self.signal.as_ref() } - /// Performs [`std::mem::take`] on the signal of the currently updated tile. + /// Performs [`std::mem::take`] on the [signal](crate::FullTile::signal) of the currently updated tile. + /// + /// # Note + /// + /// Even if this function is not called, the current tile will still be stripped from its signal whenever it is updated. + /// + /// If you do want to keep the signal where it is, then you must either + /// [`send`](UpdateContext::send) it to the current tile + /// (which will only take effect at the end of the update phase), + /// or [`keep`](UpdateContext::keep) it (which will take effect immediately but cannot be called together with `take_signal`). + /// + /// # Example + /// + /// ``` + /// # use stackline::prelude::*; + /// #[derive(Clone, Debug)] + /// pub struct PrintTile; + /// + /// impl Tile for PrintTile { + /// fn update<'b>(&'b mut self, mut ctx: UpdateContext<'b>) { + /// if let Some(mut signal) = ctx.take_signal() { + /// println!("{:?}", signal.pop()); + /// } + /// } + /// } + /// ``` #[inline] pub fn take_signal(&mut self) -> Option { std::mem::take(&mut self.signal) @@ -136,12 +173,20 @@ impl<'a> UpdateContext<'a> { } /// Sets the state of the current tile to `state`. + /// + /// # Note + /// + /// The actions of this function will only be executed *after* all the tiles of the [`Pane`] were [`updated`](Pane::step). #[inline] pub fn set_state(&mut self, state: State) { self.commit.set_state(self.position, state); } /// Sets the state of the current tile to `state.next()` + /// + /// # Note + /// + /// The actions of this function will only be executed *after* all the tiles of the [`Pane`] were [`updated`](Pane::step). #[inline] pub fn next_state(&mut self) { self.commit.set_state(self.position, self.state.next()); @@ -157,14 +202,29 @@ impl<'a> UpdateContext<'a> { if self.position == pos { None } else { - self.pane.get(pos) + unsafe { + // SAFETY: pos != self.position, thus self.pane[self.position].cell cannot be accessed + self.pane.as_ref().get(pos) + } } } - /// Returns `Some((position.x + Δx, position.y + Δy))` iff `(x + Δx, y + Δy)` is inside the pane + /// Returns `Some((position.x + Δx, position.y + Δy))` iff `(x + Δx, y + Δy)` is inside the current pane. #[inline] pub fn offset(&self, offset: (i8, i8)) -> Option<(usize, usize)> { - self.pane.offset(self.position, offset) + unsafe { + // SAFETY: Pane::offset does not read `self.pane.cells` + self.pane.as_ref().offset(self.position, offset) + } + } + + /// Returns `true` iff `(x, y)` is within the bounds of the current pane. + #[inline] + pub fn in_bounds(&self, pos: (usize, usize)) -> bool { + unsafe { + // SAFETY: Pane::in_bounds does not read `self.pane.cells` + self.pane.as_ref().in_bounds(pos) + } } /// Shortcut for calling both `ctx.offset(offset)` and `ctx.get(pos)` @@ -187,7 +247,27 @@ impl<'a> UpdateContext<'a> { } } - /// Returns `Some(pos)` iff `pos = (x + Δx, y + Δy)` is a valid position and `self.get(pos).accepts_signal(direction)` + /// Returns `Some(pos)` iff `pos = (x + Δx, y + Δy)` is a valid position and `ctx.get(pos).accepts_signal(direction)`. + /// + /// This can be used as a shortcut to [`ctx.get_offset(direction.into_offset())`](UpdateContext::get_offset) + /// paired with [`ctx.accepts_signal(new_pos, direction)`](UpdateContext::accepts_signal). + /// + /// # Example + /// + /// ``` + /// # use stackline::prelude::*; + /// # #[derive(Clone, Debug)] + /// # pub struct MyTile; + /// # impl Tile for MyTile { + /// fn update<'b>(&'b mut self, mut ctx: UpdateContext<'b>) { + /// if let Some(signal) = ctx.take_signal() { + /// if let Some(pos) = ctx.accepts_direction(Direction::Down) { + /// ctx.send(pos, signal); + /// } + /// } + /// } + /// # } + /// ``` #[inline] pub fn accepts_direction(&self, direction: Direction) -> Option<(usize, usize)> { let (pos, tile) = self.get_offset(direction.into_offset())?; @@ -198,14 +278,20 @@ impl<'a> UpdateContext<'a> { } } - /// Sends a signal to be stored in a cell (may be the current one), the signal overrides that of the other cell + /// Sends a signal to be stored in a cell (may be the current one), overriding any signal that was in that cell. + /// /// Returns true if the signal was stored in a cell, false otherwise. /// The target cell's state will be set to `Active` if it received the signal. /// The signal's `position` will be set to `pos`. + /// + /// # Note + /// + /// The actions of this function will only be executed *after* all the tiles of the [`Pane`] were [`updated`](Pane::step). + /// See [`keep`](UpdateContext::keep) for a variant of this method that takes effect immediately. pub fn send(&mut self, pos: (usize, usize), mut signal: Signal) -> Option<()> { signal.set_position(pos); - if !self.pane.in_bounds(pos) { + if !self.in_bounds(pos) { return None; } @@ -213,6 +299,41 @@ impl<'a> UpdateContext<'a> { Some(()) } + + /// Stores the current signal back in the current tile, guaranteeing that it will stay there for + /// this update cycle. See [`take_signal`](UpdateContext::take_signal) for more information. + /// + /// This method differs from [`send`](UpdateContext::send), as it takes action immediately. + /// The signal may also not be modified, as it would otherwise break the guarantees of [`Pane::step`]. + /// + /// This function will [`std::mem::take`] the signal stored in `UpdateContext`, similar to [`take_signal`](UpdateContext::take_signal). + /// If you wish to modify or send copies of the signal, then you will need to call [`signal`](UpdateContext::signal) beforehand and make + /// clones of the signal before calling `keep`. + /// + /// # Example + /// + /// ``` + /// # use stackline::prelude::*; + /// #[derive(Clone, Debug)] + /// pub struct StorageTile {}; + /// + /// impl Tile for StorageTile { + /// fn update<'b>(&'b mut self, mut ctx: UpdateContext<'b>) { + /// if ctx.signal().is_some() { + /// ctx.keep(); + /// } + /// // If we weren't to do this, then the signal would get dropped here + /// } + /// } + /// ``` + pub fn keep(&mut self) { + unsafe { + // SAFETY: we only access self.pane[self.position].signal, not self.pane[self.position].cell + self.pane.as_mut().get_mut(self.position).unwrap_or_else(|| unreachable!()).set_signal( + std::mem::take(&mut self.signal) + ); + } + } } /// Temporarily holds a list of actions to be made on a given Pane, which should be [applied](UpdateCommit::apply) diff --git a/stackline/src/pane.rs b/stackline/src/pane.rs index 1b2fa62..b08fc47 100644 --- a/stackline/src/pane.rs +++ b/stackline/src/pane.rs @@ -57,7 +57,7 @@ impl Pane { /// Returns a mutable reference to the [`Tile`] at `position`. /// - /// ## Example + /// # Example /// /// ``` /// use stackline::prelude::*; @@ -123,6 +123,7 @@ impl Pane { #[inline] fn update(&mut self, position: (usize, usize), commit: &mut UpdateCommit) -> Option<()> { + // NOTE: Tiles will only be updated once as per UpdateContext::new let (ctx, tile) = UpdateContext::new(self, position, commit)?; tile.update(ctx); @@ -130,7 +131,55 @@ impl Pane { Some(()) } - // TODO: document + /// Performs an update cycle. + /// Such an update cycle roughly consists of the following: + /// + /// - Calls [`Tile::update`] on every tile with a signal + /// - Calls [`Tile::update`] on every active tile (tiles will only be updated once) + /// - Applies all signal [`send`s](UpdateContext::send) + /// - Applies all [state changes](UpdateContext::set_state) + /// + /// # Guarantees + /// + /// To prevent unwanted behavior, the following properties are upheld by this method and the methods of [`UpdateContext`]: + /// + /// - Order-agnostism: [updating](Tile::update) a tile `A` before a tile `B` will result in the same state as if `B` was updated before `A`. + /// - As a consequence, when a tile `A` is updated, it cannot modify the state of any other tile. + /// - Any [`Signal`] or [`State`] update will only be carried out after every tile was updated. + /// The only exception to this rule is [`UpdateContext::keep`]. + /// + /// # Examples + /// + /// ``` + /// use stackline::prelude::*; + /// use stackline::tile::Diode; + /// + /// let mut pane = Pane::empty(2, 2).unwrap(); + /// + /// pane.set_tile((0, 0), Diode::new(Direction::Right)); + /// pane.set_tile((1, 0), Diode::new(Direction::Down)); + /// pane.set_tile((1, 1), Diode::new(Direction::Left)); + /// pane.set_tile((0, 1), Diode::new(Direction::Up)); + /// + /// println!("{:?}", pane); + /// // >v + /// // ^< + /// + /// // Initialize the circuit with a signal at (0, 0) + /// pane.set_signal((0, 0), stackline::signal!((0, 0), Direction::Right)); + /// + /// // Do an update step + /// pane.step(); + /// + /// // The signal has now been moved to (1, 0) by the Diode + /// assert!(pane.get((1, 0)).unwrap().signal().is_some()); + /// + /// // Do another update step + /// pane.step(); + /// + /// // The signal is now at (1, 1) + /// assert!(pane.get((1, 1)).unwrap().signal().is_some()); + /// ``` pub fn step(&mut self) { let mut commit = UpdateCommit::new();