diff --git a/ablescript/src/error.rs b/ablescript/src/error.rs index ad38ac7..fd06cd9 100644 --- a/ablescript/src/error.rs +++ b/ablescript/src/error.rs @@ -14,6 +14,7 @@ pub enum ErrorKind { UnknownVariable(String), MeloVariable(String), TopLevelEnough, + NonExitingRlyeh(i32), MissingLhs, Brian(InterpretError), Io(io::Error), @@ -51,6 +52,7 @@ impl Display for ErrorKind { ErrorKind::UnknownVariable(name) => write!(f, "unknown identifier \"{}\"", name), ErrorKind::MeloVariable(name) => write!(f, "banned variable \"{}\"", name), ErrorKind::TopLevelEnough => write!(f, "can only `enough` out of a loop"), + &ErrorKind::NonExitingRlyeh(code) => write!(f, "program exited with code {code}"), ErrorKind::Brian(err) => write!(f, "brainfuck error: {}", err), // TODO: give concrete numbers here. ErrorKind::MissingLhs => write!(f, "missing expression before binary operation"), diff --git a/ablescript/src/host_interface.rs b/ablescript/src/host_interface.rs new file mode 100644 index 0000000..740ce6e --- /dev/null +++ b/ablescript/src/host_interface.rs @@ -0,0 +1,47 @@ +/// Host Environment Interface +pub trait HostInterface { + type Data; + + /// Print a string + fn print(&mut self, string: &str, new_line: bool) -> std::io::Result<()>; + + /// Read a byte + fn read_byte(&mut self) -> std::io::Result; + + /// This function should exit the program with specified code. + /// + /// For cases where exit is not desired, just let the function return + /// and interpreter will terminate with an error. + fn exit(&mut self, code: i32); +} + +/// Standard [HostInterface] implementation +#[derive(Clone, Copy, Default)] +pub struct Standard; +impl HostInterface for Standard { + type Data = (); + + fn print(&mut self, string: &str, new_line: bool) -> std::io::Result<()> { + use std::io::Write; + + let mut stdout = std::io::stdout(); + stdout.write_all(string.as_bytes())?; + if new_line { + stdout.write_all(b"\n")?; + } + + Ok(()) + } + + fn read_byte(&mut self) -> std::io::Result { + use std::io::Read; + + let mut buf = [0]; + std::io::stdin().read_exact(&mut buf)?; + Ok(buf[0]) + } + + fn exit(&mut self, code: i32) { + std::process::exit(code); + } +} diff --git a/ablescript/src/interpret.rs b/ablescript/src/interpret.rs index d678101..82c3c36 100644 --- a/ablescript/src/interpret.rs +++ b/ablescript/src/interpret.rs @@ -12,20 +12,19 @@ use crate::{ ast::{Assignable, AssignableKind, Block, Expr, Spanned, Stmt}, consts::ablescript_consts, error::{Error, ErrorKind}, + host_interface::HostInterface, value::{Functio, Value, ValueRef, Variable}, }; use rand::random; use std::{ cmp::Ordering, collections::{HashMap, VecDeque}, - io::{stdin, stdout, Read, Write}, mem::take, ops::Range, - process::exit, }; /// An environment for executing AbleScript code. -pub struct ExecEnv { +pub struct ExecEnv { /// The stack, ordered such that `stack[stack.len() - 1]` is the /// top-most (newest) stack frame, and `stack[0]` is the /// bottom-most (oldest) stack frame. @@ -38,6 +37,9 @@ pub struct ExecEnv { /// booleans to facilitate easy manipulation. read_buf: VecDeque, + /// Interface to interact with the host interface + host_interface: H, + /// Vector of blocks to be executed at the end of the program finalisers: Vec, } @@ -49,13 +51,12 @@ struct Scope { variables: HashMap, } -impl Default for ExecEnv { +impl Default for ExecEnv +where + H: Default + HostInterface, +{ fn default() -> Self { - Self { - stack: vec![Default::default()], - read_buf: Default::default(), - finalisers: vec![], - } + Self::with_host_interface(H::default()) } } @@ -85,15 +86,20 @@ enum HaltStatus { /// standard input. pub const READ_BITS: u8 = 3; -impl ExecEnv { +impl ExecEnv { /// Create a new Scope with no predefined variable definitions or /// other information. - pub fn new() -> Self { - Self::default() + pub fn with_host_interface(host_interface: H) -> Self { + Self { + stack: vec![Default::default()], + read_buf: Default::default(), + finalisers: vec![], + host_interface, + } } /// Create a new Scope with predefined variables - pub fn new_with_vars(vars: I) -> Self + pub fn new_with_vars(host_interface: H, vars: I) -> Self where I: IntoIterator, { @@ -103,7 +109,9 @@ impl ExecEnv { Self { stack: vec![scope], - ..Default::default() + read_buf: Default::default(), + finalisers: vec![], + host_interface, } } @@ -226,15 +234,9 @@ impl ExecEnv { match &stmt.item { Stmt::Print { expr, newline } => { let value = self.eval_expr(expr)?; - if *newline { - println!("{value}"); - } else { - print!("{value}"); - stdout() - .lock() - .flush() - .map_err(|e| Error::new(e.into(), stmt.span.clone()))?; - } + self.host_interface + .print(&value.to_string(), *newline) + .map_err(|e| Error::new(e.into(), stmt.span.clone()))?; } Stmt::Dim { ident, init } => { let init = match init { @@ -315,12 +317,17 @@ impl ExecEnv { Stmt::Rlyeh => { // Maybe print a creepy error message or something // here at some point. ~~Alex - exit(random()); + let code = random(); + self.host_interface.exit(code); + return Err(Error::new( + ErrorKind::NonExitingRlyeh(code), + stmt.span.clone(), + )); } Stmt::Rickroll => { - stdout() - .write_all(include_str!("rickroll").as_bytes()) - .expect("Failed to write to stdout"); + self.host_interface + .print(include_str!("rickroll"), false) + .map_err(|e| Error::new(e.into(), stmt.span.clone()))?; } Stmt::Read(assignable) => { let mut value = 0; @@ -442,9 +449,13 @@ impl ExecEnv { span: span.to_owned(), })?; - stdout() - .write_all(&output) - .expect("Failed to write to stdout"); + match String::from_utf8(output) { + Ok(string) => self.host_interface.print(&string, false), + Err(e) => self + .host_interface + .print(&format!("{:?}", e.as_bytes()), true), + } + .map_err(|e| Error::new(e.into(), span.clone()))?; Ok(HaltStatus::Finished) } @@ -537,11 +548,10 @@ impl ExecEnv { const BITS_PER_BYTE: u8 = 8; if self.read_buf.is_empty() { - let mut data = [0]; - stdin().read_exact(&mut data)?; + let byte = self.host_interface.read_byte()?; for n in (0..BITS_PER_BYTE).rev() { - self.read_buf.push_back(((data[0] >> n) & 1) != 0); + self.read_buf.push_back(((byte >> n) & 1) != 0); } } @@ -619,12 +629,15 @@ impl ExecEnv { #[cfg(test)] mod tests { use super::*; - use crate::ast::{Expr, Literal}; + use crate::{ + ast::{Expr, Literal}, + host_interface::Standard, + }; #[test] fn basic_expression_test() { // Check that 2 + 2 = 4. - let env = ExecEnv::new(); + let env = ExecEnv::::default(); assert_eq!( env.eval_expr(&Spanned { item: Expr::BinOp { @@ -650,7 +663,7 @@ mod tests { // The sum of an integer and an aboolean causes an aboolean // coercion. - let env = ExecEnv::new(); + let env = ExecEnv::::default(); assert_eq!( env.eval_expr(&Spanned { item: Expr::BinOp { @@ -675,7 +688,7 @@ mod tests { fn overflow_should_not_panic() { // Integer overflow should throw a recoverable error instead // of panicking. - let env = ExecEnv::new(); + let env = ExecEnv::::default(); assert_eq!( env.eval_expr(&Spanned { item: Expr::BinOp { @@ -719,7 +732,7 @@ mod tests { // From here on out, I'll use this function to parse and run // expressions, because writing out abstract syntax trees by hand // takes forever and is error-prone. - fn eval(env: &mut ExecEnv, src: &str) -> Result { + fn eval(env: &mut ExecEnv, src: &str) -> Result { // We can assume there won't be any syntax errors in the // interpreter tests. let ast = crate::parser::parse(src).unwrap(); @@ -731,7 +744,7 @@ mod tests { // Functions have no return values, so use some // pass-by-reference hacks to detect the correct // functionality. - let mut env = ExecEnv::new(); + let mut env = ExecEnv::::default(); // Declaring and reading from a variable. eval(&mut env, "foo dim 32; bar dim foo + 1;").unwrap(); @@ -765,7 +778,7 @@ mod tests { // Declaration and assignment of variables declared in an `if` // statement should have no effect on those declared outside // of it. - let mut env = ExecEnv::new(); + let mut env = ExecEnv::::default(); eval( &mut env, "foo dim 1; 2 =: foo; unless (never) { foo dim 3; 4 =: foo; }", diff --git a/ablescript/src/lib.rs b/ablescript/src/lib.rs index 6daac14..5b3fab1 100644 --- a/ablescript/src/lib.rs +++ b/ablescript/src/lib.rs @@ -6,6 +6,7 @@ pub mod ast; pub mod error; +pub mod host_interface; pub mod interpret; pub mod parser; pub mod value; diff --git a/ablescript_cli/src/main.rs b/ablescript_cli/src/main.rs index 9d0b276..24e7241 100644 --- a/ablescript_cli/src/main.rs +++ b/ablescript_cli/src/main.rs @@ -46,7 +46,7 @@ fn main() { if ast_print { println!("{:#?}", ast); } - ExecEnv::new().eval_stmts(&ast) + ExecEnv::::default().eval_stmts(&ast) }) { println!( "Error `{:?}` occurred at span: {:?} = `{:?}`", diff --git a/ablescript_cli/src/repl.rs b/ablescript_cli/src/repl.rs index 0ea239f..2aebbcf 100644 --- a/ablescript_cli/src/repl.rs +++ b/ablescript_cli/src/repl.rs @@ -4,7 +4,7 @@ use rustyline::Editor; pub fn repl(ast_print: bool) { let mut rl = Editor::<()>::new(); - let mut env = ExecEnv::new(); + let mut env = ExecEnv::::default(); // If this is `Some`, the user has previously entered an // incomplete statement and is now completing it; otherwise, the