Skip to content

Commit

Permalink
Merge branch 'session-history' into commands
Browse files Browse the repository at this point in the history
Improved save logic: allowed for 0 arguments, in which case session history will be saved to history.nbt
Printed message on successful save
  • Loading branch information
rben01 committed Sep 21, 2024
2 parents 3f987a2 + e4e6c37 commit 87b0812
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 20 deletions.
69 changes: 53 additions & 16 deletions numbat-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ use config::{ColorMode, Config, ExchangeRateFetchingPolicy, IntroBanner, PrettyP
use highlighter::NumbatHighlighter;

use itertools::Itertools;
use numbat::command::{CommandParser, SourcelessCommandParser};
use numbat::command::{self, CommandParser, SourcelessCommandParser};
use numbat::diagnostic::ErrorDiagnostic;
use numbat::help::help_markup;
use numbat::markup as m;
use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystemImporter};
use numbat::pretty_print::PrettyPrint;
use numbat::resolver::CodeSource;
use numbat::{command, RuntimeError};
use numbat::session_history::{ParseEvaluationResult, SessionHistory, SessionHistoryOptions};
use numbat::{Context, NumbatError};
use numbat::{InterpreterSettings, NameResolutionError};

Expand Down Expand Up @@ -95,6 +95,11 @@ struct Args {
debug: bool,
}

struct ParseEvaluationOutcome {
control_flow: ControlFlow,
result: ParseEvaluationResult,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum ExecutionMode {
Normal,
Expand Down Expand Up @@ -190,7 +195,7 @@ impl Cli {
ExecutionMode::Normal,
PrettyPrintMode::Never,
);
if result.is_break() {
if result.control_flow.is_break() {
bail!("Interpreter error in Prelude code")
}
}
Expand All @@ -205,7 +210,7 @@ impl Cli {
ExecutionMode::Normal,
PrettyPrintMode::Never,
);
if result.is_break() {
if result.control_flow.is_break() {
bail!("Interpreter error in user initialization code")
}
}
Expand Down Expand Up @@ -247,7 +252,7 @@ impl Cli {
self.config.pretty_print,
);

let result_status = match result {
let result_status = match result.control_flow {
std::ops::ControlFlow::Continue(()) => Ok(()),
std::ops::ControlFlow::Break(_) => {
bail!("Interpreter stopped")
Expand Down Expand Up @@ -342,6 +347,8 @@ impl Cli {
rl: &mut Editor<NumbatHelper, DefaultHistory>,
interactive: bool,
) -> Result<()> {
let mut session_history = SessionHistory::default();

loop {
let readline = rl.readline(&self.config.prompt);
match readline {
Expand Down Expand Up @@ -399,11 +406,26 @@ impl Cli {
}
command::Command::Clear => rl.clear_screen()?,
command::Command::Save { dst } => {
if rl.save_history(dst).is_err() {
let error = RuntimeError::FileWrite(PathBuf::from(dst));
self.print_diagnostic(error);
continue;
};
let save_result = session_history.save(
dst,
SessionHistoryOptions {
include_err_lines: false,
trim_lines: true,
},
);
match save_result {
Ok(_) => {
let m = m::text(
"successfully saved session history to",
) + m::space()
+ m::string(dst);
println!("{}", ansi_format(&m, interactive));
}
Err(err) => {
self.print_diagnostic(*err);
continue;
}
}
}
command::Command::Quit => return Ok(()),
},
Expand All @@ -416,7 +438,10 @@ impl Cli {
continue;
}

let result = self.parse_and_evaluate(
let ParseEvaluationOutcome {
control_flow,
result,
} = self.parse_and_evaluate(
&line,
CodeSource::Text,
if interactive {
Expand All @@ -427,15 +452,17 @@ impl Cli {
self.config.pretty_print,
);

match result {
match control_flow {
std::ops::ControlFlow::Continue(()) => {}
std::ops::ControlFlow::Break(ExitStatus::Success) => {
return Ok(());
}
std::ops::ControlFlow::Break(ExitStatus::Error) => {
bail!("Interpreter stopped due to error")
}
};
}

session_history.push(line, result);
}
}
Err(ReadlineError::Interrupted) => {}
Expand All @@ -456,7 +483,7 @@ impl Cli {
code_source: CodeSource,
execution_mode: ExecutionMode,
pretty_print_mode: PrettyPrintMode,
) -> ControlFlow {
) -> ParseEvaluationOutcome {
let to_be_printed: Arc<Mutex<Vec<m::Markup>>> = Arc::new(Mutex::new(vec![]));
let to_be_printed_c = to_be_printed.clone();
let mut settings = InterpreterSettings {
Expand All @@ -465,7 +492,7 @@ impl Cli {
}),
};

let result =
let interpretation_result =
self.context
.lock()
.unwrap()
Expand All @@ -479,7 +506,12 @@ impl Cli {
PrettyPrintMode::Auto => interactive,
};

match result {
let parse_eval_result = match &interpretation_result {
Ok(_) => Ok(()),
Err(_) => Err(()),
};

let control_flow = match interpretation_result {
Ok((statements, interpreter_result)) => {
if interactive || pretty_print {
println!();
Expand Down Expand Up @@ -536,6 +568,11 @@ impl Cli {
self.print_diagnostic(e);
execution_mode.exit_status_in_case_of_error()
}
};

ParseEvaluationOutcome {
control_flow,
result: parse_eval_result,
}
}

Expand Down
13 changes: 9 additions & 4 deletions numbat/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,15 @@ impl<'a> CommandParser<'a> {
Command::List { items }
}
CommandKind::Save => {
let err_msg = "`save` requires exactly one argument, the destination";
let Some(dst) = self.inner.args.next() else {
return Err(self.err_at_idx(0, err_msg));
return Ok(Command::Save { dst: "history.nbt" });
};

if self.inner.args.next().is_some() {
return Err(self.err_through_end_from(2, err_msg));
return Err(self.err_through_end_from(
2,
"`save` requires exactly one argument, the destination",
));
}

Command::Save { dst }
Expand Down Expand Up @@ -481,7 +483,10 @@ mod test {
assert!(parse!("exit arg").is_err());
assert!(parse!("exit arg1 arg2").is_err());

assert!(parse!("save").is_err());
assert_eq!(
parse!("save").unwrap(),
Command::Save { dst: "history.nbt" }
);
assert_eq!(parse!("save arg").unwrap(), Command::Save { dst: "arg" });
assert_eq!(parse!("save .").unwrap(), Command::Save { dst: "." });
assert!(parse!("save arg1 arg2").is_err());
Expand Down
1 change: 1 addition & 0 deletions numbat/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mod product;
mod quantity;
mod registry;
pub mod resolver;
pub mod session_history;
mod span;
mod suggestion;
mod tokenizer;
Expand Down
139 changes: 139 additions & 0 deletions numbat/src/session_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use std::{fs, io, path::Path};

use crate::RuntimeError;

pub type ParseEvaluationResult = Result<(), ()>;

#[derive(Debug)]
struct SessionHistoryItem {
input: String,
result: ParseEvaluationResult,
}

#[derive(Default)]
pub struct SessionHistory(Vec<SessionHistoryItem>);

impl SessionHistory {
pub fn new() -> Self {
Self::default()
}
}

#[derive(Debug, Clone, Copy)]
pub struct SessionHistoryOptions {
pub include_err_lines: bool,
pub trim_lines: bool,
}

impl SessionHistory {
pub fn push(&mut self, input: String, result: ParseEvaluationResult) {
self.0.push(SessionHistoryItem { input, result });
}

fn save_inner(
&self,
mut w: impl io::Write,
options: SessionHistoryOptions,
err_fn: impl Fn(io::Error) -> RuntimeError,
) -> Result<(), Box<RuntimeError>> {
let SessionHistoryOptions {
include_err_lines,
trim_lines,
} = options;

for item in &self.0 {
if item.result.is_err() && !include_err_lines {
continue;
}

let input = if trim_lines {
item.input.trim()
} else {
&item.input
};

writeln!(w, "{input}").map_err(&err_fn)?
}
Ok(())
}

pub fn save(
&self,
dst: impl AsRef<Path>,
options: SessionHistoryOptions,
) -> Result<(), Box<RuntimeError>> {
let dst = dst.as_ref();
let err_fn = |_: io::Error| RuntimeError::FileWrite(dst.to_owned());

let f = fs::File::create(dst).map_err(err_fn)?;
self.save_inner(f, options, err_fn)
}
}

#[cfg(test)]
mod test {
use super::*;
use std::io::Cursor;

#[test]
fn test() {
let mut sh = SessionHistory::new();

// arbitrary non-ascii characters
sh.push(" a→ ".to_owned(), Ok(()));
sh.push(" b × c ".to_owned(), Err(()));
sh.push(" d ♔ e ⚀ f ".to_owned(), Err(()));
sh.push(" g ☼ h ▶︎ i ❖ j ".to_owned(), Ok(()));

let test_cases = [
(
SessionHistoryOptions {
include_err_lines: false,
trim_lines: false,
},
" a→ \n g ☼ h ▶︎ i ❖ j \n",
),
(
SessionHistoryOptions {
include_err_lines: true,
trim_lines: false,
},
" a→ \n b × c \n d ♔ e ⚀ f \n g ☼ h ▶︎ i ❖ j \n",
),
(
SessionHistoryOptions {
include_err_lines: false,
trim_lines: true,
},
"a→\ng ☼ h ▶︎ i ❖ j\n",
),
(
SessionHistoryOptions {
include_err_lines: true,
trim_lines: true,
},
"a→\nb × c\nd ♔ e ⚀ f\ng ☼ h ▶︎ i ❖ j\n",
),
];

for (options, expected) in test_cases {
let mut s = Cursor::new(Vec::<u8>::new());
sh.save_inner(&mut s, options, |_| unreachable!()).unwrap();
assert_eq!(expected, String::from_utf8(s.into_inner()).unwrap())
}
}

#[test]
fn test_error() {
let sh = SessionHistory::new();
assert!(sh
.save(
".", // one place we know writing will fail
SessionHistoryOptions {
include_err_lines: false,
trim_lines: false
}
)
.is_err())
}
}

0 comments on commit 87b0812

Please sign in to comment.