From 285b4f298376fd8e1054abee885234e37d087ab5 Mon Sep 17 00:00:00 2001 From: John Simon Date: Sat, 2 Nov 2019 18:59:25 -0400 Subject: [PATCH] first blood --- .editorconfig | 14 +++++ .gitignore | 2 + .gitlab-ci.yml | 20 ++++++++ .pre-commit-config.yaml | 24 +++++++++ Cargo.lock | 5 ++ Cargo.toml | 10 ++++ README.md | 34 ++++++++++++ rustfmt.toml | 19 +++++++ src/engine.rs | 59 +++++++++++++++++++++ src/lib.rs | 16 ++++++ src/safe_rc.rs | 111 ++++++++++++++++++++++++++++++++++++++++ src/state.rs | 6 +++ src/waker.rs | 17 ++++++ 13 files changed, 337 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 rustfmt.toml create mode 100644 src/engine.rs create mode 100644 src/lib.rs create mode 100644 src/safe_rc.rs create mode 100644 src/state.rs create mode 100644 src/waker.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2926a4e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# https://editorconfig.org/ + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a63c870 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,20 @@ +fmt: + # Use a third-party repo since the official repo doesn't include tags. + # https://github.com/rust-lang-nursery/docker-rust-nightly/issues/3 + image: instrumentisto/rust:nightly-2019-08-15 + before_script: + - rustup component add rustfmt-preview + script: + - cargo fmt -- --check + +check: + image: rust:1.37 + before_script: + - rustup component add clippy + script: + - cargo clippy --all-targets --features strict + +test: + image: rust:1.37 + script: + - cargo test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..28cde13 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +fail_fast: true + +repos: + - repo: local + hooks: + - id: fmt + name: fmt + language: system + files: '[.]rs$' + entry: rustup run nightly-2019-08-15 rustfmt + + - id: check + name: check + language: system + files: '[.]rs$' + entry: cargo clippy --all-targets --features strict + pass_filenames: false + + - id: test + name: test + language: system + files: '[.]rs$' + entry: cargo test + pass_filenames: false diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e31fc2c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "genawaiter" +version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9ab0e66 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "genawaiter" +version = "0.0.0" +authors = ["John Simon "] +edition = "2018" + +[dependencies] + +[features] +strict = [] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4ae704 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# nirvana-rust + +An opinionated [Rust] project starter. There are many like it, but this one is mine. + +[Rust]: https://www.rust-lang.org/ + +## Development + +### Install prerequisites + +- [Rust] +- [pre-commit] + +[pre-commit]: https://pre-commit.com/ + +### Install the pre-commit hook + +```sh +pre-commit install +``` + +This installs a Git hook that runs a quick sanity check before every commit. + +### Run the app + +```sh +cargo run +``` + +### Run the tests + +```sh +cargo test +``` diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e528840 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,19 @@ +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md + +edition = "2018" +error_on_line_overflow = true +error_on_unformatted = true +force_multiline_blocks = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +imports_layout = "HorizontalVertical" +max_width = 88 +merge_imports = true +newline_style = "Unix" +overflow_delimited_expr = true +report_fixme = "Unnumbered" +report_todo = "Unnumbered" +use_field_init_shorthand = true +version = "Two" +wrap_comments = true diff --git a/src/engine.rs b/src/engine.rs new file mode 100644 index 0000000..dd79f41 --- /dev/null +++ b/src/engine.rs @@ -0,0 +1,59 @@ +use crate::{waker, GeneratorState}; +use std::{ + cell::RefCell, + future::Future, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +type Airlock = Rc>>; + +pub fn advance( + future: Pin<&mut impl Future>, + airlock: &Airlock, +) -> GeneratorState { + let waker = waker::create(); + let mut cx = Context::from_waker(&waker); + + match future.poll(&mut cx) { + Poll::Pending => { + let value = airlock.borrow_mut().take().unwrap(); + GeneratorState::Yielded(value) + } + Poll::Ready(value) => GeneratorState::Complete(value), + } +} + +pub struct Co { + pub(crate) airlock: Airlock, +} + +impl Co { + pub fn yield_(&self, value: Y) -> impl Future + '_ { + *self.airlock.borrow_mut() = Some(value); + Barrier { + airlock: &self.airlock, + } + } +} + +pub struct Barrier<'y, Y> { + airlock: &'y Airlock, +} + +impl<'y, Y> Future for Barrier<'y, Y> { + type Output = (); + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + if self.airlock.borrow().is_none() { + // If there is no value in the airlock, resume the generator so it produces + // one. + Poll::Ready(()) + } else { + // If there is a value, pause the generator so we can yield the value to the + // caller. + Poll::Pending + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f029ee8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,16 @@ +#![feature(async_await, async_closure)] +#![warn(future_incompatible, rust_2018_compatibility, rust_2018_idioms, unused)] +#![warn(clippy::pedantic)] +// #![warn(clippy::cargo)] +#![cfg_attr(feature = "strict", deny(warnings))] + +pub use crate::{ + engine::Co, + safe_rc::Generator as SafeRcGenerator, + state::GeneratorState, +}; + +mod engine; +mod safe_rc; +mod state; +mod waker; diff --git a/src/safe_rc.rs b/src/safe_rc.rs new file mode 100644 index 0000000..40c922d --- /dev/null +++ b/src/safe_rc.rs @@ -0,0 +1,111 @@ +use crate::{engine::advance, Co, GeneratorState}; +use std::{cell::RefCell, future::Future, pin::Pin, rc::Rc}; + +pub struct Generator { + airlock: Rc>>, + future: Pin>, +} + +impl Generator { + pub fn new(start: impl FnOnce(Co) -> F) -> Self { + let airlock = Rc::new(RefCell::new(None)); + let future = { + let airlock = airlock.clone(); + Box::pin(start(Co { airlock })) + }; + Self { airlock, future } + } + + pub fn resume(&mut self) -> GeneratorState { + advance(self.future.as_mut(), &self.airlock) + } +} + +#[cfg(test)] +mod tests { + use crate::{safe_rc::Generator, Co, GeneratorState}; + use std::future::Future; + + async fn simple_producer(c: Co) -> &'static str { + c.yield_(10).await; + "done" + } + + #[test] + fn function() { + let mut gen = Generator::new(simple_producer); + assert_eq!(gen.resume(), GeneratorState::Yielded(10)); + assert_eq!(gen.resume(), GeneratorState::Complete("done")); + } + + #[test] + fn simple_closure() { + async fn gen(i: i32, co: Co) -> &'static str { + co.yield_(i * 2).await; + "done" + } + + let mut gen = Generator::new(|co| gen(5, co)); + assert_eq!(gen.resume(), GeneratorState::Yielded(10)); + assert_eq!(gen.resume(), GeneratorState::Complete("done")); + } + + #[test] + fn async_closure() { + let mut gen = Generator::new(async move |co| { + co.yield_(10).await; + "done" + }); + assert_eq!(gen.resume(), GeneratorState::Yielded(10)); + assert_eq!(gen.resume(), GeneratorState::Complete("done")); + } + + #[test] + fn pinned() { + #[inline(never)] + async fn produce(addrs: &mut Vec<*const i32>, co: Co) -> &'static str { + use std::cell::Cell; + + let sentinel: Cell = Cell::new(0x8001); + let sentinel_ref: &Cell = &sentinel; + + assert_eq!(sentinel.get(), 0x8001); + sentinel_ref.set(0x8002); + assert_eq!(sentinel.get(), 0x8002); + addrs.push(sentinel.as_ptr()); + + co.yield_(10).await; + + assert_eq!(sentinel.get(), 0x8002); + sentinel_ref.set(0x8003); + assert_eq!(sentinel.get(), 0x8003); + addrs.push(sentinel.as_ptr()); + + co.yield_(20).await; + + assert_eq!(sentinel.get(), 0x8003); + sentinel_ref.set(0x8004); + assert_eq!(sentinel.get(), 0x8004); + addrs.push(sentinel.as_ptr()); + + "done" + } + + fn create_generator( + addrs: &mut Vec<*const i32>, + ) -> Generator + '_> { + let mut gen = Generator::new(move |co| produce(addrs, co)); + assert_eq!(gen.resume(), GeneratorState::Yielded(10)); + gen + } + + let mut addrs = Vec::new(); + let mut gen = create_generator(&mut addrs); + + assert_eq!(gen.resume(), GeneratorState::Yielded(20)); + assert_eq!(gen.resume(), GeneratorState::Complete("done")); + drop(gen); + + assert!(addrs.iter().all(|&p| p == addrs[0])); + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..f5e3133 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,6 @@ +#[cfg_attr(test, derive(PartialEq, Debug))] +#[allow(clippy::module_name_repetitions)] +pub enum GeneratorState { + Yielded(Y), + Complete(R), +} diff --git a/src/waker.rs b/src/waker.rs new file mode 100644 index 0000000..53290cd --- /dev/null +++ b/src/waker.rs @@ -0,0 +1,17 @@ +use std::{ + ptr, + task::{RawWaker, RawWakerVTable, Waker}, +}; + +pub fn create() -> Waker { + unsafe { Waker::from_raw(RAW_WAKER) } +} + +const VTABLE: RawWakerVTable = RawWakerVTable::new( + /* clone */ |_| panic!(), + /* wake */ |_| {}, + /* wake_by_ref */ |_| {}, + /* drop */ |_| {}, +); + +const RAW_WAKER: RawWaker = RawWaker::new(ptr::null(), &VTABLE);