Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expression examples #65

Merged
merged 1 commit into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ pub(super) mod state;

use futures::{FutureExt, SinkExt, StreamExt};

use crate::repl::{
driver::{ReplCommand, ReplEvent},
example::ReplExample,
use crate::{
examples::Example,
expression::driver::{EvaluateExpression, ExpressionEvent},
repl::driver::{ReplCommand, ReplEvent},
};

use self::state::State;

pub(crate) struct Inputs {
pub(crate) repl_examples: Vec<ReplExample>,
pub(crate) examples: Vec<Example>,
pub(crate) repl_events: futures::stream::LocalBoxStream<'static, ReplEvent>,
pub(crate) expression_events: futures::stream::LocalBoxStream<'static, ExpressionEvent>,
}

pub(crate) struct Outputs {
pub(crate) execution_handle: futures::future::LocalBoxFuture<'static, ()>,
pub(crate) repl_commands: futures::stream::LocalBoxStream<'static, ReplCommand>,
pub(crate) expression_commands: futures::stream::LocalBoxStream<'static, EvaluateExpression>,
pub(crate) done: futures::future::LocalBoxFuture<'static, anyhow::Result<()>>,
pub(crate) eprintln_strings: futures::stream::LocalBoxStream<'static, String>,
}
Expand All @@ -25,32 +28,43 @@ pub(crate) struct Outputs {
enum OutputEvent {
Done(anyhow::Result<()>),
ReplCommand(ReplCommand),
ExpressionCommand(EvaluateExpression),
Eprintln(String),
}

#[derive(Debug)]
enum InputEvent {
ReplExample(ReplExample),
Example(Example),
ReplEvent(ReplEvent),
ExpressionEvent(ExpressionEvent),
}

pub(crate) fn app(inputs: Inputs) -> Outputs {
let Inputs {
repl_examples,
examples,
repl_events,
expression_events,
} = inputs;

let repl_examples = futures::stream::iter(repl_examples).map(InputEvent::ReplExample);
let examples = futures::stream::iter(examples).map(InputEvent::Example);

let repl_events = repl_events.map(InputEvent::ReplEvent);
let expression_events = expression_events.map(InputEvent::ExpressionEvent);

let input_events =
futures::stream::select_all([repl_examples.boxed_local(), repl_events.boxed_local()]);
let input_events = futures::stream::select_all([
examples.boxed_local(),
repl_events.boxed_local(),
expression_events.boxed_local(),
]);

let output_events = input_events
.scan(State::default(), |state, event| {
let output = match event {
InputEvent::ReplExample(repl_example) => state.repl_example(repl_example),
InputEvent::Example(example) => state.example(example),
InputEvent::ReplEvent(repl_event) => state.repl_event(repl_event),
InputEvent::ExpressionEvent(expression_event) => {
state.expression_event(expression_event)
}
};

let output = match output {
Expand All @@ -64,6 +78,8 @@ pub(crate) fn app(inputs: Inputs) -> Outputs {

let (eprintln_sender, eprintln_strings) = futures::channel::mpsc::unbounded::<String>();
let (repl_commands_sender, repl_commands) = futures::channel::mpsc::unbounded::<ReplCommand>();
let (expression_commands_sender, expression_commands) =
futures::channel::mpsc::unbounded::<EvaluateExpression>();
let (done_sender, done) = futures::channel::mpsc::unbounded::<anyhow::Result<()>>();

let execution_handle = output_events.for_each(move |output_event| match output_event {
Expand All @@ -81,6 +97,13 @@ pub(crate) fn app(inputs: Inputs) -> Outputs {
}
.boxed_local()
}
OutputEvent::ExpressionCommand(evaluate_expression) => {
let mut sender = expression_commands_sender.clone();
async move {
sender.send(evaluate_expression).await.unwrap();
}
.boxed_local()
}
OutputEvent::Eprintln(string) => {
let mut sender = eprintln_sender.clone();
async move {
Expand All @@ -93,6 +116,7 @@ pub(crate) fn app(inputs: Inputs) -> Outputs {
Outputs {
eprintln_strings: eprintln_strings.boxed_local(),
repl_commands: repl_commands.boxed_local(),
expression_commands: expression_commands.boxed_local(),
done: done
.into_future()
.map(|(next_item, _tail)| next_item.unwrap())
Expand Down
137 changes: 116 additions & 21 deletions src/app/state.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
pub(crate) mod expression_state;
pub(crate) mod repl_state;

use anyhow::bail;
use indoc::formatdoc;

use crate::{
example_id::ExampleId,
repl::{
driver::{ReplCommand, ReplEvent, ReplQuery},
example::ReplExample,
},
examples::Example,
expression::driver::{EvaluateExpression, ExpressionEvent},
repl::driver::{ReplCommand, ReplEvent, ReplQuery},
};

use self::repl_state::{ReplExampleState, ReplSessionExpecting, ReplSessionLive, ReplSessionState};
use self::{
expression_state::ExpressionExampleState,
repl_state::{ReplExampleState, ReplSessionExpecting, ReplSessionLive, ReplSessionState},
};

use super::OutputEvent;

Expand All @@ -18,16 +24,25 @@ pub(super) struct State {
}

impl State {
pub(super) fn repl_example(
&mut self,
repl_example: ReplExample,
) -> anyhow::Result<Vec<OutputEvent>> {
let id = repl_example.id.clone();
pub(super) fn example(&mut self, example: Example) -> anyhow::Result<Vec<OutputEvent>> {
let (id, example_state, event) = match example {
Example::Repl(example) => {
let example_id = example.id.clone();
let example_state = ExampleState::Repl(ReplExampleState::new(example));
let event = OutputEvent::ReplCommand(ReplCommand::Spawn(example_id.clone()));
(example_id, example_state, event)
}
Example::Expression(example) => {
let example_id = example.id.clone();
let example_state = ExampleState::Expression(ExpressionExampleState::Pending);
let event = OutputEvent::ExpressionCommand(EvaluateExpression(example));
(example_id, example_state, event)
}
};

self.examples
.insert(id.clone(), ReplExampleState::new(repl_example))?;
self.examples.insert(id.clone(), example_state)?;

Ok(vec![OutputEvent::ReplCommand(ReplCommand::Spawn(id))])
Ok(vec![event])
}

pub(super) fn repl_event(&mut self, repl_event: ReplEvent) -> anyhow::Result<Vec<OutputEvent>> {
Expand All @@ -45,7 +60,7 @@ impl State {
) -> anyhow::Result<Vec<OutputEvent>> {
let id = spawn?;

let session = self.examples.get_mut(&id)?;
let session = self.examples.get_mut_repl(&id)?;

if let ReplSessionState::Live(_) = &session.state {
return Err(anyhow::anyhow!("spawned session {session:?} already live"));
Expand Down Expand Up @@ -88,7 +103,7 @@ impl State {
id: ExampleId,
result: std::io::Result<u8>,
) -> anyhow::Result<Vec<OutputEvent>> {
let session_live = self.examples.get_mut(&id)?;
let session_live = self.examples.get_mut_repl(&id)?;
let session_live = session_live.state.live_mut()?;
let ch = result?;

Expand Down Expand Up @@ -152,7 +167,7 @@ impl State {
}

fn next_query(&mut self, id: &ExampleId) -> anyhow::Result<Vec<OutputEvent>> {
let session = self.examples.get_mut(id)?;
let session = self.examples.get_mut_repl(id)?;

let ReplSessionState::Live(session_live) = &mut session.state else {
anyhow::bail!("expected session {id} to be live");
Expand All @@ -175,7 +190,7 @@ impl State {
}

fn session_end(&mut self, id: &ExampleId) -> anyhow::Result<Vec<OutputEvent>> {
let session = self.examples.get_mut(id)?;
let session = self.examples.get_mut_repl(id)?;
session.state = ReplSessionState::Killing;
Ok(vec![
OutputEvent::ReplCommand(ReplCommand::Kill(id.clone())),
Expand All @@ -195,26 +210,81 @@ impl State {
.collect();
Ok(string)
}

pub(crate) fn expression_event_output(
&mut self,
expression_output: std::io::Result<(ExampleId, std::process::Output)>,
) -> anyhow::Result<Vec<OutputEvent>> {
let (example_id, expression_output) = expression_output?;

if !expression_output.status.success() {
let stderr = String::from_utf8_lossy(&expression_output.stderr);
bail!("{example_id}\n{stderr}")
}

if expression_output.stdout != b"null\n" {
let stdout = String::from_utf8_lossy(&expression_output.stdout);
let stdout = stdout.trim_end();

let message = formatdoc! {"
{example_id}
evaluated into non-null
note: examples must evaluate into null
value: {stdout}"};

bail!("{message}");
}

self.examples.remove(&example_id)?;

let mut events = vec![OutputEvent::Eprintln(Self::fmt_pass(&example_id))];

if self.examples.is_empty() {
events.push(OutputEvent::Done(Ok(())))
}

Ok(events)
}

pub(crate) fn expression_event(
&mut self,
expression_event: ExpressionEvent,
) -> Result<Vec<OutputEvent>, anyhow::Error> {
match expression_event {
ExpressionEvent::Spawn(result) => self.expression_event_spawn(result),
ExpressionEvent::Output(result) => self.expression_event_output(result),
}
}

fn expression_event_spawn(
&mut self,
result: Result<ExampleId, std::io::Error>,
) -> anyhow::Result<Vec<OutputEvent>> {
let example_id = result?;
let example_state = self.examples.get_mut_expression(&example_id)?;
*example_state = ExpressionExampleState::Spawned;
Ok(vec![])
}
}

#[derive(Debug, Default)]
pub(crate) struct ExamplesState(std::collections::BTreeMap<ExampleId, ReplExampleState>);
pub(crate) struct ExamplesState(std::collections::BTreeMap<ExampleId, ExampleState>);

impl ExamplesState {
pub(crate) fn insert(&mut self, id: ExampleId, state: ReplExampleState) -> anyhow::Result<()> {
pub(crate) fn insert(&mut self, id: ExampleId, state: ExampleState) -> anyhow::Result<()> {
if self.0.insert(id.clone(), state).is_some() {
anyhow::bail!("duplicate session id {id:?}");
};
Ok(())
}

pub(crate) fn get_mut(&mut self, id: &ExampleId) -> anyhow::Result<&mut ReplExampleState> {
pub(crate) fn get_mut(&mut self, id: &ExampleId) -> anyhow::Result<&mut ExampleState> {
self.0
.get_mut(id)
.ok_or_else(|| anyhow::anyhow!("repl session not found {id:?}"))
}

pub(crate) fn remove(&mut self, id: &ExampleId) -> anyhow::Result<ReplExampleState> {
pub(crate) fn remove(&mut self, id: &ExampleId) -> anyhow::Result<ExampleState> {
self.0
.remove(id)
.ok_or_else(|| anyhow::anyhow!("repl session not found {id:?}"))
Expand All @@ -223,4 +293,29 @@ impl ExamplesState {
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}

fn get_mut_repl(&mut self, id: &ExampleId) -> anyhow::Result<&mut ReplExampleState> {
let example_state = self.get_mut(id)?;
let ExampleState::Repl(repl_example_state) = example_state else {
anyhow::bail!("expected repl example state");
};
Ok(repl_example_state)
}

fn get_mut_expression(
&mut self,
id: &ExampleId,
) -> anyhow::Result<&mut ExpressionExampleState> {
let example_state = self.get_mut(id)?;
let ExampleState::Expression(expression_example_state) = example_state else {
anyhow::bail!("expected expression example state");
};
Ok(expression_example_state)
}
}

#[derive(Debug)]
pub(crate) enum ExampleState {
Repl(ReplExampleState),
Expression(ExpressionExampleState),
}
6 changes: 6 additions & 0 deletions src/app/state/expression_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[derive(Debug, Default)]
pub(crate) enum ExpressionExampleState {
#[default]
Pending,
Spawned,
}
28 changes: 22 additions & 6 deletions src/examples.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use crate::expression::ExpressionExample;
use crate::repl::example::ReplExample;
use crate::repl::example::NIX_REPL_LANG_TAG;
use itertools::Itertools;

pub(crate) fn obtain(glob: &str) -> anyhow::Result<Vec<ReplExample>> {
#[derive(Debug, Clone)]
pub(crate) enum Example {
Repl(ReplExample),
Expression(ExpressionExample),
}

pub(crate) fn obtain(glob: &str) -> anyhow::Result<Vec<Example>> {
glob::glob(glob)?
.map(|path| {
let path = camino::Utf8PathBuf::try_from(path?)?;
Expand All @@ -27,15 +34,24 @@ pub(crate) fn obtain(glob: &str) -> anyhow::Result<Vec<ReplExample>> {
.filter_map(|(path, ast)| {
if let comrak::nodes::NodeValue::CodeBlock(code_block) = ast.value {
let comrak::nodes::NodeCodeBlock { info, literal, .. } = code_block;
if let Some(NIX_REPL_LANG_TAG) = info.split_ascii_whitespace().next() {
Some((path, ast.sourcepos.start.line, literal.clone()))
} else {
None
match info.split_ascii_whitespace().next() {
Some(NIX_REPL_LANG_TAG) => {
let line = ast.sourcepos.start.line;
let repl_example =
ReplExample::try_new(path, line, literal.clone()).map(Example::Repl);
Some(repl_example)
}
Some("nix") => {
let line = ast.sourcepos.start.line;
let expression_example =
ExpressionExample::new(path, line, literal.clone());
Some(Ok(Example::Expression(expression_example)))
}
_ => None,
}
} else {
None
}
})
.map(|(path, line, contents)| ReplExample::try_new(path, line, contents))
.try_collect()
}
16 changes: 16 additions & 0 deletions src/expression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub(crate) mod driver;

use crate::example_id::ExampleId;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExpressionExample {
pub(crate) id: ExampleId,
pub(crate) expression: String,
}

impl ExpressionExample {
pub(crate) fn new(path: camino::Utf8PathBuf, line: usize, expression: String) -> Self {
let id = ExampleId::new(path, line);
Self { id, expression }
}
}
Loading
Loading