Last active
June 21, 2016 08:20
-
-
Save matklad/c36df9e03e66e5b4bc6521a783fb445c to your computer and use it in GitHub Desktop.
A "let it crash" style REPL skeleton in Rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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