// Copyright 2017 Jeremy Wall // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! The build stage of the ucg compiler. use std::cell::RefCell; use std::collections::HashMap; use std::convert::TryInto; use std::error::Error; use std::fs::File; use std::io::Read; use std::path::PathBuf; use std::process; use std::rc::Rc; use atty; use atty::Stream; use rustyline; use rustyline::error::ReadlineError; use simple_error; use crate::ast::*; use crate::build::opcode::pointer::OpPointer; use crate::build::opcode::translate; use crate::build::opcode::translate::PositionMap; use crate::build::opcode::Environment; use crate::build::opcode::VM; use crate::error; use crate::iter::OffsetStrIter; use crate::parse::parse; pub mod format; pub mod ir; pub mod opcode; pub mod scope; pub mod stdlib; pub use self::ir::Val; /// The result of a build. type BuildResult = Result<(), Box>; /// AssertCollector collects the results of assertions in the UCG AST. pub struct AssertCollector { pub counter: i32, pub success: bool, pub summary: String, pub failures: String, } impl AssertCollector { pub fn new() -> Self { Self { counter: 0, success: true, summary: String::new(), failures: String::new(), } } fn record_assert_result(&mut self, msg: &str, is_success: bool) { if !is_success { let msg = format!("{} - NOT OK: {}\n", self.counter, msg); self.summary.push_str(&msg); self.failures.push_str(&msg); self.success = false; } else { let msg = format!("{} - OK: {}\n", self.counter, msg); self.summary.push_str(&msg); } self.counter += 1; } } // TODO(jwall): I think the Rc is no longer necessary. /// Builder handles building ucg code for a single file. pub struct FileBuilder<'a, Stdout, Stderr> where Stdout: std::io::Write + Clone, Stderr: std::io::Write + Clone, { pub environment: &'a RefCell>, working_dir: PathBuf, strict: bool, // FIXME(jwall): These need to be compiled and added to the op cache. // specifically in the environment. std: Rc>, import_path: &'a Vec, pub last: Option>, pub out: Option>, validate_mode: bool, } impl<'a, Stdout, Stderr> FileBuilder<'a, Stdout, Stderr> where Stdout: std::io::Write + Clone, Stderr: std::io::Write + Clone, { /// Constructs a new Builder. pub fn new>( working_dir: P, import_paths: &'a Vec, environment: &'a RefCell>, ) -> Self { FileBuilder { environment: environment, strict: false, // Our import stack is initialized with ourself. working_dir: working_dir.into(), std: Rc::new(stdlib::get_libs()), import_path: import_paths, out: None, last: None, validate_mode: false, } } pub fn clone_builder(&self) -> Self { FileBuilder { environment: self.environment.clone(), strict: self.strict, working_dir: self.working_dir.clone(), std: self.std.clone(), import_path: self.import_path, out: None, last: None, validate_mode: self.validate_mode, } } pub fn set_strict(&mut self, strict: bool) { self.strict = strict; } /// Builds a ucg file at the named path. pub fn build>(&mut self, file: P) -> BuildResult { let file = file.into(); self.working_dir = file.parent().unwrap().to_path_buf(); let mut f = self.open_file(&Position::new(0, 0, 0), &file)?; let mut s = String::new(); f.read_to_string(&mut s)?; let input = OffsetStrIter::new(&s).with_src_file(file.clone()); // TODO(jwall): Pass in the file name? let eval_result = self.eval_input(input, Some(file.clone())); match eval_result { Ok(v) => { self.last = Some(v); Ok(()) } Err(e) => { let err = simple_error::SimpleError::new(&format!( "Error building file: {}\n{}", file.to_string_lossy(), e.as_ref() )); Err(Box::new(err)) } } } fn open_file>(&self, pos: &Position, path: P) -> Result> { let path = path.into(); match File::open(&path) { Ok(f) => Ok(f), Err(e) => Err(error::BuildError::with_pos( format!("Error opening file {} {}", path.to_string_lossy(), e), error::ErrorType::TypeFail, pos.clone(), ) .to_boxed()), } } /// Puts the builder in validation mode. /// /// Among other things this means that assertions will be evaluated and their results /// will be saved in a report for later output. pub fn enable_validate_mode(&mut self) { self.validate_mode = true; } /// Builds a list of parsed UCG Statements. pub fn eval_stmts(&mut self, ast: Vec, path: Option) -> BuildResult { // We should probably stash this in an op_cache somewhere? let ops = translate::AST::translate(ast, &self.working_dir); let mut vm = VM::new(self.strict, Rc::new(ops), &self.working_dir); if path.is_some() { vm.set_path(path.unwrap()); } if self.validate_mode { vm.enable_validate_mode(); } vm.run(self.environment)?; self.out = Some(Rc::new(vm.symbols_to_tuple(false).into())); Ok(()) } pub fn repl(&mut self, mut editor: rustyline::Editor<()>, config_home: PathBuf) -> BuildResult { // loop let mut lines = crate::io::StatementAccumulator::new(); if atty::is(Stream::Stdin) { println!("Welcome to the UCG repl. Ctrl-D to exit, Ctrl-C to abort expression."); println!("Type '#help' for help."); println!(""); } // Initialize VM with an empty OpPointer let mut vm = VM::new(self.strict, Rc::new(PositionMap::new()), &self.working_dir); loop { // print prompt let line = match editor.readline(&format!("{}> ", lines.next_line())) { Ok(l) => l, Err(e) => { if let ReadlineError::Eof = e { eprintln!("Recieved EOF Exiting..."); process::exit(0); } if let ReadlineError::Interrupted = e { // Reset our lines and start over again eprintln!("Interrupted!"); lines.reset(); continue; } eprintln!("Error: {}", e); process::exit(1); } }; // repl commands are only valid while not accumulating a statement; let trimmed = line.trim(); if trimmed.starts_with("#") { // handle the various commands. if trimmed.starts_with("#help") { println!(include_str!("../help/repl.txt")); } else if trimmed.starts_with("#del") { // remove a named binding from the builder output. let args: Vec<&str> = trimmed.split(" ").skip(1).collect(); if args.len() != 1 { // print usage of the #del command eprintln!("The '#del' command expects a single argument specifying \nthe binding to delete."); } else { let key = args[0].to_string(); if let None = vm.remove_symbol(&key) { eprintln!("No such binding {}", key); } } } else if trimmed.starts_with("#exit") { process::exit(0); } else { eprintln!("Invalid repl command..."); eprintln!(""); println!(include_str!("../help/repl.txt")); } continue; } lines.push(line); // check to see if that line is a statement loop { // read a statement if let Some(stmt) = lines.get_statement() { // if it is then // eval statement let stmts = parse(OffsetStrIter::new(&stmt), None)?; let ops = translate::AST::translate(stmts, &self.working_dir); vm = vm.to_new_pointer(OpPointer::new(Rc::new(ops))); match vm.run(self.environment) { // print the result Err(e) => eprintln!("{}", e), Ok(_) => { match vm.last { Some((ref val, _)) => { println!("{}", val); vm.last = None; } None => {} } editor.history_mut().add(stmt); editor.save_history(&config_home)?; } } // start loop over at prompt. break; } // if not then keep accumulating lines without a prompt lines.push(editor.readline(&format!("{}> ", lines.next_line()))?); } } } // TODO(jwall): The repl is going to have to be in here. pub fn eval_input( &mut self, input: OffsetStrIter, path: Option, ) -> Result, Box> { match parse(input.clone(), None) { Ok(stmts) => { self.eval_stmts(stmts, path)?; if let Some(v) = self.out.clone() { return Ok(v); } unreachable!(); } Err(err) => Err(Box::new(err)), } } /// Evaluate an input string as UCG. pub fn eval_string(&mut self, input: &str) -> Result, Box> { self.eval_input(OffsetStrIter::new(input), None) } pub fn eval_expr(&mut self, expr: Expression) -> Result, Box> { let mut ops_map = translate::PositionMap { ops: Vec::new(), pos: Vec::new(), }; translate::AST::translate_stmt( Statement::Expression(expr), &mut ops_map, &self.working_dir, ); let mut vm = VM::new(self.strict, Rc::new(ops_map), &self.working_dir); if self.validate_mode { vm.enable_validate_mode(); } vm.run(self.environment)?; if let Some((val, _)) = vm.last.clone() { return Ok(Rc::new(val.try_into()?)); } unreachable!(); } pub fn get_out_by_name(&self, name: &str) -> Option> { if let Some(val) = self.out.clone() { if let &Val::Tuple(ref flds) = val.as_ref() { for (k, v) in flds.iter() { if k == name { return Some(v.clone()); } } } } return None; } pub fn assert_results(&self) -> bool { self.environment.borrow().assert_results.success } pub fn assert_summary(&self) -> String { self.environment.borrow().assert_results.summary.clone() } } #[cfg(test)] mod compile_test; #[cfg(test)] mod test;