Implement generic REPL

This commit is contained in:
cdo committed 2026-09-26 10:40:32 +01:00
1 parent a756756a9f
commit 69e0edac24
2 files changed
+251

No files matched your search

+1
View File
@@ -6,3 +6,4 @@ pub mod lex;
pub mod parse; pub mod parse;
pub mod print; pub mod print;
pub mod eval; pub mod eval;
pub mod repl;
+250
View File
@@ -0,0 +1,250 @@
use crate::eval::Evaluator;
use std::io::BufRead;
use std::io::Write;
pub struct Repl<I, O, E> {
input: I,
output: O,
evaluator: E,
}
impl<I, O, E> Repl<std::io::BufReader<I>, std::io::BufWriter<O>, E>
where
I: std::io::Read,
O: std::io::Write,
E: Evaluator,
E::Expr: std::fmt::Display + std::str::FromStr,
E::Error: std::fmt::Display,
<E::Expr as std::str::FromStr>::Err: std::fmt::Display
{
pub fn new(input: I, output: O, evaluator: E) -> Self {
Self {
input: std::io::BufReader::new(input),
output: std::io::BufWriter::new(output),
evaluator,
}
}
pub fn run(&mut self) -> std::io::Result<()> {
let mut line = String::new();
loop {
write!(self.output, "> ")?;
self.output.flush()?;
line.clear();
match self.input.read_line(&mut line)? {
0 => {
writeln!(self.output, "")?;
self.output.flush()?;
break Ok(())
}
_ => self.handle(&line)?,
}
}
}
fn handle(&mut self, line: &str) -> std::io::Result<()> {
match line.parse::<E::Expr>() {
Ok(input) => {
match self.evaluator.eval(input) {
Ok(result) => self.print_expr(&result)?,
Err(error) => self.print_error(&error)?,
}
}
Err(error) => self.print_error(&error)?,
}
Ok(())
}
fn print_expr(&mut self, expr: &E::Expr) -> std::io::Result<()> {
writeln!(self.output, "{}", expr)
}
fn print_error<T: std::fmt::Display>(
&mut self, error: &T) -> Result<(), std::io::Error>
{
writeln!(self.output, "ERROR: {}", error)
}
}
#[cfg(test)]
mod tests {
use super::*;
enum Expr {
Foo,
Bar,
}
enum Error {
Read,
Eval,
}
struct Eval;
struct ErrorReader;
impl std::str::FromStr for Expr {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"foo" => Ok(Expr::Foo),
"bar" => Ok(Expr::Bar),
_ => Err(Error::Read),
}
}
}
impl std::fmt::Display for Expr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expr::Foo => write!(f, "foo"),
Expr::Bar => write!(f, "bar"),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Read => write!(f, "read"),
Error::Eval => write!(f, "eval"),
}
}
}
impl Evaluator for Eval {
type Expr = Expr;
type Error = Error;
fn eval(&mut self, input: Expr) -> Result<Expr, Error> {
match input {
Expr::Foo => Ok(Expr::Bar),
_ => Err(Error::Eval),
}
}
}
impl ErrorReader {
pub fn error() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, "test")
}
}
impl std::io::Read for ErrorReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(ErrorReader::error())
}
}
#[test]
fn repl_prints_prompt() {
let reader = std::io::Cursor::new(b"");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
{
let mut repl = Repl::new(reader, &mut writer, evaluator);
repl.run().unwrap();
}
let output = String::from_utf8(
writer.into_inner().unwrap()).unwrap();
assert_eq!(output, "> \n");
}
#[test]
fn repl_reads_expression_evals_and_prints_result() {
let reader = std::io::Cursor::new(b"foo\n");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
{
let mut repl = Repl::new(reader, &mut writer, evaluator);
repl.run().unwrap();
}
let output = String::from_utf8(
writer.into_inner().unwrap()).unwrap();
assert_eq!(output, "> bar\n> \n");
}
#[test]
fn read_errors_are_reported() {
let reader = std::io::Cursor::new(b"baz\n");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
{
let mut repl = Repl::new(reader, &mut writer, evaluator);
repl.run().unwrap();
}
let output = String::from_utf8(
writer.into_inner().unwrap()).unwrap();
assert_eq!(output, "> ERROR: read\n> \n");
}
#[test]
fn eval_errors_are_reported() {
let reader = std::io::Cursor::new(b"bar\n");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
{
let mut repl = Repl::new(reader, &mut writer, evaluator);
repl.run().unwrap();
}
let output = String::from_utf8(
writer.into_inner().unwrap()).unwrap();
assert_eq!(output, "> ERROR: eval\n> \n");
}
#[test]
fn interaction_can_have_many_lines() {
let reader = std::io::Cursor::new(b"foo\nfoo\nfoo\n");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
{
let mut repl = Repl::new(reader, &mut writer, evaluator);
repl.run().unwrap();
}
let output = String::from_utf8(
writer.into_inner().unwrap()).unwrap();
assert_eq!(output, "> bar\n> bar\n> bar\n> \n");
}
#[test]
fn errors_are_recovered_from() {
let reader = std::io::Cursor::new(b"foo\nbar\nfoo\nbaz\nfoo\n");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
{
let mut repl = Repl::new(reader, &mut writer, evaluator);
repl.run().unwrap();
}
let output = String::from_utf8(
writer.into_inner().unwrap()).unwrap();
assert_eq!(
output,
"> bar\n> ERROR: eval\n> bar\n> ERROR: read\n> bar\n> \n");
}
#[test]
fn run_returns_ok_when_exiting_normally()
{
let reader = std::io::Cursor::new(b"");
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
let mut repl = Repl::new(reader, &mut writer, evaluator);
let result = repl.run();
assert!(matches!(result, Ok(())));
}
#[test]
fn run_returns_error_when_exiting_abnormally()
{
let reader = ErrorReader {};
let mut writer = std::io::BufWriter::new(Vec::new());
let evaluator = Eval {};
let mut repl = Repl::new(reader, &mut writer, evaluator);
let result = repl.run();
assert!(matches!(result, Err(_)));
}
}