📝 UpdateContext::keep and document UpdateContext

main
Shad Amethyst 2 years ago
parent e303f15d5a
commit 33946a4c18
Signed by: amethyst
GPG Key ID: D970C8DD1D6DEE36

@ -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<Pane>,
state: State,
signal: Option<Signal>,
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<Signal> {
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)

@ -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();

Loading…
Cancel
Save