From 8654d25c65913c82b30e3d158ea4ebec4a34f945 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Sun, 14 Aug 2022 15:15:41 +0200 Subject: [PATCH] :sparkles: Improve build.rs, basic stack ops, methods for World --- stackline/build.rs | 37 +++---- stackline/src/lib.rs | 1 + stackline/src/signal.rs | 28 ++++- stackline/src/world.rs | 41 ++++++++ stackline/tests/common/mod.rs | 65 ++++++++++-- stackline/tests/storage.rs | 39 +++++++ stackline/tests/storage/counter.json | 1 + stackline/tests/storage/reader.json | 1 + stackline/tiles/arithmetic.rs | 147 +++++++++++++++++++++++++++ stackline/tiles/stack.rs | 6 +- stackline/tiles/storage.rs | 103 +++++++++++++++++++ stackline/tiles/wire.rs | 8 +- 12 files changed, 441 insertions(+), 36 deletions(-) create mode 100644 stackline/tests/storage.rs create mode 100644 stackline/tests/storage/counter.json create mode 100644 stackline/tests/storage/reader.json create mode 100644 stackline/tiles/arithmetic.rs create mode 100644 stackline/tiles/storage.rs diff --git a/stackline/build.rs b/stackline/build.rs index 6a5f9b9..de1688d 100644 --- a/stackline/build.rs +++ b/stackline/build.rs @@ -33,33 +33,28 @@ fn main() { .unwrap_or_else(|err| panic!("Couldn't read {:?}: {}", src_path, err)); let mut local_names: Vec = Vec::new(); - // TODO: don't throw an error when a parsing error occured; - // Instead, include the file so that rustc can give a helpful error - let syntax = syn::parse_file(&contents) - .unwrap_or_else(|err| panic!("Unable to parse file {:?}: {}", src_path, err)); - - for item in syntax.items.iter() { - #[allow(clippy::single_match)] - // I'd like to keep the match for future-proofing, for instance if I need to match for a macro call - match item { - Item::Impl(item) => { - if let Some(name) = parse_impl_tile(item) { - local_names.push(name); + match syn::parse_file(&contents) { + Ok(syntax) => { + for item in syntax.items.iter() { + if let Item::Impl(item) = item { + if let Some(name) = parse_impl_tile(item) { + local_names.push(name); + } } } - _ => {} + } + Err(err) => { + eprintln!("Unable to parse file {:?}: {}", src_path, err); } } - if !local_names.is_empty() { - let canonical = fs::canonicalize(src_path.clone()).unwrap_or_else(|err| { - panic!("Couldn't canonicalize {}: {}", src_path.display(), err) - }); - for name in local_names.iter() { - names.push(name.clone()); - } - files.push((canonical, local_names)); + let canonical = fs::canonicalize(src_path.clone()).unwrap_or_else(|err| { + panic!("Couldn't canonicalize {}: {}", src_path.display(), err) + }); + for name in local_names.iter() { + names.push(name.clone()); } + files.push((canonical, local_names)); } } diff --git a/stackline/src/lib.rs b/stackline/src/lib.rs index 01d5ea7..ad67e86 100644 --- a/stackline/src/lib.rs +++ b/stackline/src/lib.rs @@ -9,6 +9,7 @@ This library is the rust implementation of the core logic of the language. #![feature(div_duration)] #![feature(drain_filter)] +#![feature(try_blocks)] use std::num::NonZeroUsize; diff --git a/stackline/src/signal.rs b/stackline/src/signal.rs index 53e350a..1325eb4 100644 --- a/stackline/src/signal.rs +++ b/stackline/src/signal.rs @@ -28,8 +28,8 @@ impl From for Value { } } -impl From for Value { - fn from(x: u32) -> Value { +impl From for Value { + fn from(x: i32) -> Value { Value::Number(x.into()) } } @@ -55,6 +55,24 @@ impl std::fmt::Display for Value { } } +impl std::ops::Add for Value { + type Output = Self; + + fn add(self, other: Self) -> Self { + use Value::*; + use std::fmt::Write; + + match (self, other) { + (Number(x), Number(y)) => Number(x + y), + (String(mut x), y) => { + write!(&mut x, "{}", y).unwrap(); + String(x) + } + (x, String(y)) => String(format!("{}{}", x, y)), + } + } +} + /// The unit of information that [`Tile`]s transmit between each other. /// A `Signal` is made up of a [`stack`](Signal::stack), and tracks its [`position`](Signal::position) and [`direction`](Signal::direction). /// @@ -191,6 +209,12 @@ impl Signal { } } +impl PartialEq for Signal { + fn eq(&self, other: &Self) -> bool { + self.stack == other.stack + } +} + /// Creates a signal with initial values in its stack. /// /// The syntax for the macro is `signal!(position, direction, [value1, value2, ...])`, where: diff --git a/stackline/src/world.rs b/stackline/src/world.rs index d434534..5afd0d1 100644 --- a/stackline/src/world.rs +++ b/stackline/src/world.rs @@ -57,6 +57,8 @@ impl World { self.panes.insert(name, pane); } + // TODO: get_pane_at, get_pane_at_mut + pub fn get(&self, (x, y): (i32, i32)) -> Option> { for pane in self.panes.values() { let x2 = x - pane.position().0; @@ -95,6 +97,45 @@ impl World { None } + pub fn get_as(&self, (x, y): (i32, i32)) -> Option> + where + for<'c> &'c AnyTile: TryInto<&'c T>, + { + for pane in self.panes.values() { + let x2 = x - pane.position().0; + let y2 = y - pane.position().1; + if x2 >= 0 + && x2 < pane.width().get() as i32 + && y2 >= 0 + && y2 < pane.height().get() as i32 + { + let x2 = x2 as usize; + let y2 = y2 as usize; + if let Some(tile) = pane.get_as::((x2, y2)) { + return Some(tile); + } + } + } + None + } + + pub fn set_signal(&mut self, (x, y): (i32, i32), signal: Signal) { + for pane in self.panes.values_mut() { + let x2 = x - pane.position().0; + let y2 = y - pane.position().1; + if x2 >= 0 + && x2 < pane.width().get() as i32 + && y2 >= 0 + && y2 < pane.height().get() as i32 + { + let x2 = x2 as usize; + let y2 = y2 as usize; + pane.set_signal((x2, y2), signal); + break + } + } + } + pub fn get_mut(&mut self, (x, y): (i32, i32)) -> Option<&mut FullTile> { for pane in self.panes.values_mut() { let x2 = x - pane.position().0; diff --git a/stackline/tests/common/mod.rs b/stackline/tests/common/mod.rs index 9898e33..0f98ad0 100644 --- a/stackline/tests/common/mod.rs +++ b/stackline/tests/common/mod.rs @@ -29,8 +29,9 @@ macro_rules! assert_signal { ( $world:expr, $x:expr, $y:expr ) => {{ let guard = $world .get(($x, $y)) - .expect(&format!("Couldn't get tile at {}:{}", $x, $y)); + .unwrap_or_else(|| panic!("Couldn't get tile at {}:{}", $x, $y)); let signal = guard.signal(); + assert!( signal.is_some(), "Expected signal at {}:{}!\n{}", @@ -38,12 +39,13 @@ macro_rules! assert_signal { $y, $world ); - signal + + signal.unwrap() }}; - ( $world:expr, $x:expr, $y:expr, [ $( $data:expr ),* ] ) => {{ - let signal = assert_signal!($pane, $x, $y); - // TODO: check that signal.data == data + ( $world:expr, $x:expr, $y:expr, $signal:expr ) => {{ + let signal = assert_signal!($world, $x, $y); + assert_eq!(signal, $signal); }}; } @@ -52,8 +54,9 @@ macro_rules! assert_no_signal { ( $world:expr, $x:expr, $y:expr ) => {{ let guard = $world .get(($x, $y)) - .expect(&format!("Couldn't get tile at {}:{}", $x, $y)); + .unwrap_or_else(|| panic!("Couldn't get tile at {}:{}", $x, $y)); let signal = guard.signal(); + assert!( signal.is_none(), "Expected no signal at {}:{}!\n{}", @@ -64,6 +67,56 @@ macro_rules! assert_no_signal { }}; } +#[macro_export] +macro_rules! assert_stored { + ( $world:expr, $x:expr, $y:expr ) => {{ + let guard = $world + .get_as::(($x, $y)) + .unwrap_or_else(|| panic!("Couldn't get store tile at {}:{}", $x, $y)); + let signal = guard.signal().cloned(); + + assert!( + signal.is_some(), + "Expected stored signal at {}:{}!\n{}", + $x, + $y, + $world + ); + + signal.unwrap() + }}; + + ( $world:expr, $x:expr, $y:expr, $signal:expr ) => {{ + let signal = assert_stored!($world, $x, $y); + assert_eq!( + signal, + $signal, + concat!("Expected stored signal at {}:{} to be equal to ", stringify!($signal), "!\n{}"), + $x, + $y, + $world + ); + }}; +} + +#[macro_export] +macro_rules! assert_no_stored { + ( $world:expr, $x:expr, $y:expr ) => {{ + let guard = $world + .get_as::(($x, $y)) + .unwrap_or_else(|| panic!("Couldn't get store tile at {}:{}", $x, $y)); + let signal = guard.signal(); + + assert!( + signal.is_none(), + "Expected no stored signal at {}:{}!\n{}", + $x, + $y, + $world + ); + }}; +} + #[macro_export] macro_rules! assert_display_eq { ( $world:expr, $path:expr ) => {{ diff --git a/stackline/tests/storage.rs b/stackline/tests/storage.rs new file mode 100644 index 0000000..c5e5199 --- /dev/null +++ b/stackline/tests/storage.rs @@ -0,0 +1,39 @@ +#[allow(unused_imports)] +use stackline::prelude::*; +use stackline::signal; +mod common; + +#[test] +fn test_reader() { + let mut world = load_test!("tests/storage/reader.json"); + world.init(); + + world.set_signal((0, 0), signal!(Direction::Down, [0])); + + run!(world, 5); + assert_stored!(world, 0, 1, signal!([0])); + assert_no_stored!(world, 2, 0); + + world.set_signal((1, 2), signal!(Direction::Up, [-1])); + run!(world, 5); + assert_stored!(world, 2, 0, signal!([0])); + + world.set_signal((0, 0), signal!(Direction::Down, [1])); + + run!(world, 5); + assert_stored!(world, 0, 1, signal!([1])); + assert_stored!(world, 2, 0, signal!([0])); +} + +#[test] +fn test_counter() { + let mut world = load_test!("tests/storage/counter.json"); + world.init(); + + for n in 1..10 { + world.set_signal((1, 3), signal!(Direction::Up)); + println!("{}", world); + run!(world, 11); + assert_stored!(world, 0, 2, signal!([n])); + } +} diff --git a/stackline/tests/storage/counter.json b/stackline/tests/storage/counter.json new file mode 100644 index 0000000..33c6a8b --- /dev/null +++ b/stackline/tests/storage/counter.json @@ -0,0 +1 @@ +{"panes":{"main":{"tiles":[{"cell":{"Diode":"Down"},"signal":null,"state":"Idle","updated":false},{"cell":{"Add":null},"signal":null,"state":"Idle","updated":false},{"cell":{"Diode":"Left"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":{"Wire":"Vertical"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":{"Push":{"value":{"Number":1.0}}},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":{"Store":{"signal":{"direction":"Up","stack":[{"Number":0.0}]}}},"signal":null,"state":"Idle","updated":false},{"cell":{"Reader":"Right"},"signal":null,"state":"Idle","updated":false},{"cell":{"Diode":"Up"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":{"Diode":"Up"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false}],"width":4,"height":4,"position":[0,0]}},"blink_speed":{"secs":0,"nanos":250000000}} \ No newline at end of file diff --git a/stackline/tests/storage/reader.json b/stackline/tests/storage/reader.json new file mode 100644 index 0000000..d59efb0 --- /dev/null +++ b/stackline/tests/storage/reader.json @@ -0,0 +1 @@ +{"panes":{"main":{"tiles":[{"cell":{"Diode":"Down"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":{"Store":{"signal":null}},"signal":null,"state":"Idle","updated":false},{"cell":{"Store":{"signal":null}},"signal":null,"state":"Idle","updated":false},{"cell":{"Reader":"Right"},"signal":null,"state":"Idle","updated":false},{"cell":{"Diode":"Up"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false},{"cell":{"Diode":"Up"},"signal":null,"state":"Idle","updated":false},{"cell":null,"signal":null,"state":"Idle","updated":false}],"width":3,"height":3,"position":[0,0]}},"blink_speed":{"secs":0,"nanos":250000000}} \ No newline at end of file diff --git a/stackline/tiles/arithmetic.rs b/stackline/tiles/arithmetic.rs new file mode 100644 index 0000000..c87181d --- /dev/null +++ b/stackline/tiles/arithmetic.rs @@ -0,0 +1,147 @@ +//! Arithmetic operations: add, subtract, etc. + +use crate::prelude::*; +use crate::tile::prelude::*; + +macro_rules! binary_op { + () => { + fn update<'b>(&'b mut self, mut context: UpdateContext<'b>) { + if let Some(mut signal) = context.take_signal() { + if signal.len() >= 2 { + let first = signal.pop().unwrap_or_else(|| unreachable!()); + let second = signal.pop().unwrap_or_else(|| unreachable!()); + + if let Some(res) = self.binary_op(second, first) { + signal.push(res); + } + } + + if let Some(coords) = context.offset(signal.direction().into_offset()) { + context.send(coords, signal.direction(), signal).unwrap_or_else(|_| unreachable!()); + } + } + + if context.state() != State::Idle { + context.next_state(); + } + } + } +} + +/// Adds two values together: `[..., a, b] -> [..., a+b]` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Add; + +impl Add { + fn binary_op(&self, left: Value, right: Value) -> Option { + Some(left + right) + } +} + +impl Tile for Add { + binary_op!(); + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + TextChar::from_state('\u{2295}', ctx.state) // CIRCLED PLUS + } +} + +/// Subtracts two values: `[..., a, b] -> [..., a-b]` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Sub; + +impl Sub { + fn binary_op(&self, left: Value, right: Value) -> Option { + use Value::*; + + match (left, right) { + (Number(x), Number(y)) => Some(Number(x - y)), + (String(mut x), Number(y)) => { + x.truncate(y as usize); + Some(String(x)) + } + (String(x), String(y)) => { + Some(String(x.split(&y).collect())) + } + _ => None + } + } +} + +impl Tile for Sub { + binary_op!(); + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + TextChar::from_state('\u{2296}', ctx.state) // CIRCLED MINUS + } +} + +/// Multiplies two values together: `[..., a, b] -> [..., a*b]` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Mul; + +impl Mul { + fn binary_op(&self, left: Value, right: Value) -> Option { + use Value::*; + + match (left, right) { + (Number(x), Number(y)) => Some(Number(x * y)), + (String(x), Number(y)) => Some(String(x.repeat(y as usize))), + _ => None + } + } +} + +impl Tile for Mul { + binary_op!(); + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + TextChar::from_state('\u{2297}', ctx.state) // CIRCLED TIMES + } +} + +/// Divides two values together: `[..., a, b] -> [..., a/b]` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Div; + +impl Div { + fn binary_op(&self, left: Value, right: Value) -> Option { + use Value::*; + + match (left, right) { + (Number(x), Number(y)) => Some(Number(x / y)), + _ => None + } + } +} + +impl Tile for Div { + binary_op!(); + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + TextChar::from_state('\u{2298}', ctx.state) // CIRCLED DIVISION SLASH + } +} + +/// Computes the modulo of two values: `[..., a, b] -> [..., a%b]` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Mod; + +impl Mod { + fn binary_op(&self, left: Value, right: Value) -> Option { + use Value::*; + + match (left, right) { + (Number(x), Number(y)) => Some(Number(x % y)), + _ => None + } + } +} + +impl Tile for Mod { + binary_op!(); + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + TextChar::from_state('\u{2299}', ctx.state) // CIRCLED DOT OPERATOR + } +} diff --git a/stackline/tiles/stack.rs b/stackline/tiles/stack.rs index 2b5814e..df8cc56 100644 --- a/stackline/tiles/stack.rs +++ b/stackline/tiles/stack.rs @@ -39,7 +39,7 @@ impl Tile for Push { } fn draw_simple(&self, ctx: DrawContext) -> TextChar { - TextChar::from_state('p', ctx.state) + TextChar::from_state('P', ctx.state) } } @@ -82,9 +82,9 @@ impl Tile for Pop { fn draw_simple(&self, ctx: DrawContext) -> TextChar { if self.amount == 1 { - TextChar::from_state('P', ctx.state) + TextChar::from_state('\u{2c63}', ctx.state) // Latin Capital Letter P with Stroke } else { - TextChar::from_state(ctx.blink.scroll(&format!("PPP{}", self.amount)), ctx.state) + TextChar::from_state(ctx.blink.scroll(&format!("\u{2c63}\u{2c63}\u{2c63}{}", self.amount)), ctx.state) } } } diff --git a/stackline/tiles/storage.rs b/stackline/tiles/storage.rs new file mode 100644 index 0000000..ba3d436 --- /dev/null +++ b/stackline/tiles/storage.rs @@ -0,0 +1,103 @@ +//! Arithmetic operations: add, subtract, etc. + +use crate::prelude::*; +use crate::tile::prelude::*; + +use veccell::VecRef; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Store { + signal: Option, +} + +impl Store { + pub fn signal(&self) -> Option<&Signal> { + self.signal.as_ref() + } +} + +impl Tile for Store { + fn update<'b>(&'b mut self, mut context: UpdateContext<'b>) { + if let Some(signal) = context.take_signal() { + let position = context.position(); + + // NOTE: we *could* write the signal immediately, + // but by delaying the write we can read from a `Store` without being order-dependent + context.callback(move |pane| { + if let Some(mut this) = pane.borrow_mut_as::(position) { + this.signal = Some(signal); + } + }); + } + + if context.state() != State::Idle { + context.next_state(); + } + } + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + if self.signal.is_some() { + TextChar::from_state('\u{25c9}', ctx.state) // FISHEYE + } else { + TextChar::from_state('\u{25cb}', ctx.state) // WHITE CIRCLE + } + } +} + +/// When a signal is received, reads a signal from a [`Store`] (at the reader's tail), +/// then outputs that signal (at the reader's head). +/// +/// # Example +/// +/// The following circuit can receive a value in `B`, then store it indefinitely. +/// Sending a signal in `A` will output the stored signal from `B` in `OUT`. +/// +/// ```text +/// o = Store +/// ▸ = Reader(Right) +/// +/// (A) ---+ +/// | +/// (B) --o▸-- (OUT) +/// ``` +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Reader(Direction); + +impl Tile for Reader { + fn update<'b>(&'b mut self, mut context: UpdateContext<'b>) { + if let Some(_signal) = context.take_signal() { + let _: Option<()> = try { + let store_position = context.offset(self.0.opposite().into_offset())?; + let store = context.get(store_position).and_then(get_store)?; + let signal = store.signal.clone()?; + drop(store); + + let target_position = context.offset(self.0.into_offset())?; + + context.send(target_position, self.0, signal); + }; + } + + if context.state() != State::Idle { + context.next_state(); + } + } + + fn draw_simple(&self, ctx: DrawContext) -> TextChar { + match self.0 { + Direction::Down => TextChar::from_state('\u{25bd}', ctx.state), // WHITE DOWN-POINTING TRIANGLE + Direction::Left => TextChar::from_state('\u{25c1}', ctx.state), // WHITE LEFT-POINTING TRIANGLE + Direction::Right => TextChar::from_state('\u{25b7}', ctx.state), // WHITE RIGHT-POINTING TRIANGLE + Direction::Up => TextChar::from_state('\u{25b3}', ctx.state), // WHITE UP-POINTING TRIANGLE + } + } +} + +/// Tries to convert a [`FullTile`] to a [`Store`] +fn get_store<'a>(full: VecRef<'a, FullTile>) -> Option> { + VecRef::try_map(full, |tile| { + let tile = tile.get().ok_or(())?; + let store = tile.try_into().map_err(|_| ())?; + Ok::<&Store, ()>(store) + }).ok() +} diff --git a/stackline/tiles/wire.rs b/stackline/tiles/wire.rs index 7936cef..c01b5f9 100644 --- a/stackline/tiles/wire.rs +++ b/stackline/tiles/wire.rs @@ -36,9 +36,9 @@ impl Tile for Wire { fn draw_simple(&self, ctx: DrawContext) -> TextChar { let ch = match self.0 { - Orientation::Horizontal => '-', - Orientation::Vertical => '|', - Orientation::Any => '+', + Orientation::Horizontal => '-', // BOX DRAWINGS LIGHT HORIZONTAL + Orientation::Vertical => '|', // BOX DRAWINGS LIGHT VERTICAL + Orientation::Any => '+', // BOX DRAWINGS LIGHT HORIZONTAL AND VERTICAL }; TextChar::from_state(ch, ctx.state) @@ -79,7 +79,7 @@ impl Tile for Diode { fn draw_simple(&self, ctx: DrawContext) -> TextChar { let ch = match self.0 { Direction::Up => '^', - Direction::Down => 'v', + Direction::Down => '\u{2335}', // COUNTERSINK Direction::Left => '<', Direction::Right => '>', };