From 8389b0674be968ae5ccb728e53a838675970c9c7 Mon Sep 17 00:00:00 2001 From: Kurt Lawrence Date: Tue, 17 Dec 2019 21:52:08 +1100 Subject: [PATCH 1/4] added auto format to test code --- src/linking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linking.rs b/src/linking.rs index ae25595..9ef478e 100644 --- a/src/linking.rs +++ b/src/linking.rs @@ -147,7 +147,7 @@ //! Papyrus uses `AssertUnwindSafe` wrappers to make this work, however it makes `app_data` vulnerable to breaking //! invariant states if a panic is triggered. //! -//! The developer should keep this in mind when implementing a linked REPL. +//! The developer should keep this in mind when implementing a linked REPL. //! Some guidelines: //! //! 1. Keep the app_data that is being transfered simple. From 873c1c3e56a4a5890ebcbbbb293c31409f9b6a9a Mon Sep 17 00:00:00 2001 From: Kurt Lawrence Date: Wed, 18 Dec 2019 13:26:46 +1100 Subject: [PATCH 2/4] Add library store --- docs/modocs/repl.md | Bin 1029 -> 0 bytes docs/src/SUMMARY.md | 1 - docs/src/api.repl.md | 4 - docs/src/chapter_1.md | 1 - modoc.config | 1 - src/compile/execute.rs | 6 +- src/compile/mod.rs | 8 +- src/repl/data.rs | 2 + src/repl/eval.rs | 908 +++++++++++++++++++++-------------------- src/repl/mod.rs | Bin 6273 -> 6144 bytes 10 files changed, 482 insertions(+), 449 deletions(-) delete mode 100644 docs/modocs/repl.md delete mode 100644 docs/src/api.repl.md delete mode 100644 docs/src/chapter_1.md diff --git a/docs/modocs/repl.md b/docs/modocs/repl.md deleted file mode 100644 index 39c8b0d353838f9a0a474c7a43a9083158d34ced..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1029 ScmZQz7zLvtFd70QIs^a&1pom6 diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 0c876dd..151e912 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -5,5 +5,4 @@ - [Commands](api.cmds.md) - [Output](api.output.md) - [Linking](api.linking.md) - - [REPL](api.repl.md) diff --git a/docs/src/api.repl.md b/docs/src/api.repl.md deleted file mode 100644 index 7f44770..0000000 --- a/docs/src/api.repl.md +++ /dev/null @@ -1,4 +0,0 @@ -# REPL - -{{#include ../modocs/repl.md}} - diff --git a/docs/src/chapter_1.md b/docs/src/chapter_1.md deleted file mode 100644 index b743fda..0000000 --- a/docs/src/chapter_1.md +++ /dev/null @@ -1 +0,0 @@ -# Chapter 1 diff --git a/modoc.config b/modoc.config index c3f1f6f..70c6f77 100644 --- a/modoc.config +++ b/modoc.config @@ -2,5 +2,4 @@ "docs/modocs/cmds.md" = [ "src/cmds/mod.rs" ] "docs/modocs/code.md" = [ "src/code.rs" ] "docs/modocs/linking.md" = [ "src/linking.rs" ] -"docs/modocs/repl.md" = [ "src/repl/mod.rs" ] "docs/modocs/output.md" = [ "src/output/mod.rs" ] diff --git a/src/compile/execute.rs b/src/compile/execute.rs index 58d294f..b2f1d54 100644 --- a/src/compile/execute.rs +++ b/src/compile/execute.rs @@ -7,7 +7,7 @@ use std::path::Path; /// function signature! type DataFunc = unsafe fn(D) -> Kserd<'static>; -type ExecResult = Result, &'static str>; +type ExecResult = Result<(Kserd<'static>, Library), &'static str>; pub(crate) fn exec, D, W: Write + Send>( library_file: P, @@ -33,7 +33,7 @@ fn exec_no_redirect, Data>( let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { func(app_data) })); match res { - Ok(kserd) => Ok(kserd), + Ok(kserd) => Ok((kserd, lib)), Err(_) => Err("a panic occured with evaluation"), } } @@ -74,7 +74,7 @@ fn exec_and_redirect, Data, W: Write + Send>( let res = res.map_err(|_| "crossbeam scoping failed")?; match res { - Ok(kserd) => Ok(kserd), + Ok(kserd) => Ok((kserd, lib)), Err(_) => Err("a panic occured with evaluation"), } } diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 6da7058..2179c48 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -38,7 +38,7 @@ mod tests { // eval let r = exec::<_, _, std::io::Sink>(path, "_lib_intern_eval", &(), None).unwrap(); // execute library fn - assert_eq!(r, Kserd::new_num(4)); + assert_eq!(r.0, Kserd::new_num(4)); } #[test] @@ -63,7 +63,7 @@ mod tests { // eval let r = exec::<_, _, std::io::Sink>(path, "_lib_intern_eval", &(), None).unwrap(); // execute library fn - assert_eq!(r, Kserd::new_num(4)); + assert_eq!(r.0, Kserd::new_num(4)); } #[test] @@ -94,7 +94,7 @@ mod tests { // eval let r = exec::<_, _, std::io::Sink>(path, "_lib_intern_eval", &(), None).unwrap(); // execute library fn - assert_eq!(r, Kserd::new_num(4)); + assert_eq!(r.0, Kserd::new_num(4)); } #[test] @@ -125,7 +125,7 @@ mod tests { // eval let r = exec(path, "_lib_intern_eval", &(), Some(&mut std::io::sink())).unwrap(); // execute library fn - assert_eq!(r, Kserd::new_num(4)); + assert_eq!(r.0, Kserd::new_num(4)); } #[test] diff --git a/src/repl/data.rs b/src/repl/data.rs index dc7685e..6c02f5b 100644 --- a/src/repl/data.rs +++ b/src/repl/data.rs @@ -20,6 +20,8 @@ impl Default for ReplData { redirect_on_execution: true, editing: None, editing_src: None, + loadedlibs: VecDeque::new(), + loaded_libs_size_limit: 0, }; r.with_cmdtree_builder(Builder::new("papyrus")) diff --git a/src/repl/eval.rs b/src/repl/eval.rs index 55c61d3..69214ea 100644 --- a/src/repl/eval.rs +++ b/src/repl/eval.rs @@ -1,435 +1,473 @@ -use super::*; -use crate::{ - cmds::{self, CommandResult}, - code::{self, Input, SourceCode, StmtGrp}, - compile, -}; -use std::borrow::{Borrow, BorrowMut}; -use std::ops::{Deref, DerefMut}; -use std::path::Path; -use std::sync::{Arc, Mutex}; - -impl Repl { - /// Evaluates the read input, compiling and executing the code and printing all line prints until - /// a result is found. This result gets passed back as a print ready repl. - pub fn eval(self, app_data: &mut D) -> EvalResult { - use std::cell::Cell; - use std::rc::Rc; - - let ptr = Rc::into_raw(Rc::new(app_data)); - - // as I am playing around with pointers here, I am going to do assertions in the rebuilding - // if from_raw is called more than once, it is memory unsafe, so count the calls and assert it is only 1 - let rebuilds: Rc> = Rc::new(Cell::new(0)); - - let func = || { - let b = Rc::clone(&rebuilds); - - let n = b.get(); - - assert_eq!(n, 0, "unsafe memory operation, can only rebuild Rc once."); - - b.set(n + 1); - - let c = unsafe { Rc::from_raw(ptr) }; - - Rc::try_unwrap(c) - .map_err(|_| "there should only be one strong reference") - .unwrap() - }; - - map_variants(self, func, func) - } - - /// Begin listening to line change events on the output. - pub fn output_listen(&mut self) -> output::Receiver { - self.state.output.listen() - } - - /// Close the sender side of the output channel. - pub fn close_channel(&mut self) { - self.state.output.close() - } - - /// The current output. - /// - /// The output contains colouring ANSI escape codes, the prompt, and all input. - pub fn output(&self) -> &str { - self.state.output.buffer() - } -} - -impl Repl { - /// Same as `eval` but will evaluate on another thread, not blocking this one. - /// - /// An `Arc::clone` will be taken of `app_data`. - pub fn eval_async(self, app_data: &Arc>) -> Evaluating { - let (tx, rx) = crossbeam_channel::bounded(1); - - let clone = Arc::clone(app_data); - - std::thread::spawn(move || { - let eval = map_variants( - self, - || clone.lock().expect("failed getting lock of data"), - || clone.lock().expect("failed getting lock of data"), - ); - - tx.send(eval).unwrap(); - }); - - Evaluating { jh: rx } - } -} - -impl Evaluating { - /// Evaluating has finished. - pub fn completed(&self) -> bool { - !self.jh.is_empty() - } - - /// Waits for the evaluating to finish before return the result. - /// If evaluating is `completed` this will return immediately. - pub fn wait(self) -> EvalResult { - self.jh - .recv() - .expect("receiving eval result from async thread failed") - } -} - -fn map_variants( - repl: Repl, - obtain_mut_data: Fmut, - obtain_brw_data: Fbrw, -) -> EvalResult -where - Fmut: FnOnce() -> Rmut, - Rmut: DerefMut, - Fbrw: FnOnce() -> Rbrw, - Rbrw: Deref, -{ - let Repl { - state, - mut data, - more, - data_mrker, - } = repl; - - let Evaluate { mut output, result } = state; - - let mut keep_mutating = false; // default to stop mutating phase - // can't cancel before as handle program requires it for decisions - - // map variants into Result - let mapped = match result { - InputResult::Command(cmds) => { - let r = data.handle_command(&cmds, &mut output, obtain_mut_data); - keep_mutating = data.linking.mutable; // a command can alter the mutating state, needs to persist - r.map(|x| EvalOutput::Print(x)) - } - InputResult::Program(input) => { - Ok(data.handle_program(input, &mut output, obtain_mut_data, obtain_brw_data)) - } - InputResult::InputError(err) => Ok(EvalOutput::Print(Cow::Owned(err))), - InputResult::Eof => Err(Signal::Exit), - _ => Ok(EvalOutput::Print(Cow::Borrowed(""))), - }; - - let (eval_output, sig) = match mapped { - Ok(hir) => (hir, Signal::None), - Err(sig) => (EvalOutput::Print(Cow::Borrowed("")), sig), - }; - - data.linking.mutable = keep_mutating; // always cancel a mutating block on evaluation?? - // the alternative would be to keep alive on compilation failures, might not for now though. - // this would have to be individually handled in each match arm and it, rather let the user - // have to reinstate mutability if they fuck up input. - - EvalResult { - signal: sig, - repl: Repl { - state: Print { - output, - data: eval_output, - }, - data, - more, - data_mrker, - }, - } -} - -impl ReplData { - fn handle_command( - &mut self, - cmds: &str, - writer: &mut W, - obtain_mut_app_data: F, - ) -> Result, Signal> - where - F: FnOnce() -> R, - R: DerefMut, - W: io::Write, - { - use cmdtree::LineResult as lr; - - let tuple = match self.cmdtree.parse_line(cmds, true, writer) { - lr::Exit => return Err(Signal::Exit), - lr::Cancel => { - self.linking.mutable = false; // reset the mutating on cancel - self.editing = None; // reset the editing on cancel - Cow::Borrowed("cancelled input and returned to root") - } - lr::Action(res) => match res { - CommandResult::BeginMutBlock => { - self.linking.mutable = true; - Cow::Borrowed("beginning mut block") - } - CommandResult::EditAlter(ei) => Cow::Borrowed(cmds::edit_alter(self, ei)), - CommandResult::EditReplace(ei, val) => { - let r = Cow::Borrowed(cmds::edit_alter(self, ei)); - - if r.is_empty() { - return Err(Signal::ReEvaluate(val)); - } else { - r - } - } - CommandResult::SwitchModule(path) => { - Cow::Borrowed(crate::cmds::switch_module(self, &path)) - } - - CommandResult::ActionOnReplData(action) => Cow::Owned(action(self, writer)), - CommandResult::ActionOnAppData(action) => { - let mut r = obtain_mut_app_data(); - let app_data: &mut D = r.borrow_mut(); - let s = action(app_data, writer); - Cow::Owned(s) - } - CommandResult::Empty => Cow::Borrowed(""), - }, - _ => Cow::Borrowed(""), - }; - - Ok(tuple) - } - - fn handle_program( - &mut self, - mut input: Input, - writer: &mut Output, - obtain_mut_data: Fmut, - obtain_brw_data: Fbrw, - ) -> EvalOutput - where - Fmut: FnOnce() -> Rmut, - Rmut: DerefMut, - Fbrw: FnOnce() -> Rbrw, - Rbrw: Deref, - { - let (nitems, ncrates) = (input.items.len(), input.crates.len()); - - let has_stmts = input.stmts.len() > 0; - - let (lstmts, litem, lcrates) = { - let src = self.current_src(); - (src.stmts.len(), src.items.len(), src.crates.len()) - }; - - let mut undo = true; - - let (stmt_idx, item_idx, crate_idx) = if let Some(ei) = self.editing.take() { - let src = self.get_current_file_mut(); // remove at the index - // then insert, so - // acts like replace - - undo = false; - - match ei.editing { - // we clear the edits if the indices fall outside the bounds - Editing::Stmt => { - if ei.index >= lstmts { - input.stmts.clear(); - } else { - src.stmts.remove(ei.index); - } - - (ei.index, litem, lcrates) - } - Editing::Item => { - if ei.index >= litem { - input.items.clear(); - } else { - src.items.remove(ei.index); - } - - (lstmts, ei.index, lcrates) - } - Editing::Crate => { - if ei.index >= lcrates { - input.crates.clear(); - } else { - src.crates.remove(ei.index); - } - - (lstmts, litem, ei.index) - } - } - } else { - (lstmts, litem, lcrates) - }; - - self.insert_input(input, stmt_idx, item_idx, crate_idx); - - let maybe_pop_input = |repl_data: &mut ReplData| { - if undo { - let src = repl_data.get_current_file_mut(); - - if has_stmts { - src.stmts.remove(stmt_idx); - } - - for _ in 0..nitems { - src.items.remove(item_idx); - } - - for _ in 0..ncrates { - src.crates.remove(crate_idx); - } - } - }; - - // build directory - let res = compile::build_compile_dir(&self.compilation_dir, &self.mods_map, &self.linking); - if let Err(e) = res { - maybe_pop_input(self); // failed so don't save - return EvalOutput::Print(Cow::Owned(format!( - "failed to build compile directory: {}", - e - ))); - } - - // compile - let lib_file = compile::compile(&self.compilation_dir, &self.linking, |line| { - writer.erase_last_line(); - writer.write_str(line); - }); - - writer.erase_last_line(); - - let lib_file = match lib_file { - Ok(f) => f, - Err(e) => { - maybe_pop_input(self); // failed so don't save - return EvalOutput::Print(Cow::Owned(format!("{}", e))); - } - }; - - if has_stmts { - // execute - let exec_res = { - // Has to be done to make linux builds work - // see: - // https://github.com/nagisa/rust_libloading/issues/5 - // https://github.com/nagisa/rust_libloading/issues/41 - // https://github.com/nagisa/rust_libloading/issues/49 - // - // Basically the api function `dlopen` will keep loaded libraries in memory to avoid - // continuously allocating memory. It only does not release the library when thread_local data - // is hanging around, and it seems `println!()` is something that does this. - // Hence to avoid not having the library not updated with a new `new()` call, a different lib - // name is passed to the function. - // This is very annoying as it has needless fs interactions and a growing fs footprint but - // what can you do ¯\_(ツ)_/¯ - let lib_file = rename_lib_file(lib_file).expect("failed renaming library file"); - - // FIXME If removing the true this won't work through terminals - // Need to trial it in linux though as I remember it breaking a lot... - // If it is all good, I should be able to just redirect _all_ output - let redirect_wtr = if self.redirect_on_execution { - Some(writer) - } else { - None - }; - - let mut fn_name = String::new(); - code::eval_fn_name(&code::into_mod_path_vec(self.current_mod()), &mut fn_name); - - if self.linking.mutable { - let mut r = obtain_mut_data(); - let app_data: &mut D = r.borrow_mut(); - compile::exec(&lib_file, &fn_name, app_data, redirect_wtr) - } else { - let r = obtain_brw_data(); - let app_data: &D = r.borrow(); - compile::exec(&lib_file, &fn_name, app_data, redirect_wtr) - } - }; - match exec_res { - Ok(s) => { - if self.linking.mutable { - maybe_pop_input(self); // don't save mutating inputs - EvalOutput::Print(Cow::Owned(format!("finished mutating block: {}", s))) - // don't print as `out#` - } else { - EvalOutput::Data(s) - } - } - Err(e) => { - maybe_pop_input(self); // failed so don't save - EvalOutput::Print(Cow::Borrowed(e)) - } - } - } else { - // this will keep inputs, might not be preferrable to do so in mutating state? - EvalOutput::Print(Cow::Borrowed("")) // do not execute if no extra statements have been added - } - } - - fn insert_input(&mut self, input: Input, stmt_idx: usize, item_idx: usize, crate_idx: usize) { - let Input { - items, - crates, - stmts, - } = input; - - let src = self.get_current_file_mut(); - - if !stmts.is_empty() { - src.stmts.insert(stmt_idx, StmtGrp(stmts)); - } - - for item in items.into_iter().rev() { - src.items.insert(item_idx, item); - } - - for cr in crates.into_iter().rev() { - src.crates.insert(crate_idx, cr); - } - } - - fn get_current_file_mut(&mut self) -> &mut SourceCode { - self.mods_map.get_mut(&self.current_mod).expect(&format!( - "file map does not have key: {}", - self.current_mod.display() - )) - } -} - -/// Renames the library into a distinct file name by incrementing a counter. -/// Could fail if the number of libs grows enormous, greater than `u64`. This would mean, with -/// `u64 = 18,446,744,073,709,551,615`, even with 1KB files (prolly not) this would be -/// 18,446,744,073 TB. User will probably know something is up. -fn rename_lib_file>(compiled_lib: P) -> io::Result { - let no_parent = PathBuf::new(); - let mut idx: u64 = 0; - let parent = compiled_lib.as_ref().parent().unwrap_or(&no_parent); - let name = |i| format!("papyrus.mem-code.lib.{}", i); - let mut lib_path = parent.join(&name(idx)); - while lib_path.exists() { - idx += 1; - lib_path = parent.join(&name(idx)); - } - std::fs::rename(&compiled_lib, &lib_path)?; - Ok(lib_path) -} +use super::*; +use crate::{ + cmds::{self, CommandResult}, + code::{self, Input, SourceCode, StmtGrp}, + compile, +}; +use std::borrow::{Borrow, BorrowMut}; +use std::ops::{Deref, DerefMut}; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +impl Repl { + /// Evaluates the read input, compiling and executing the code and printing all line prints until + /// a result is found. This result gets passed back as a print ready repl. + pub fn eval(self, app_data: &mut D) -> EvalResult { + use std::cell::Cell; + use std::rc::Rc; + + let ptr = Rc::into_raw(Rc::new(app_data)); + + // as I am playing around with pointers here, I am going to do assertions in the rebuilding + // if from_raw is called more than once, it is memory unsafe, so count the calls and assert it is only 1 + let rebuilds: Rc> = Rc::new(Cell::new(0)); + + let func = || { + let b = Rc::clone(&rebuilds); + + let n = b.get(); + + assert_eq!(n, 0, "unsafe memory operation, can only rebuild Rc once."); + + b.set(n + 1); + + let c = unsafe { Rc::from_raw(ptr) }; + + Rc::try_unwrap(c) + .map_err(|_| "there should only be one strong reference") + .unwrap() + }; + + map_variants(self, func, func) + } + + /// Begin listening to line change events on the output. + pub fn output_listen(&mut self) -> output::Receiver { + self.state.output.listen() + } + + /// Close the sender side of the output channel. + pub fn close_channel(&mut self) { + self.state.output.close() + } + + /// The current output. + /// + /// The output contains colouring ANSI escape codes, the prompt, and all input. + pub fn output(&self) -> &str { + self.state.output.buffer() + } +} + +impl Repl { + /// Same as `eval` but will evaluate on another thread, not blocking this one. + /// + /// An `Arc::clone` will be taken of `app_data`. + pub fn eval_async(self, app_data: &Arc>) -> Evaluating { + let (tx, rx) = crossbeam_channel::bounded(1); + + let clone = Arc::clone(app_data); + + std::thread::spawn(move || { + let eval = map_variants( + self, + || clone.lock().expect("failed getting lock of data"), + || clone.lock().expect("failed getting lock of data"), + ); + + tx.send(eval).unwrap(); + }); + + Evaluating { jh: rx } + } +} + +impl Evaluating { + /// Evaluating has finished. + pub fn completed(&self) -> bool { + !self.jh.is_empty() + } + + /// Waits for the evaluating to finish before return the result. + /// If evaluating is `completed` this will return immediately. + pub fn wait(self) -> EvalResult { + self.jh + .recv() + .expect("receiving eval result from async thread failed") + } +} + +fn map_variants( + repl: Repl, + obtain_mut_data: Fmut, + obtain_brw_data: Fbrw, +) -> EvalResult +where + Fmut: FnOnce() -> Rmut, + Rmut: DerefMut, + Fbrw: FnOnce() -> Rbrw, + Rbrw: Deref, +{ + let Repl { + state, + mut data, + more, + data_mrker, + } = repl; + + let Evaluate { mut output, result } = state; + + let mut keep_mutating = false; // default to stop mutating phase + // can't cancel before as handle program requires it for decisions + + // map variants into Result + let mapped = match result { + InputResult::Command(cmds) => { + let r = data.handle_command(&cmds, &mut output, obtain_mut_data); + keep_mutating = data.linking.mutable; // a command can alter the mutating state, needs to persist + r.map(|x| EvalOutput::Print(x)) + } + InputResult::Program(input) => { + Ok(data.handle_program(input, &mut output, obtain_mut_data, obtain_brw_data)) + } + InputResult::InputError(err) => Ok(EvalOutput::Print(Cow::Owned(err))), + InputResult::Eof => Err(Signal::Exit), + _ => Ok(EvalOutput::Print(Cow::Borrowed(""))), + }; + + let (eval_output, sig) = match mapped { + Ok(hir) => (hir, Signal::None), + Err(sig) => (EvalOutput::Print(Cow::Borrowed("")), sig), + }; + + data.linking.mutable = keep_mutating; // always cancel a mutating block on evaluation?? + // the alternative would be to keep alive on compilation failures, might not for now though. + // this would have to be individually handled in each match arm and it, rather let the user + // have to reinstate mutability if they fuck up input. + + EvalResult { + signal: sig, + repl: Repl { + state: Print { + output, + data: eval_output, + }, + data, + more, + data_mrker, + }, + } +} + +impl ReplData { + fn handle_command( + &mut self, + cmds: &str, + writer: &mut W, + obtain_mut_app_data: F, + ) -> Result, Signal> + where + F: FnOnce() -> R, + R: DerefMut, + W: io::Write, + { + use cmdtree::LineResult as lr; + + let tuple = match self.cmdtree.parse_line(cmds, true, writer) { + lr::Exit => return Err(Signal::Exit), + lr::Cancel => { + self.linking.mutable = false; // reset the mutating on cancel + self.editing = None; // reset the editing on cancel + Cow::Borrowed("cancelled input and returned to root") + } + lr::Action(res) => match res { + CommandResult::BeginMutBlock => { + self.linking.mutable = true; + Cow::Borrowed("beginning mut block") + } + CommandResult::EditAlter(ei) => Cow::Borrowed(cmds::edit_alter(self, ei)), + CommandResult::EditReplace(ei, val) => { + let r = Cow::Borrowed(cmds::edit_alter(self, ei)); + + if r.is_empty() { + return Err(Signal::ReEvaluate(val)); + } else { + r + } + } + CommandResult::SwitchModule(path) => { + Cow::Borrowed(crate::cmds::switch_module(self, &path)) + } + + CommandResult::ActionOnReplData(action) => Cow::Owned(action(self, writer)), + CommandResult::ActionOnAppData(action) => { + let mut r = obtain_mut_app_data(); + let app_data: &mut D = r.borrow_mut(); + let s = action(app_data, writer); + Cow::Owned(s) + } + CommandResult::Empty => Cow::Borrowed(""), + }, + _ => Cow::Borrowed(""), + }; + + Ok(tuple) + } + + fn handle_program( + &mut self, + mut input: Input, + writer: &mut Output, + obtain_mut_data: Fmut, + obtain_brw_data: Fbrw, + ) -> EvalOutput + where + Fmut: FnOnce() -> Rmut, + Rmut: DerefMut, + Fbrw: FnOnce() -> Rbrw, + Rbrw: Deref, + { + let (nitems, ncrates) = (input.items.len(), input.crates.len()); + + let has_stmts = input.stmts.len() > 0; + + let (lstmts, litem, lcrates) = { + let src = self.current_src(); + (src.stmts.len(), src.items.len(), src.crates.len()) + }; + + let mut undo = true; + + let (stmt_idx, item_idx, crate_idx) = if let Some(ei) = self.editing.take() { + let src = self.get_current_file_mut(); // remove at the index + // then insert, so + // acts like replace + + undo = false; + + match ei.editing { + // we clear the edits if the indices fall outside the bounds + Editing::Stmt => { + if ei.index >= lstmts { + input.stmts.clear(); + } else { + src.stmts.remove(ei.index); + } + + (ei.index, litem, lcrates) + } + Editing::Item => { + if ei.index >= litem { + input.items.clear(); + } else { + src.items.remove(ei.index); + } + + (lstmts, ei.index, lcrates) + } + Editing::Crate => { + if ei.index >= lcrates { + input.crates.clear(); + } else { + src.crates.remove(ei.index); + } + + (lstmts, litem, ei.index) + } + } + } else { + (lstmts, litem, lcrates) + }; + + self.insert_input(input, stmt_idx, item_idx, crate_idx); + + let maybe_pop_input = |repl_data: &mut ReplData| { + if undo { + let src = repl_data.get_current_file_mut(); + + if has_stmts { + src.stmts.remove(stmt_idx); + } + + for _ in 0..nitems { + src.items.remove(item_idx); + } + + for _ in 0..ncrates { + src.crates.remove(crate_idx); + } + } + }; + + // build directory + let res = compile::build_compile_dir(&self.compilation_dir, &self.mods_map, &self.linking); + if let Err(e) = res { + maybe_pop_input(self); // failed so don't save + return EvalOutput::Print(Cow::Owned(format!( + "failed to build compile directory: {}", + e + ))); + } + + // compile + let lib_file = compile::compile(&self.compilation_dir, &self.linking, |line| { + writer.erase_last_line(); + writer.write_str(line); + }); + + writer.erase_last_line(); + + let lib_file = match lib_file { + Ok(f) => f, + Err(e) => { + maybe_pop_input(self); // failed so don't save + return EvalOutput::Print(Cow::Owned(format!("{}", e))); + } + }; + + if has_stmts { + // execute + let exec_res = { + // Has to be done to make linux builds work + // see: + // https://github.com/nagisa/rust_libloading/issues/5 + // https://github.com/nagisa/rust_libloading/issues/41 + // https://github.com/nagisa/rust_libloading/issues/49 + // + // Basically the api function `dlopen` will keep loaded libraries in memory to avoid + // continuously allocating memory. It only does not release the library when thread_local data + // is hanging around, and it seems `println!()` is something that does this. + // Hence to avoid not having the library not updated with a new `new()` call, a different lib + // name is passed to the function. + // This is very annoying as it has needless fs interactions and a growing fs footprint but + // what can you do ¯\_(ツ)_/¯ + let lib_file = rename_lib_file(lib_file).expect("failed renaming library file"); + + // FIXME If removing the true this won't work through terminals + // Need to trial it in linux though as I remember it breaking a lot... + // If it is all good, I should be able to just redirect _all_ output + let redirect_wtr = if self.redirect_on_execution { + Some(writer) + } else { + None + }; + + let mut fn_name = String::new(); + code::eval_fn_name(&code::into_mod_path_vec(self.current_mod()), &mut fn_name); + + if self.linking.mutable { + let mut r = obtain_mut_data(); + let app_data: &mut D = r.borrow_mut(); + compile::exec(&lib_file, &fn_name, app_data, redirect_wtr) + } else { + let r = obtain_brw_data(); + let app_data: &D = r.borrow(); + compile::exec(&lib_file, &fn_name, app_data, redirect_wtr) + } + }; + match exec_res { + Ok((kserd, lib)) => { + // store vec, maybe + add_to_limit_vec(&mut self.loadedlibs, lib, self.loaded_libs_size_limit); + + if self.linking.mutable { + maybe_pop_input(self); // don't save mutating inputs + EvalOutput::Print(Cow::Owned(format!("finished mutating block: {}", kserd))) + // don't print as `out#` + } else { + EvalOutput::Data(kserd) + } + } + Err(e) => { + maybe_pop_input(self); // failed so don't save + EvalOutput::Print(Cow::Borrowed(e)) + } + } + } else { + // this will keep inputs, might not be preferrable to do so in mutating state? + EvalOutput::Print(Cow::Borrowed("")) // do not execute if no extra statements have been added + } + } + + fn insert_input(&mut self, input: Input, stmt_idx: usize, item_idx: usize, crate_idx: usize) { + let Input { + items, + crates, + stmts, + } = input; + + let src = self.get_current_file_mut(); + + if !stmts.is_empty() { + src.stmts.insert(stmt_idx, StmtGrp(stmts)); + } + + for item in items.into_iter().rev() { + src.items.insert(item_idx, item); + } + + for cr in crates.into_iter().rev() { + src.crates.insert(crate_idx, cr); + } + } + + fn get_current_file_mut(&mut self) -> &mut SourceCode { + self.mods_map.get_mut(&self.current_mod).expect(&format!( + "file map does not have key: {}", + self.current_mod.display() + )) + } +} + +/// Renames the library into a distinct file name by incrementing a counter. +/// Could fail if the number of libs grows enormous, greater than `u64`. This would mean, with +/// `u64 = 18,446,744,073,709,551,615`, even with 1KB files (prolly not) this would be +/// 18,446,744,073 TB. User will probably know something is up. +fn rename_lib_file>(compiled_lib: P) -> io::Result { + let no_parent = PathBuf::new(); + let mut idx: u64 = 0; + let parent = compiled_lib.as_ref().parent().unwrap_or(&no_parent); + let name = |i| format!("papyrus.mem-code.lib.{}", i); + let mut lib_path = parent.join(&name(idx)); + while lib_path.exists() { + idx += 1; + lib_path = parent.join(&name(idx)); + } + std::fs::rename(&compiled_lib, &lib_path)?; + Ok(lib_path) +} + +fn add_to_limit_vec(store: &mut VecDeque, item: T, limit: usize) { + match (limit, store.len()) { + (0, 0) => (), // do nothing, lib will drop after this + (0, _x) => store.clear(), // zero limit and store has something, clear them + (limit, _) => { + let limit = limit - 1; // limit will be gt zero + store.truncate(limit); // truncate to limit - 1 length, as we will add new lib in + store.push_front(item); // we keep the newest versions at front of queue + } + } +} + +#[test] +fn vec_limited_testing() { + let mut vec: VecDeque = VecDeque::new(); + vec.push_front(-3); + vec.push_front(-2); + + add_to_limit_vec(&mut vec, 0, 3); + assert_eq!(&vec, &[0, -2, -3]); + + add_to_limit_vec(&mut vec, 3, 3); + assert_eq!(&vec, &[3, 0, -2]); + + add_to_limit_vec(&mut vec, -1, 0); + assert!(vec.is_empty()); + add_to_limit_vec(&mut vec, -1, 0); + assert!(vec.is_empty()); + + add_to_limit_vec(&mut vec, 0, 1); + add_to_limit_vec(&mut vec, 1, 1); + add_to_limit_vec(&mut vec, 2, 1); + assert_eq!(&vec, &[2]); +} diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 39be96b78f82183e878fefad8af316a73b2069a6..e0434b5990438b0794dc436fbee3df8946f75339 100644 GIT binary patch delta 937 zcmZWo!HU#C5JgasfJg5IUKECj!kmJ5TJYe4potpfw{iEkLbRAlD!BbUzL3@GU6COW2`TKM%Kz?Oe z!eUSeX?QqtWTf<-?!Zi*I&THQg4`6%^J}}fV3QCpE6g`k=cztuYns$ zONeXuIBT0W7Ovu-Z3SPGLGBJEFEZ6tUT;HJBk}f>p=?!@>D>qg?Q?Qr1x-X8B_+a; zLW)A`T#AfSC!wQJ%Q;9OaQLq;NLKDJ2JJ5qMNcUzt`84#{2)w=j2=f#)f%o^0hcGD z7%q{}E0BCAM#-)kot=~mL5kbN(1UQ6R5_a?W)02khqODzc`JdSt=c1RVoxPUWw9!L Q7KPrpPdIeP`)7ZOf7!V_7XSbN delta 1060 zcmZoLXf&+X*H=_v7&uTra8`qD9<_g9BVaWA2R8hLFs$d=;Lf-?iE%E+=3L&%TmaoJ B3X=c; From 14f586e8566c45a947c35d023e874bf84462cea0 Mon Sep 17 00:00:00 2001 From: Kurt Lawrence Date: Thu, 19 Dec 2019 14:33:15 +1100 Subject: [PATCH 3/4] implement renaming of libs before loading in for evaluation --- Cargo.lock | 57 ++++++++++ Cargo.toml | 1 + src/compile/build.rs | 252 +++++++++++++++++++++++++------------------ src/compile/mod.rs | 2 +- src/repl/data.rs | 10 ++ src/repl/eval.rs | 36 +------ 6 files changed, 218 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c91191..b6ad320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,14 @@ name = "byteorder" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "c2-chacha" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cast" version = "0.2.3" @@ -641,6 +649,7 @@ dependencies = [ "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "term_cursor 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -667,6 +676,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro2" version = "0.4.30" @@ -740,6 +754,27 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -761,6 +796,14 @@ dependencies = [ "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand_os" version = "0.1.3" @@ -1281,6 +1324,14 @@ name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vec_map" version = "0.8.1" @@ -1373,6 +1424,7 @@ dependencies = [ "checksum blake2b_simd 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b83b7baab1e671718d78204225800d6b170e648188ac7dc992e9d6bddf87d0c0" "checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" +"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" "checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" @@ -1432,6 +1484,7 @@ dependencies = [ "checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72" "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" @@ -1440,9 +1493,12 @@ dependencies = [ "checksum racer 2.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6d7ffceb4da3e0a29c18986f0469c209f4db3ab9f2ffe286eaa1104a3e5028" "checksum racer-cargo-metadata 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2b60cd72291a641dbaa649e9e328df552186dda1fea834c55cf28594a25b7c6f" "checksum racer-interner 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "206a244afd319767bdf97cf4e94c0d5d3b1de9cb23fd25434e7992cca4d4fa4c" +"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" +"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" "checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" "checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a" "checksum rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e18c91676f670f6f0312764c759405f13afb98d5d73819840cf72a518487bff" @@ -1501,6 +1557,7 @@ dependencies = [ "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" diff --git a/Cargo.toml b/Cargo.toml index f19bcd7..a7ca932 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ libloading = { version = "0.5", default-features = false } log = { version = "0.4", default-features = false } racer = { version = "2.1", default-features = false, optional = true, features = [ "metadata" ] } syn = { version = "1", default-features = false, optional = false, features = [ "full", "printing", "parsing" ] } +uuid = { version = "0.8", default-features = false, optional = false, features = [ "v4" ] } [dev-dependencies] criterion = "0.3" diff --git a/src/compile/build.rs b/src/compile/build.rs index 606d5b7..878d259 100644 --- a/src/compile/build.rs +++ b/src/compile/build.rs @@ -1,107 +1,145 @@ -use super::LIBRARY_NAME; -use std::io::{self, BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::{error, fmt}; - -/// Run `rustc` in the given compilation directory. -pub fn compile( - compile_dir: P, - linking_config: &crate::linking::LinkingConfiguration, - mut stderr_line_cb: F, -) -> Result -where - P: AsRef, - F: FnMut(&str), -{ - let compile_dir = compile_dir.as_ref(); - let lib_file = compile_dir.join("target/debug/"); - let lib_file = if cfg!(windows) { - lib_file.join(format!("{}.dll", LIBRARY_NAME)) - } else { - lib_file.join(format!("lib{}.so", LIBRARY_NAME)) - }; - - let mut args = vec!["rustc".to_owned(), "--".to_owned(), "-Awarnings".to_owned()]; - - for external in linking_config.external_libs.iter() { - args.push("-L".to_owned()); - args.push(format!("dependency={}", external.deps_path().display())); - args.push("--extern".to_owned()); - args.push(format!( - "{}={}", - external.lib_name(), - external.lib_path().display() - )); - } - - let mut child = Command::new("cargo") - .current_dir(compile_dir) - .args(&args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|_| CompilationError::NoBuildCommand)?; - - let stderr = { - let rdr = BufReader::new(child.stderr.as_mut().expect("stderr should be piped")); - let mut s = String::new(); - for line in rdr.lines() { - let line = line.unwrap(); - stderr_line_cb(&line); - s.push_str(&line); - s.push('\n'); - } - s - }; - - match child.wait() { - Ok(ex) => { - if ex.success() { - Ok(lib_file) - } else { - Err(CompilationError::CompileError(stderr)) - } - } - Err(e) => Err(CompilationError::IOError(e)), - } -} - -/// Error type for compilation. -#[derive(Debug)] -pub enum CompilationError { - /// Failed to initialise `cargo build`. Usually because `cargo` is not in your `PATH` or Rust is not installed. - NoBuildCommand, - /// A compiling error occured, with the contents of the stderr. - CompileError(String), - /// Generic IO errors. - IOError(io::Error), -} - -impl error::Error for CompilationError {} - -impl fmt::Display for CompilationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - CompilationError::NoBuildCommand => { - write!(f, "cargo build command failed to start, is rust installed?") - } - CompilationError::CompileError(e) => write!(f, "{}", e), - CompilationError::IOError(e) => write!(f, "io error occurred: {}", e), - } - } -} - -#[test] -fn compilation_error_fmt_test() { - let e = CompilationError::NoBuildCommand; - assert_eq!( - &e.to_string(), - "cargo build command failed to start, is rust installed?" - ); - let e = CompilationError::CompileError("compile err".to_string()); - assert_eq!(&e.to_string(), "compile err"); - let ioe = io::Error::new(io::ErrorKind::Other, "test"); - let e = CompilationError::IOError(ioe); - assert_eq!(&e.to_string(), "io error occurred: test"); -} +use super::LIBRARY_NAME; +use std::io::{self, BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::{error, fmt}; + +/// Run `rustc` in the given compilation directory. +pub fn compile( + compile_dir: P, + linking_config: &crate::linking::LinkingConfiguration, + mut stderr_line_cb: F, +) -> Result +where + P: AsRef, + F: FnMut(&str), +{ + let compile_dir = compile_dir.as_ref(); + let lib_file = compile_dir.join("target/debug/"); + let lib_file = if cfg!(windows) { + lib_file.join(format!("{}.dll", LIBRARY_NAME)) + } else { + lib_file.join(format!("lib{}.so", LIBRARY_NAME)) + }; + + let mut args = vec!["rustc".to_owned(), "--".to_owned(), "-Awarnings".to_owned()]; + + for external in linking_config.external_libs.iter() { + args.push("-L".to_owned()); + args.push(format!("dependency={}", external.deps_path().display())); + args.push("--extern".to_owned()); + args.push(format!( + "{}={}", + external.lib_name(), + external.lib_path().display() + )); + } + + let mut child = Command::new("cargo") + .current_dir(compile_dir) + .args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|_| CompilationError::NoBuildCommand)?; + + let stderr = { + let rdr = BufReader::new(child.stderr.as_mut().expect("stderr should be piped")); + let mut s = String::new(); + for line in rdr.lines() { + let line = line.unwrap(); + stderr_line_cb(&line); + s.push_str(&line); + s.push('\n'); + } + s + }; + + match child.wait() { + Ok(ex) => { + if ex.success() { + Ok(lib_file) + } else { + Err(CompilationError::CompileError(stderr)) + } + } + Err(e) => Err(CompilationError::IOError(e)), + } +} + +/// Function to rename the output library file and remove the associated dependency. +/// +/// In relation to [#44](https://github.com/kurtlawrence/papyrus/issues/44), loading a library will +/// effectively lock a library file. This is especially pervasive in windows where the `.dll` locks +/// in both the target directory and the inner `deps` folder. The locking makes subsequent +/// compilations fail with io errors. +/// +/// To separate the locking from the loading, the compiled library is renamed _then_ loaded. This +/// function renames the specified library with a random UUID. It also deletes the similarly name +/// library in the `deps` folder. _If there is no `deps` folder, or no library inside the folder, +/// the deletion silently fails_. This step is required for Windows but the behaviour is kept +/// standard across platforms. +pub fn unshackle_library_file>(libpath: P) -> PathBuf { + let libpath = libpath.as_ref(); + let lib = libpath.file_name().expect("there should be a file name"); + let depsfile = libpath + .parent() + .expect("there will be parent") + .join("deps") + .join(lib); + if depsfile.exists() { + std::fs::remove_file(depsfile).ok(); // allow deps files removal to fail + } + rename_lib_file(libpath).unwrap_or_else(|_| libpath.to_owned()) +} + +fn rename_lib_file>(compiled_lib: P) -> io::Result { + let no_parent = PathBuf::new(); + let parent = compiled_lib.as_ref().parent().unwrap_or(&no_parent); + let name = || format!("papyrus.{}.lib", uuid::Uuid::new_v4().to_hyphenated()); + let mut lib_path = parent.join(&name()); + while lib_path.exists() { + lib_path = parent.join(&name()); + } + std::fs::rename(&compiled_lib, &lib_path)?; + Ok(lib_path) +} + +/// Error type for compilation. +#[derive(Debug)] +pub enum CompilationError { + /// Failed to initialise `cargo build`. Usually because `cargo` is not in your `PATH` or Rust is not installed. + NoBuildCommand, + /// A compiling error occured, with the contents of the stderr. + CompileError(String), + /// Generic IO errors. + IOError(io::Error), +} + +impl error::Error for CompilationError {} + +impl fmt::Display for CompilationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CompilationError::NoBuildCommand => { + write!(f, "cargo build command failed to start, is rust installed?") + } + CompilationError::CompileError(e) => write!(f, "{}", e), + CompilationError::IOError(e) => write!(f, "io error occurred: {}", e), + } + } +} + +#[test] +fn compilation_error_fmt_test() { + let e = CompilationError::NoBuildCommand; + assert_eq!( + &e.to_string(), + "cargo build command failed to start, is rust installed?" + ); + let e = CompilationError::CompileError("compile err".to_string()); + assert_eq!(&e.to_string(), "compile err"); + let ioe = io::Error::new(io::ErrorKind::Other, "test"); + let e = CompilationError::IOError(ioe); + assert_eq!(&e.to_string(), "io error occurred: test"); +} diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 2179c48..87279a0 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -4,7 +4,7 @@ mod build; mod construct; mod execute; -pub use self::build::{compile, CompilationError}; +pub use self::build::{compile, unshackle_library_file, CompilationError}; pub use self::construct::build_compile_dir; pub(crate) use self::execute::exec; diff --git a/src/repl/data.rs b/src/repl/data.rs index 6c02f5b..d8825ed 100644 --- a/src/repl/data.rs +++ b/src/repl/data.rs @@ -80,6 +80,16 @@ impl ReplData { &self.linking } + /// Clears the cached loaded libraries. + /// + /// This can be used to clear resources. Loaded libraries are stored up to the + /// [`loaded_libs_size_limit`] but can be cleared earlier if need be. + /// + /// [`loaded_libs_size_limit`]: ReplData + pub fn clear_loaded_libs(&mut self) { + self.loadedlibs.clear() + } + /// Not meant to used by developer. Use the macros instead. /// [See _linking_ module](../pfh/linking.html) pub unsafe fn set_data_type(mut self, data_type: &str) -> Self { diff --git a/src/repl/eval.rs b/src/repl/eval.rs index 69214ea..9ee55f7 100644 --- a/src/repl/eval.rs +++ b/src/repl/eval.rs @@ -328,20 +328,10 @@ impl ReplData { if has_stmts { // execute let exec_res = { - // Has to be done to make linux builds work - // see: - // https://github.com/nagisa/rust_libloading/issues/5 - // https://github.com/nagisa/rust_libloading/issues/41 - // https://github.com/nagisa/rust_libloading/issues/49 - // - // Basically the api function `dlopen` will keep loaded libraries in memory to avoid - // continuously allocating memory. It only does not release the library when thread_local data - // is hanging around, and it seems `println!()` is something that does this. - // Hence to avoid not having the library not updated with a new `new()` call, a different lib - // name is passed to the function. - // This is very annoying as it has needless fs interactions and a growing fs footprint but - // what can you do ¯\_(ツ)_/¯ - let lib_file = rename_lib_file(lib_file).expect("failed renaming library file"); + // once compilation succeeds and we are going to evaluate it (which libloads) we + // first rename the files to avoid locking for the next compilation that might + // happen + let lib_file = compile::unshackle_library_file(lib_file); // FIXME If removing the true this won't work through terminals // Need to trial it in linux though as I remember it breaking a lot... @@ -419,24 +409,6 @@ impl ReplData { } } -/// Renames the library into a distinct file name by incrementing a counter. -/// Could fail if the number of libs grows enormous, greater than `u64`. This would mean, with -/// `u64 = 18,446,744,073,709,551,615`, even with 1KB files (prolly not) this would be -/// 18,446,744,073 TB. User will probably know something is up. -fn rename_lib_file>(compiled_lib: P) -> io::Result { - let no_parent = PathBuf::new(); - let mut idx: u64 = 0; - let parent = compiled_lib.as_ref().parent().unwrap_or(&no_parent); - let name = |i| format!("papyrus.mem-code.lib.{}", i); - let mut lib_path = parent.join(&name(idx)); - while lib_path.exists() { - idx += 1; - lib_path = parent.join(&name(idx)); - } - std::fs::rename(&compiled_lib, &lib_path)?; - Ok(lib_path) -} - fn add_to_limit_vec(store: &mut VecDeque, item: T, limit: usize) { match (limit, store.len()) { (0, 0) => (), // do nothing, lib will drop after this From 87328c8d89ca8d85e91ea67e6ed8a8ebc3eab2cf Mon Sep 17 00:00:00 2001 From: Kurt Lawrence Date: Thu, 19 Dec 2019 16:47:20 +1100 Subject: [PATCH 4/4] remove redirection of output --- CHANGELOG.md | 5 ++ Cargo.lock | 12 --- Cargo.toml | 2 - src/compile/execute.rs | 191 ++++++++++++----------------------------- src/compile/mod.rs | 8 +- src/repl/data.rs | 1 - src/repl/eval.rs | 14 +-- src/repl/mod.rs | 4 - src/repl/read.rs | 4 +- src/run/interface.rs | 5 -- 10 files changed, 68 insertions(+), 178 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8c77a..84c9ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Version - Restructure of repository +- Libraries can now be cached and not dropped once evaluation is finished. +- Removed ability to redirect output of evaluation. + - This functionality is broken and requires + a. More testing + b. Better use case ## 0.12.0 - Handle `Item::Use` when parsing input. diff --git a/Cargo.lock b/Cargo.lock index b6ad320..30f5efc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,7 +637,6 @@ dependencies = [ "colored 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "crossterm 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -645,7 +644,6 @@ dependencies = [ "libloading 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "racer 2.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "shh 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "term_cursor 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1167,15 +1165,6 @@ dependencies = [ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "shh" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "signal-hook" version = "0.1.12" @@ -1537,7 +1526,6 @@ dependencies = [ "checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" "checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" "checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7" -"checksum shh 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9cff8b151d3d290604d9c93f0400bd4d98e2837a5461532bc6c95d0129ea0964" "checksum signal-hook 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "7a9c17dd3ba2d36023a5c9472ecddeda07e27fd0b05436e8c1e0c8f178185652" "checksum signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" diff --git a/Cargo.toml b/Cargo.toml index a7ca932..a22b43e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,13 +19,11 @@ codecov = { repository = "kurtlawrence/papyrus" } # My crates cmdtree = { version = "0.10", default-features = false } kserd = { version = "0.1", default-features = false, optional = false, features = [ "format" ] } -shh = { version = "1", default-features = false } # crates.io backtrace = { version = "0.3", default-features = false } colored = { version = "1", default-features = false } crossbeam-channel = { version = "0.4", default-features = false } -crossbeam-utils = { version = "0.7", default-features = false } crossterm = { version = "0.14", default-features = false, optional = true } dirs = { version = "2", default-features = false } fxhash = { version = "0.2", default-features = false } diff --git a/src/compile/execute.rs b/src/compile/execute.rs index b2f1d54..20b5a42 100644 --- a/src/compile/execute.rs +++ b/src/compile/execute.rs @@ -1,135 +1,56 @@ -use ::kserd::Kserd; -use libloading::{Library, Symbol}; -use std::io::{self, Write}; -use std::path::Path; - -/// We don't type anything here. You must be **VERY** careful to pass through the correct borrow to match the -/// function signature! -type DataFunc = unsafe fn(D) -> Kserd<'static>; - -type ExecResult = Result<(Kserd<'static>, Library), &'static str>; - -pub(crate) fn exec, D, W: Write + Send>( - library_file: P, - function_name: &str, - app_data: D, - wtr: Option<&mut W>, -) -> ExecResult { - if let Some(wtr) = wtr { - exec_and_redirect(library_file, function_name, app_data, wtr) - } else { - exec_no_redirect(library_file, function_name, app_data) - } -} - -fn exec_no_redirect, Data>( - library_file: P, - function_name: &str, - app_data: Data, -) -> ExecResult { - let lib = get_lib(library_file)?; - let func = get_func(&lib, function_name)?; - - let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { func(app_data) })); - - match res { - Ok(kserd) => Ok((kserd, lib)), - Err(_) => Err("a panic occured with evaluation"), - } -} - -fn exec_and_redirect, Data, W: Write + Send>( - library_file: P, - function_name: &str, - app_data: Data, - output_wtr: &mut W, -) -> ExecResult { - let lib = get_lib(library_file)?; - let func = get_func(&lib, function_name)?; - - let (tx, rx) = crossbeam_channel::bounded(0); - - let (stdout_gag, stderr_gag) = - get_gags().map_err(|_| "failed to apply redirect gags on stdout and stderr")?; - - let res = crossbeam_utils::thread::scope(|scope| { - let jh = if cfg!(debug_assertions) { - // don't redirect on debug builds, such that dbg!() can print through to terminal for debugging. - drop(stderr_gag); - scope.spawn(|_| redirect_output_loop(output_wtr, rx, stdout_gag, std::io::empty())) - } else { - scope.spawn(|_| redirect_output_loop(output_wtr, rx, stdout_gag, stderr_gag)) - }; - - let res = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { func(app_data) })); - - tx.send(()).expect("sending signal to stop gagging failed"); - jh.join() - .expect("joining gagging thread failed") - .expect("failed to redirect output loop"); - res - }); - - let res = res.map_err(|_| "crossbeam scoping failed")?; - - match res { - Ok(kserd) => Ok((kserd, lib)), - Err(_) => Err("a panic occured with evaluation"), - } -} - -fn get_lib>(path: P) -> Result { - // If segfaults are occurring maybe use this, SIGSEV? - // This is shown in https://github.com/nagisa/rust_libloading/issues/41 - // let lib: Library = - // libloading::os::unix::Library::open(Some(library_file.as_ref()), 0x2 | 0x1000) - // .unwrap() - // .into(); - Library::new(path.as_ref()).map_err(|e| { - error!("failed to load library file: {}", e); - "failed to load library file" - }) -} - -fn get_func<'l, Data>( - lib: &'l Library, - name: &str, -) -> Result>, &'static str> { - unsafe { - lib.get(name.as_bytes()) - .map_err(|_| "failed to find function in library") - } -} - -fn get_gags() -> io::Result<(shh::ShhStdout, shh::ShhStderr)> { - Ok((shh::stdout()?, shh::stderr()?)) -} - -fn redirect_output_loop( - wtr: &mut W, - rx: crossbeam_channel::Receiver<()>, - mut stdout_gag: R1, - mut stderr_gag: R2, -) -> io::Result<()> { - loop { - std::thread::sleep(std::time::Duration::from_millis(2)); // add in some delay as reading occurs to avoid smashing cpu. - - let mut buf = Vec::new(); - - // read/write stderr first - stderr_gag.read_to_end(&mut buf)?; - - stdout_gag.read_to_end(&mut buf)?; - - wtr.write_all(&buf)?; - - match rx.try_recv() { - Ok(_) => break, // stop signal sent - Err(crossbeam_channel::TryRecvError::Disconnected) => break, // tx dropped - _ => (), - }; - } - - Ok(()) -} +use ::kserd::Kserd; +use libloading::{Library, Symbol}; +use std::path::Path; + +/// We don't type anything here. You must be **VERY** careful to pass through the correct borrow to match the +/// function signature! +type DataFunc = unsafe fn(D) -> Kserd<'static>; + +type ExecResult = Result<(Kserd<'static>, Library), &'static str>; + +pub(crate) fn exec, D>( + library_file: P, + function_name: &str, + app_data: D, +) -> ExecResult { + exec_no_redirect(library_file, function_name, app_data) +} + +fn exec_no_redirect, Data>( + library_file: P, + function_name: &str, + app_data: Data, +) -> ExecResult { + let lib = get_lib(library_file)?; + let func = get_func(&lib, function_name)?; + + let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { func(app_data) })); + + match res { + Ok(kserd) => Ok((kserd, lib)), + Err(_) => Err("a panic occured with evaluation"), + } +} + +fn get_lib>(path: P) -> Result { + // If segfaults are occurring maybe use this, SIGSEV? + // This is shown in https://github.com/nagisa/rust_libloading/issues/41 + // let lib: Library = + // libloading::os::unix::Library::open(Some(library_file.as_ref()), 0x2 | 0x1000) + // .unwrap() + // .into(); + Library::new(path.as_ref()).map_err(|e| { + error!("failed to load library file: {}", e); + "failed to load library file" + }) +} + +fn get_func<'l, Data>( + lib: &'l Library, + name: &str, +) -> Result>, &'static str> { + unsafe { + lib.get(name.as_bytes()) + .map_err(|_| "failed to find function in library") + } +} diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 87279a0..a0183e9 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -36,7 +36,7 @@ mod tests { let path = compile(&compile_dir, &linking_config, |_| ()).unwrap(); // eval - let r = exec::<_, _, std::io::Sink>(path, "_lib_intern_eval", &(), None).unwrap(); // execute library fn + let r = exec::<_, _>(path, "_lib_intern_eval", &()).unwrap(); // execute library fn assert_eq!(r.0, Kserd::new_num(4)); } @@ -61,7 +61,7 @@ mod tests { let path = compile(&compile_dir, &linking_config, |_| ()).unwrap(); // eval - let r = exec::<_, _, std::io::Sink>(path, "_lib_intern_eval", &(), None).unwrap(); // execute library fn + let r = exec::<_, _>(path, "_lib_intern_eval", &()).unwrap(); // execute library fn assert_eq!(r.0, Kserd::new_num(4)); } @@ -92,7 +92,7 @@ mod tests { let path = compile(&compile_dir, &linking_config, |_| ()).unwrap(); // eval - let r = exec::<_, _, std::io::Sink>(path, "_lib_intern_eval", &(), None).unwrap(); // execute library fn + let r = exec::<_, _>(path, "_lib_intern_eval", &()).unwrap(); // execute library fn assert_eq!(r.0, Kserd::new_num(4)); } @@ -123,7 +123,7 @@ mod tests { let path = compile(&compile_dir, &linking_config, |_| ()).unwrap(); // eval - let r = exec(path, "_lib_intern_eval", &(), Some(&mut std::io::sink())).unwrap(); // execute library fn + let r = exec(path, "_lib_intern_eval", &()).unwrap(); // execute library fn assert_eq!(r.0, Kserd::new_num(4)); } diff --git a/src/repl/data.rs b/src/repl/data.rs index d8825ed..0773dc4 100644 --- a/src/repl/data.rs +++ b/src/repl/data.rs @@ -17,7 +17,6 @@ impl Default for ReplData { out_colour: Color::BrightGreen, compilation_dir: default_compile_dir(), linking: LinkingConfiguration::default(), - redirect_on_execution: true, editing: None, editing_src: None, loadedlibs: VecDeque::new(), diff --git a/src/repl/eval.rs b/src/repl/eval.rs index 9ee55f7..f519fcd 100644 --- a/src/repl/eval.rs +++ b/src/repl/eval.rs @@ -6,7 +6,6 @@ use crate::{ }; use std::borrow::{Borrow, BorrowMut}; use std::ops::{Deref, DerefMut}; -use std::path::Path; use std::sync::{Arc, Mutex}; impl Repl { @@ -333,26 +332,17 @@ impl ReplData { // happen let lib_file = compile::unshackle_library_file(lib_file); - // FIXME If removing the true this won't work through terminals - // Need to trial it in linux though as I remember it breaking a lot... - // If it is all good, I should be able to just redirect _all_ output - let redirect_wtr = if self.redirect_on_execution { - Some(writer) - } else { - None - }; - let mut fn_name = String::new(); code::eval_fn_name(&code::into_mod_path_vec(self.current_mod()), &mut fn_name); if self.linking.mutable { let mut r = obtain_mut_data(); let app_data: &mut D = r.borrow_mut(); - compile::exec(&lib_file, &fn_name, app_data, redirect_wtr) + compile::exec(&lib_file, &fn_name, app_data) } else { let r = obtain_brw_data(); let app_data: &D = r.borrow(); - compile::exec(&lib_file, &fn_name, app_data, redirect_wtr) + compile::exec(&lib_file, &fn_name, app_data) } }; match exec_res { diff --git a/src/repl/mod.rs b/src/repl/mod.rs index e0434b5..c2c69f4 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -61,10 +61,6 @@ pub struct ReplData { /// The external crate linking configuration, linking: LinkingConfiguration, - /// Flag if output is to be redirected. Generally redirection is needed, - /// `DefaultTerminal` however will not require it (fucks linux). - redirect_on_execution: bool, - /// Flag for editing a statement, item, or crate. /// /// If a value is set when an evaluation starts, the input buffer diff --git a/src/repl/read.rs b/src/repl/read.rs index 4cef821..8b443c2 100644 --- a/src/repl/read.rs +++ b/src/repl/read.rs @@ -2,9 +2,7 @@ use super::*; impl Default for Repl { fn default() -> Self { - let mut data = ReplData::default(); - - data.redirect_on_execution = false; + let data = ReplData::default(); let mut r = Repl { state: Read { diff --git a/src/run/interface.rs b/src/run/interface.rs index 38dea79..29038f9 100644 --- a/src/run/interface.rs +++ b/src/run/interface.rs @@ -78,11 +78,6 @@ impl InputBuffer { self.buf.len() } - pub fn clear(&mut self) { - self.buf.clear(); - self.pos = 0; - } - pub fn insert(&mut self, ch: char) { self.buf.insert(self.pos, ch); self.pos += 1;