Skip to content

Instantly share code, notes, and snippets.

@matklad
Last active June 21, 2016 08:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matklad/c36df9e03e66e5b4bc6521a783fb445c to your computer and use it in GitHub Desktop.
Save matklad/c36df9e03e66e5b4bc6521a783fb445c to your computer and use it in GitHub Desktop.
A "let it crash" style REPL skeleton in Rust
use std::thread;
use std::io::prelude::*;
/// This is an attempt to write a REPL application in
/// Erlangish "let it crash" style.
///
/// References:
/// - http://ferd.ca/the-zen-of-erlang.html
/// - http://joeduffyblog.com/2016/02/07/the-error-model/
///
/// We will use OS threads as a failure boundary. Although Rust currently
/// lacks Erlang style lightweight processes, OS threads should be OK as
/// long as their number is determined by the application architecture, and
/// not by the load.
///
/// That is, using one thread for each "service" is OK, because there is a
/// constant number of services. In contrast, using one thread per client
/// is not the most scalable solution.
/// This our main thread. Its job is to launch the repl thread
/// and print an error message if it fails.
fn main() {
if let Err(_) = thread::spawn(repl).join() {
// Here we just terminate the program. Another option
// is to respawn the repl thread.
println!("Unexpected error :(");
}
//TODO: set exit code properly
}
/// This is the repl thread. It spawns a single interpreter thread
/// and acts as a middleman between the user and the interpreter.
/// If the interpreter dies, it gets restarted.
/// If the user dies, hm, let's ignore this case for now.
fn repl() {
println!("gp v0.0.1");
let mut gp = interpreter::spawn();
let mut buffer = String::new();
loop {
print!("> ");
// This unwrap is not a sloppy code, but is pretty intentional.
// If stdout fails, then something is terribly wrong, so it seems
// reasonable to kill the thread.
std::io::stdout().flush().unwrap();
// I forgot this call initially, and unfortunately it was
// that kind of bug which messes up the state, but does not
// cause a panic :(
buffer.clear();
// Again, this unwrap is intentional.
std::io::stdin().read_line(&mut buffer).unwrap();
let input = buffer.trim();
if input == "q" {
gp.terminate();
break;
}
let result = match gp.evaluate(input.to_string()) {
Err(interpreter::Dead) => {
println!("Unexpected error, restarting");
// Interpreter is stateless, so restarting is really transparent.
// For the stateful interpreter, it should be possible to add an
// initial state as a parameter of the `spawn` function and
// implement the `get_state` request to allow for checkpoints.
gp = interpreter::spawn();
continue;
}
Ok(result) => result
};
match result {
Err(interpreter::SyntaxError) => println!("Syntax error"),
Ok(value) => println!("{}", value),
}
}
println!("Bye! o/");
}
/// The module which encapsulates the interpreter thread.
mod interpreter {
// We want bidirectional communication with the interpreter,
// so these imports will be handy
use std::sync::mpsc::{channel, Sender, Receiver};
use std::thread;
/// The handler to the interpreter thread.
/// See `Interpreter::spawn` function for the main loop.
pub struct Interpreter {
req: Sender<Request>,
res: Receiver<Response>,
}
pub type Value = i64;
pub struct Dead;
pub struct SyntaxError;
impl Interpreter {
/// This function synchronously evaluates `code`.
/// It communicates with another thread, and returns `Dead`
/// if it doesn't answer back.
pub fn evaluate(&self, code: String) -> Result<Result<Value, SyntaxError>, Dead> {
if let Err(_) = self.req.send(Request::Evaluate(code)) {
// This is actually to optimistic. We know that the other side
// of the channel is closed, but we don't know for sure that
// the thread is really dead. It could've just dropped the channel!
// This is a good place to plug in a library which would provide the
// main loop and enforce proper termination.
//
// But no library can help us if the thread is stuck in the
// infinite loop :(
return Err(Dead);
}
self.res.recv().map_err(|_| Dead)
}
pub fn terminate(self) {
// Again, this is a best effort termination, the
// other side can just ignore our request, and we
// can't do anything about it.
//
// TODO: implement `Drop` for interpreter anc call terminate there
// to make sure that if supervising thread dies, then the child thread
// is also teared down.
let _ignore = self.req.send(Request::Terminate);
}
}
pub fn spawn() -> Interpreter {
// `tx` is transmitter,
// `rx` is receiver.
let (req_tx, req_rx) = channel();
let (res_tx, res_rx) = channel();
thread::spawn(move || {
loop {
// And again, we can safely `unwrap` here, because
// we are talking to our parent and if it is dead,
// we'd better die as well.
let req = req_rx.recv().unwrap();
match req {
Request::Terminate => return,
Request::Evaluate(code) => {
res_tx.send(eval(&code)).unwrap();
}
}
}
});
Interpreter {
req: req_tx,
res: res_rx
}
}
enum Request {
Evaluate(String),
Terminate
}
type Response = Result<Value, SyntaxError>;
/// And this is the actual logic of our interpreter,
/// which, as always happens with logic, works for
/// the majority of cases, but sometimes panics.
///
/// Can you spot an error? :)
fn eval(code: &str) -> Result<Value, SyntaxError> {
if code.chars().all(|c| c.is_digit(10)) {
Ok(code.parse::<i64>().unwrap())
} else {
Err(SyntaxError)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment