use crate::ast::{ArgSep, Expr, Value};
use crate::exec::{BuiltinCommand, Machine};
use failure::Fallible;
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
pub trait Console {
fn input(&mut self, prompt: &str, previous: &str) -> io::Result<String>;
fn print(&mut self, text: &str) -> io::Result<()>;
}
pub struct InputCommand {
console: Rc<RefCell<dyn Console>>,
}
impl InputCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Self {
Self { console }
}
}
impl BuiltinCommand for InputCommand {
fn name(&self) -> &'static str {
"INPUT"
}
fn syntax(&self) -> &'static str {
"[\"prompt\"] <;|,> variableref"
}
fn description(&self) -> &'static str {
"Obtains user input from the console.
The first expression to this function must be empty or evaluate to a string, and specifies \
the prompt to print. If this first argument is followed by the short `;` separator, the \
prompt is extended with a question mark.
The second expression to this function must be a bare variable reference and indicates the \
variable to update with the obtained input."
}
fn exec(&self, args: &[(Option<Expr>, ArgSep)], machine: &mut Machine) -> Fallible<()> {
if args.len() != 2 {
bail!("INPUT requires two arguments");
}
let mut prompt = match &args[0].0 {
Some(e) => match e.eval(machine.get_vars())? {
Value::Text(t) => t,
_ => bail!("INPUT prompt must be a string"),
},
None => "".to_owned(),
};
if let ArgSep::Short = args[0].1 {
prompt += "? ";
}
let vref = match &args[1].0 {
Some(Expr::Symbol(vref)) => vref,
_ => bail!("INPUT requires a variable reference"),
};
let mut console = self.console.borrow_mut();
let mut previous_answer = String::new();
loop {
match console.input(&prompt, &previous_answer) {
Ok(answer) => {
match Value::parse_as(vref.ref_type(), answer.trim_end()) {
Ok(value) => return machine.get_mut_vars().set(vref, value),
Err(e) => console.print(&format!("Retry input: {}", e))?,
}
previous_answer = answer;
}
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
console.print(&format!("Retry input: {}", e))?
}
Err(e) => return Err(e.into()),
}
}
}
}
pub struct PrintCommand {
console: Rc<RefCell<dyn Console>>,
}
impl PrintCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Self {
Self { console }
}
}
impl BuiltinCommand for PrintCommand {
fn name(&self) -> &'static str {
"PRINT"
}
fn syntax(&self) -> &'static str {
"[expr1 [<;|,> .. exprN]]"
}
fn description(&self) -> &'static str {
"Prints a message to the console.
The expressions given as arguments are all evaluated and converted to strings. Arguments \
separated by the short `;` separator are concatenated with a single space, while arguments \
separated by the long `,` separator are concatenated with a tab character."
}
fn exec(&self, args: &[(Option<Expr>, ArgSep)], machine: &mut Machine) -> Fallible<()> {
let mut text = String::new();
for arg in args.iter() {
if let Some(expr) = arg.0.as_ref() {
text += &expr.eval(machine.get_vars())?.to_string();
}
match arg.1 {
ArgSep::End => break,
ArgSep::Short => text += " ",
ArgSep::Long => text += "\t",
}
}
self.console.borrow_mut().print(&text)?;
Ok(())
}
}
pub fn all_commands(console: Rc<RefCell<dyn Console>>) -> Vec<Rc<dyn BuiltinCommand>> {
vec![
Rc::from(InputCommand::new(console.clone())),
Rc::from(PrintCommand::new(console)),
]
}
#[cfg(test)]
pub(crate) mod testutils {
use super::*;
use std::io;
pub(crate) struct MockConsole {
golden_in: Box<dyn Iterator<Item = &'static (&'static str, &'static str, &'static str)>>,
captured_out: Vec<String>,
}
impl MockConsole {
pub(crate) fn new(
golden_in: &'static [(&'static str, &'static str, &'static str)],
) -> Self {
Self {
golden_in: Box::from(golden_in.iter()),
captured_out: vec![],
}
}
pub(crate) fn captured_out(&self) -> &[String] {
self.captured_out.as_slice()
}
}
impl Console for MockConsole {
fn input(&mut self, prompt: &str, previous: &str) -> io::Result<String> {
let (expected_prompt, expected_previous, answer) = self.golden_in.next().unwrap();
assert_eq!(expected_prompt, &prompt);
assert_eq!(expected_previous, &previous);
Ok((*answer).to_owned())
}
fn print(&mut self, text: &str) -> io::Result<()> {
self.captured_out.push(text.to_owned());
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::testutils::*;
use super::*;
use crate::exec::MachineBuilder;
fn do_ok_test(
input: &str,
golden_in: &'static [(&'static str, &'static str, &'static str)],
expected_out: &'static [&'static str],
) {
let console = Rc::from(RefCell::from(MockConsole::new(golden_in)));
let mut machine = MachineBuilder::default()
.add_builtins(all_commands(console.clone()))
.build();
machine
.exec(&mut input.as_bytes())
.expect("Execution failed");
assert_eq!(expected_out, console.borrow().captured_out());
}
fn do_error_test(
input: &str,
golden_in: &'static [(&'static str, &'static str, &'static str)],
expected_out: &'static [&'static str],
expected_err: &str,
) {
let console = Rc::from(RefCell::from(MockConsole::new(golden_in)));
let mut machine = MachineBuilder::default()
.add_builtins(all_commands(console.clone()))
.build();
assert_eq!(
expected_err,
format!(
"{}",
machine
.exec(&mut input.as_bytes())
.expect_err("Execution did not fail")
)
);
assert_eq!(expected_out, console.borrow().captured_out());
}
fn do_simple_error_test(input: &str, expected_err: &str) {
do_error_test(input, &[], &[], expected_err);
}
#[test]
fn test_input_ok() {
do_ok_test("INPUT ; foo\nPRINT foo", &[("? ", "", "9")], &["9"]);
do_ok_test("INPUT ; foo\nPRINT foo", &[("? ", "", "-9")], &["-9"]);
do_ok_test("INPUT , bar?\nPRINT bar", &[("", "", "true")], &["TRUE"]);
do_ok_test("INPUT ; foo$\nPRINT foo", &[("? ", "", "")], &[""]);
do_ok_test(
"INPUT \"With question mark\"; a$\nPRINT a$",
&[("With question mark? ", "", "some long text")],
&["some long text"],
);
do_ok_test(
"prompt$ = \"Indirectly without question mark\"\nINPUT prompt$, b\nPRINT b * 2",
&[("Indirectly without question mark", "", "42")],
&["84"],
);
}
#[test]
fn test_input_retry() {
do_ok_test(
"INPUT ; b?",
&[("? ", "", ""), ("? ", "", "true")],
&["Retry input: Invalid boolean literal "],
);
do_ok_test(
"INPUT ; b?",
&[("? ", "", "0"), ("? ", "0", "true")],
&["Retry input: Invalid boolean literal 0"],
);
do_ok_test(
"a = 3\nINPUT ; a",
&[("? ", "", ""), ("? ", "", "7")],
&["Retry input: Invalid integer literal "],
);
do_ok_test(
"a = 3\nINPUT ; a",
&[("? ", "", "x"), ("? ", "x", "7")],
&["Retry input: Invalid integer literal x"],
);
}
#[test]
fn test_input_errors() {
do_simple_error_test("INPUT", "INPUT requires two arguments");
do_simple_error_test("INPUT ; ,", "INPUT requires two arguments");
do_simple_error_test("INPUT ;", "INPUT requires a variable reference");
do_simple_error_test("INPUT 3 ; a", "INPUT prompt must be a string");
do_simple_error_test("INPUT ; a + 1", "INPUT requires a variable reference");
do_simple_error_test(
"INPUT \"a\" + TRUE; b?",
"Cannot add Text(\"a\") and Boolean(true)",
);
}
#[test]
fn test_print_ok() {
do_ok_test("PRINT", &[], &[""]);
do_ok_test("PRINT ;", &[], &[" "]);
do_ok_test("PRINT ,", &[], &["\t"]);
do_ok_test("PRINT ;,;,", &[], &[" \t \t"]);
do_ok_test("PRINT 3", &[], &["3"]);
do_ok_test("PRINT 3 = 5", &[], &["FALSE"]);
do_ok_test("PRINT true;123;\"foo bar\"", &[], &["TRUE 123 foo bar"]);
do_ok_test("PRINT 6,1;3,5", &[], &["6\t1 3\t5"]);
do_ok_test(
"word = \"foo\"\nPRINT word, word\nPRINT word + \"s\"",
&[],
&["foo\tfoo", "foos"],
);
}
#[test]
fn test_print_errors() {
do_simple_error_test("PRINT a b", "Unexpected value in expression");
do_simple_error_test("PRINT 3 + TRUE", "Cannot add Integer(3) and Boolean(true)");
}
}