Add contracts to several methods, force_send

main
Shad Amethyst 2 years ago
parent f3d224e26a
commit 489cdd1750
Signed by: amethyst
GPG Key ID: D970C8DD1D6DEE36

@ -6,9 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dyn-clone = "1.0"
# dyn-clone = "1.0"
palette = "0.6"
enum_dispatch = "0.3"
contracts = { version = "0.6.3", features = ["override_debug"] }
[dev-dependencies]
colored = "2.0"

@ -1,6 +1,8 @@
use super::*;
use std::ptr::NonNull;
// TODO: write VecCell, to make miri happy
/** 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.
@ -14,11 +16,12 @@ use std::ptr::NonNull;
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 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
- Most methods that modify the state of a tile will instead store the modified state in a temporary buffer
and apply the modifications after every tile was updated. The only exception to this is [`UpdateContext::keep`].
# Example
@ -73,7 +76,23 @@ pub struct UpdateContext<'a> {
// 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
/// Returns `None` if the tile was already updated, is empty or does not exist.
#[ensures(
old(pane.get(position).is_none()) -> ret.is_none(),
"Should return None if the tile does not exist"
)]
#[ensures(
old(pane.get(position).is_some() && pane.get(position).unwrap().updated) -> ret.is_none(),
"Should return None if the tile was already updated"
)]
#[ensures(
old(pane.get(position).is_some() && pane.get(position).unwrap().get().is_none()) -> ret.is_none(),
"Should return None if the tile is empty"
)]
#[ensures(
ret.is_some() -> ret.as_ref().unwrap().0.commit.updates.iter().find(|&&x| x == position).is_some(),
"Should add an entry in self.commit.updates if result is Some"
)]
pub(crate) fn new(
pane: &'a mut Pane,
position: (usize, usize),
@ -124,7 +143,25 @@ impl<'a> UpdateContext<'a> {
self.position
}
/// Returns the [signal](crate::FullTile::signal) of the currently updated tile.
/// Returns the [`width`](Pane::width) of the current [`Pane`].
#[inline]
pub fn width(&self) -> NonZeroUsize {
unsafe {
// SAFETY: we only read self.pane.width
self.pane.as_ref().width()
}
}
/// Returns the [`height`](Pane::height) of the current [`Pane`].
#[inline]
pub fn height(&self) -> NonZeroUsize {
unsafe {
// SAFETY: we only read self.pane.height
self.pane.as_ref().height()
}
}
/// Returns a reference to the [signal](crate::FullTile::signal) of the currently updated tile.
#[inline]
pub fn signal<'b>(&'b self) -> Option<&'b Signal>
where
@ -160,6 +197,7 @@ impl<'a> UpdateContext<'a> {
/// }
/// ```
#[inline]
#[ensures(self.signal.is_none(), "Should leave the signal to None")]
pub fn take_signal(&mut self) -> Option<Signal> {
std::mem::take(&mut self.signal)
}
@ -176,8 +214,14 @@ impl<'a> UpdateContext<'a> {
///
/// The actions of this function will only be executed *after* all the tiles of the [`Pane`] were [`updated`](Pane::step).
#[inline]
#[ensures(
self.commit.states.iter().find(|(x, y, _)| self.position == (*x, *y)).is_some(),
"Should add an entry in self.commit.states"
)]
#[ensures(self.state == state)]
pub fn set_state(&mut self, state: State) {
self.commit.set_state(self.position, state);
self.state = state;
self.commit.set_state(self.position, self.state);
}
/// Sets the state of the current tile to `state.next()`
@ -186,13 +230,23 @@ impl<'a> UpdateContext<'a> {
///
/// The actions of this function will only be executed *after* all the tiles of the [`Pane`] were [`updated`](Pane::step).
#[inline]
#[ensures(
self.commit.states.iter().find(|(x, y, _)| self.position == (*x, *y)).is_some(),
"Should add an entry in self.commit.states"
)]
#[ensures(self.state == old(self.state).next())]
pub fn next_state(&mut self) {
self.commit.set_state(self.position, self.state.next());
self.state = self.state.next();
self.commit.set_state(self.position, self.state);
}
/// Returns an immutable reference to the [FullTile] at `pos` in the current [Pane].
/// Returns `None` if the tile is borrowed mutably, if it is the current tile or if it does not exist.
#[inline]
#[ensures(
ret.is_some() -> ret.unwrap().get().is_some() ->
std::ptr::addr_of!(*ret.unwrap().get().unwrap()) != self.current_ptr()
)]
pub fn get<'b>(&'b self, pos: (usize, usize)) -> Option<&'b FullTile>
where
'a: 'b,
@ -218,10 +272,11 @@ impl<'a> UpdateContext<'a> {
/// Returns `true` iff `(x, y)` is within the bounds of the current pane.
#[inline]
pub fn in_bounds(&self, pos: (usize, usize)) -> bool {
#[ensures(ret == true -> position.0 < self.width().get() && position.1 < self.height().get())]
pub fn in_bounds(&self, position: (usize, usize)) -> bool {
unsafe {
// SAFETY: Pane::in_bounds does not read `self.pane.cells`
self.pane.as_ref().in_bounds(pos)
self.pane.as_ref().in_bounds(position)
}
}
@ -238,6 +293,7 @@ impl<'a> UpdateContext<'a> {
/// Returns whether or not the tile at `pos` accepts a signal coming from `direction`.
/// If the tile does not exist, then this function will return `false`.
#[inline]
#[ensures(ret == true -> self.get(pos).is_some() && self.get(pos).unwrap().get().is_some())]
pub fn accepts_signal(&self, pos: (usize, usize), direction: Direction) -> bool {
match self.get(pos) {
Some(tile) => tile.accepts_signal(direction),
@ -276,9 +332,10 @@ impl<'a> UpdateContext<'a> {
}
}
// TODO: return Result
/// 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.
/// Returns Some(()) if the signal was stored in a cell, None 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`.
///
@ -286,18 +343,48 @@ impl<'a> UpdateContext<'a> {
///
/// 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.in_bounds(pos) {
#[ensures(
!self.in_bounds(position) -> ret.is_none(),
"Should return None if position is out of bounds"
)]
#[ensures(
ret.is_some() -> self.commit.signals.iter().find(|(x, y, _)| position == (*x, *y)).is_some(),
"Should add an entry in self.commit.signals if result is Some"
)]
pub fn force_send(&mut self, position: (usize, usize), mut signal: Signal) -> Option<()> {
signal.set_position(position);
if !self.in_bounds(position) {
return None;
}
self.commit.send(pos, signal);
self.commit.send(position, signal);
Some(())
}
/// Sends a signal to `position` if there is a tile at `position` that will accept our signal
///
/// # 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.
#[ensures(
!self.in_bounds(position) -> ret.is_none(),
"Should return None if position is out of bounds"
)]
#[ensures(
ret.is_some() -> self.commit.signals.iter().find(|(x, y, _)| position == (*x, *y)).is_some(),
"Should add an entry in self.commit.signals if result is Some"
)]
pub fn send(&mut self, position: (usize, usize), signal: Signal) -> Option<()> {
if self.accepts_signal(position, signal.direction()) {
self.force_send(position, signal)
} else {
None
}
}
/// 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.
///
@ -308,6 +395,8 @@ impl<'a> UpdateContext<'a> {
/// 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`.
///
/// If `take_signal` or `keep` are called before this functions, then it will do nothing.
///
/// # Example
///
/// ```
@ -324,7 +413,12 @@ impl<'a> UpdateContext<'a> {
/// }
/// }
/// ```
#[ensures(self.signal.is_none())]
pub fn keep(&mut self) {
if self.signal.is_none() {
return;
}
unsafe {
// SAFETY: we only access self.pane[self.position].signal, not self.pane[self.position].cell
self.pane
@ -334,6 +428,14 @@ impl<'a> UpdateContext<'a> {
.set_signal(std::mem::take(&mut self.signal));
}
}
/// Returns a pointer to self.pane[self.position].cell
fn current_ptr(&self) -> *const AnyTile {
let reference = unsafe {
self.pane.as_ref().get(self.position).unwrap()
};
&*reference.get().unwrap()
}
}
/// Temporarily holds a list of actions to be made on a given Pane, which should be [applied](UpdateCommit::apply)

@ -7,6 +7,9 @@ This library is the rust implementation of the core logic of the language.
*/
#[macro_use]
extern crate contracts;
use std::num::NonZeroUsize;
pub mod signal;

@ -32,6 +32,8 @@ impl Pane {
/// // Perform a simulation step
/// pane.step();
/// ```
#[ensures(ret.is_some() -> width > 0)]
#[ensures(ret.is_some() -> height > 0)]
pub fn empty(width: usize, height: usize) -> Option<Self> {
// TODO: check that width * height is a valid usize
let length = width.checked_mul(height)?;
@ -45,6 +47,36 @@ impl Pane {
})
}
/// Returns the width of the current pane
///
/// ## Example
///
/// ```
/// # use stackline::prelude::*;
/// let pane = Pane::empty(4, 7).unwrap();
///
/// assert_eq!(pane.width().get(), 4);
/// ```
#[inline]
pub fn width(&self) -> NonZeroUsize {
self.width
}
/// Returns the height of the current pane
///
/// ## Example
///
/// ```
/// # use stackline::prelude::*;
/// let pane = Pane::empty(4, 7).unwrap();
///
/// assert_eq!(pane.height().get(), 7);
/// ```
#[inline]
pub fn height(&self) -> NonZeroUsize {
self.height
}
/// Given a `position = (x, y)` and an `offset = (Δx, Δy)`,
/// returns `Some((x + Δx, y + Δy))` if `(x + Δx, y + Δy)` is inside the `Pane`.
///
@ -64,6 +96,10 @@ impl Pane {
/// assert_eq!(pane.offset((1, 0), (3, 0)), None); // (1 + 3, 0 + 0) = (4, 0), outside
/// ```
#[inline]
#[ensures(ret.is_some() -> position.0 as isize + offset.0 as isize >= 0)]
#[ensures(ret.is_some() -> position.1 as isize + offset.1 as isize >= 0)]
#[ensures(ret.is_some() -> position.0 as isize + (offset.0 as isize) < self.width.get() as isize)]
#[ensures(ret.is_some() -> position.1 as isize + (offset.1 as isize) < self.height.get() as isize)]
pub fn offset(&self, position: (usize, usize), offset: (i8, i8)) -> Option<(usize, usize)> {
if offset.0 < 0 && (-offset.0) as usize > position.0
|| offset.1 < 0 && (-offset.1) as usize > position.1
@ -101,6 +137,7 @@ impl Pane {
/// let tile = pane.get((0, 0)).unwrap();
/// ```
#[inline]
#[ensures(self.in_bounds(position) -> ret.is_some())]
pub fn get<'b>(&'b self, position: (usize, usize)) -> Option<&'b FullTile> {
if !self.in_bounds(position) {
return None;
@ -125,6 +162,7 @@ impl Pane {
/// tile.set_state(State::Active);
/// ```
#[inline]
#[ensures(old(self.in_bounds(position)) -> ret.is_some())]
pub fn get_mut<'b>(&'b mut self, position: (usize, usize)) -> Option<&'b mut FullTile> {
if !self.in_bounds(position) {
return None;
@ -136,6 +174,7 @@ impl Pane {
/// Sets the tile at `position` to `tile`. `T` must either implement [`Tile`] or be `()`.
#[inline]
#[ensures(self.in_bounds(position) -> ret.is_some())]
pub fn set_tile<T>(&mut self, position: (usize, usize), tile: T) -> Option<()>
where
FullTile: From<T>,
@ -170,6 +209,7 @@ impl Pane {
/// assert_eq!(pane.get_state((0, 0)), Some(State::Dormant));
/// ```
#[inline]
#[ensures(self.in_bounds(position) -> ret.is_some())]
pub fn get_state(&self, position: (usize, usize)) -> Option<State> {
self.get(position).map(|x| x.state().clone())
}
@ -200,6 +240,8 @@ impl Pane {
/// assert!(pane.get((0, 0)).unwrap().signal().is_some());
/// ```
#[inline]
#[ensures(ret.is_some() -> self.in_bounds(position) && self.get(position).unwrap().get().is_some())]
#[ensures(!self.in_bounds(position) -> ret.is_none())]
pub fn set_signal(&mut self, position: (usize, usize), mut signal: Signal) -> Option<()> {
signal.set_position(position);
if let Some(tile) = self.get_mut(position) {
@ -231,6 +273,8 @@ impl Pane {
}
#[inline]
#[ensures(self.in_bounds(position) -> self.get(position).unwrap().updated)]
#[ensures(!self.in_bounds(position) -> ret.is_none())]
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)?;

@ -0,0 +1,59 @@
use super::*;
#[macro_export]
macro_rules! test_tile_setup {
( $width:expr, $height:expr, [ $( $x:expr ),* ] ) => {{
assert!($width > 0);
assert!($height > 0);
let mut pane = crate::pane::Pane::empty($width, $height).unwrap();
let mut index = 0;
$(
{
let x = index % $width;
let y = index / $width;
*pane.get_mut((x, y)).unwrap() = crate::tile::FullTile::from($x);
index += 1;
}
)*
assert!(index == $width * $height);
pane
}}
}
#[macro_export]
macro_rules! test_set_signal {
( $pane:expr, $pos:expr, $dir:expr ) => {
$pane.set_signal($pos, crate::signal::Signal::empty($pos, $dir)).unwrap();
};
}
#[macro_export]
macro_rules! assert_signal {
( $pane:expr, $pos:expr ) => {{
let guard = $pane
.get($pos)
.expect(&format!("Couldn't get tile at {:?}", $pos));
let signal = guard.signal();
assert!(signal.is_some());
signal
}};
( $pane:expr, $pos:expr, [ $( $data:expr ),* ] ) => {{
let signal = assert_signal!($pane, $pos);
// TODO: check that signal.data == data
}};
}
#[macro_export]
macro_rules! assert_no_signal {
( $pane:expr, $pos:expr) => {{
let guard = $pane
.get($pos)
.expect(&format!("Couldn't get tile at {:?}", $pos));
let signal = guard.signal();
assert!(signal.is_none());
}};
}

@ -1,9 +1,18 @@
/*! This module contains the [`Tile`] trait, which defines common behavior for all tiles in stackline.
* It also contains [`FullTile`], which holds the state and signal of the different tiles alongside a [`Tile`] wrapped in [`AnyTile`].
*
* Lastly, [`AnyTile`] is an enum containing all of the [`Tile`] instances that were discovered in the `tiles/` directory.
* See [its documentation](AnyTile) for more information on the discovery process.
*/
use super::*;
use dyn_clone::{clone_box, DynClone};
mod full;
pub use full::*;
#[cfg(test)]
#[macro_use]
mod macros;
// Generated rust file containing the AnyTile enum, which combines the structs implementing Tile found in tiles/.
// Its definition looks like this:
// #[derive(Clone, Debug)]
@ -17,8 +26,113 @@ pub use full::*;
// Note that all the implementing types will be read from the files in tiles/ and re-exported.
include!(concat!(env!("OUT_DIR"), "/anytile.rs"));
/// The `Tile` trait defines shared behavior for every tile in the language.
///
/// Tiles are the building block of the language, and take the role of the instructions of the language.
/// With this trait, you may define the following behaviors of instructions:
/// - what it should do if it receives a [`Signal`], with [`Tile::update`]
/// - when [`Signal`s](Signal) can be sent to it, with [`Tile::accepts_signal`]
/// - how it should be displayed on the screen, with [`Tile::draw`]
///
/// # Example
///
/// Let's start by implementing a basic [`Tile`], which simply forwards any incomming [`Signal`] to its right.
/// Create a file in the `tiles/` folder containing the following
///
/// ```ignore
/// // First, import the needed types. Because we are writing files
/// // that will be part of the "stackline" crate, we have to import them using `crate`:
/// use crate::prelude::*;
/// use crate::tile::prelude::*;
///
/// // Tiles must implement Clone and Debug
/// #[derive(Clone, Debug)]
/// pub struct MyTile {
/// // This is where your tile can store its internal state.
/// // For this tile, we don't need any!
/// }
///
/// impl MyTile {
/// // It's a good idea to provide a constructor for writing tests
/// pub fn new() -> Self {
/// MyTile {}
/// }
/// }
///
/// impl Tile for MyTile {
/// // The update method is where we will put the logic of our simple tile
/// fn update<'b>(&'b mut self, mut context: UpdateContext<'b>) {
/// // Check if we have a signal
/// if let Some(signal) = context.take_signal() {
/// // We do have a signal, so we will forward it to the tile on our right.
///
/// // First, get the coordinates of the tile to our right:
/// if let Some(right_position) = context.offset((1, 0)) {
/// // Then, send the signal! We need to tell that we are moving the signal to the right.
/// context.send(right_position, signal.moved(Direction::Right));
/// }
/// }
///
/// // If we are Active, become Dormant
/// // If we are Dormant, become Idle
/// if context.state() != State::Idle {
/// context.next_state();
/// }
/// }
///
/// // The Tile trait provides a default implementations for the other methods,
/// // which satisfy our needs.
/// }
///
/// // Lastly, we should write unit tests for our code
/// #[cfg(test)]
/// mod test {
/// use super::*;
///
/// #[test]
/// fn test_my_tile() {
/// use crate::tile::Wire;
/// use crate::utils::Orientation::*;
///
/// // We create a Pane containing our tile, surrounded by wires:
/// // |
/// // -X-
/// // |
/// // With X our tile
/// let mut pane = test_tile_setup!(3, 3, [
/// (), Wire::new(Vertical), (),
/// Wire::new(Horizontal), MyTile::new(), Wire::new(Horizontal),
/// (), Wire::new(Vertical), ()
/// ]);
///
/// // Send signals coming from the top, bottom and left of our tile,
/// // And check that they are all forwarded to the right
/// let signals = [
/// (0, 1, Direction::Right),
/// (1, 0, Direction::Down),
/// (1, 2, Direction::Up),
/// ];
///
/// for (x, y, dir) in signals {
/// test_set_signal!(pane, (x, y), dir);
/// pane.step();
///
/// // Our tile should accept the signal
/// assert_signal!(pane, (1, 1));
///
/// pane.step();
/// // Our tile should have moved the signal to the right
/// assert_no_signal!(pane, (1, 1));
/// assert_signal!(pane, (2, 1));
///
/// pane.step();
/// pane.step();
/// }
/// }
/// }
/// ```
#[enum_dispatch(AnyTile)]
pub trait Tile: DynClone + std::fmt::Debug {
pub trait Tile: std::clone::Clone + std::fmt::Debug {
/// Function to be called when the tile needs to be updated.
#[inline]
fn update<'b>(&'b mut self, mut context: UpdateContext<'b>) {
@ -80,63 +194,9 @@ pub trait Tile: DynClone + std::fmt::Debug {
// }
// }
#[cfg(test)]
mod crate_macros {
#[macro_export]
macro_rules! test_tile_setup {
( $width:expr, $height:expr, [ $( $x:expr ),* ] ) => {{
assert!($width > 0);
assert!($height > 0);
let mut pane = Pane::empty($width, $height).unwrap();
let mut index = 0;
$(
{
let x = index % $width;
let y = index / $width;
*pane.get_mut((x, y)).unwrap() = FullTile::from($x);
index += 1;
}
)*
assert!(index == $width * $height);
pane
}}
}
#[macro_export]
macro_rules! test_set_signal {
( $pane:expr, $pos:expr, $dir:expr ) => {
$pane.set_signal($pos, Signal::empty($pos, $dir)).unwrap();
};
}
#[macro_export]
macro_rules! assert_signal {
( $pane:expr, $pos:expr ) => {{
let guard = $pane
.get($pos)
.expect(&format!("Couldn't get tile at {:?}", $pos));
let signal = guard.signal();
assert!(signal.is_some());
signal
}};
( $pane:expr, $pos:expr, [ $( $data:expr ),* ] ) => {{
let signal = assert_signal!($pane, $pos);
// TODO: check that signal.data == data
}};
}
#[macro_export]
macro_rules! assert_no_signal {
( $pane:expr, $pos:expr) => {{
let guard = $pane
.get($pos)
.expect(&format!("Couldn't get tile at {:?}", $pos));
let signal = guard.signal();
assert!(signal.is_none());
}};
}
pub mod prelude {
pub use crate::tile::{FullTile, AnyTile};
pub use crate::signal::Signal;
pub use crate::utils::State;
pub use crate::text::*;
}

@ -1,6 +1,8 @@
//! Wires and diodes
use super::*;
// use super::*;
use crate::prelude::*;
use crate::tile::prelude::*;
#[derive(Clone, Debug)]
pub struct Wire(Orientation);
@ -20,7 +22,7 @@ impl Tile for Wire {
}
if let Some(pos) = context.accepts_direction(direction) {
context.send(pos, signal.clone_move(direction));
context.force_send(pos, signal.clone_move(direction));
}
}
}
@ -62,7 +64,7 @@ impl Tile for Diode {
return;
}
if let Some(pos) = context.accepts_direction(self.0) {
if let Some(pos) = context.offset(self.0.into_offset()) {
context.send(pos, signal.moved(self.0));
}
}
@ -102,7 +104,7 @@ impl Resistor {
impl Tile for Resistor {
fn update<'b>(&'b mut self, mut context: UpdateContext<'b>) {
if let Some(signal) = std::mem::take(&mut self.signal) {
if let Some(pos) = context.accepts_direction(self.direction) {
if let Some(pos) = context.offset(self.direction.into_offset()) {
context.send(pos, signal.moved(self.direction));
}
}

Loading…
Cancel
Save