diff --git a/Cargo.toml b/Cargo.toml index 04300c67..dcbe3dd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,9 @@ atty = { version = "0.2", default-features = false, optional = true } git-testament = { version = "0.1", optional = true } rand = { version = "0.7", optional = true } rodio = { version = "0.11.0", optional = true } +rustyline = { version = "6.2.0", optional = true, default-features = false } +rustyline-derive = { version = "0.3.1", optional = true } +dirs-next = { version = "1.0.1", optional = true } [dev-dependencies] env_logger = { version = "0.7", default-features = false } @@ -50,20 +53,27 @@ proptest = "^0.9.6" proptest-derive = "0.1" [features] -default = ["cc", "codegen", "color-backtrace"] +default = ["cc", "codegen", "color-backtrace", "repl"] # The `swcc` binary cc = ["ansi_term", "git-testament", "tempfile", "pico-args", "codegen", "atty"] codegen = ["cranelift", "cranelift-module", "cranelift-object"] jit = ["codegen", "cranelift-simplejit"] +# The `swcci` binary +repl = ["jit", "rustyline", "rustyline-derive", "ansi_term", "dirs-next"] salty = ["rand", "rodio"] # for internal use _test_headers = [] [[bin]] name = "swcc" -path = "src/main.rs" +path = "src/bin/swcc.rs" required-features = ["cc"] +[[bin]] +name = "swcci" +path = "src/bin/swcci.rs" +required-features = ["repl"] + [[bench]] name = "examples" harness = false diff --git a/src/analyze/mod.rs b/src/analyze/mod.rs index b872f2f7..ec7f94e7 100644 --- a/src/analyze/mod.rs +++ b/src/analyze/mod.rs @@ -139,6 +139,15 @@ impl PureAnalyzer { pub fn warnings(&mut self) -> VecDeque { std::mem::take(&mut self.error_handler.warnings) } + + /// Return all errors seen so far. + /// + /// These errors are consumed and will not be returned if you call + /// `errors()` again. + pub fn errors(&mut self) -> VecDeque { + self.error_handler.by_ref().collect() + } + // I type these a lot #[inline(always)] fn err(&mut self, e: SemanticError, l: Location) { diff --git a/src/bin/repl/commands.rs b/src/bin/repl/commands.rs new file mode 100644 index 00000000..10d70a73 --- /dev/null +++ b/src/bin/repl/commands.rs @@ -0,0 +1,27 @@ +use super::Repl; +use std::collections::HashMap; + +pub fn default_commands() -> HashMap<&'static str, fn(&mut Repl, &str)> { + let mut map = HashMap::<&'static str, fn(&mut Repl, &str)>::new(); + map.insert("help", help_command); + map.insert("h", help_command); + map.insert("quit", quit_command); + map.insert("q", quit_command); + map +} + +fn help_command(_repl: &mut Repl, _args: &str) { + print!( + "\ +Available commands: + {p}help|h Shows this message + {p}quit|q Quits the repl +", + p = super::PREFIX + ); +} + +fn quit_command(repl: &mut Repl, _args: &str) { + repl.save_history(); + std::process::exit(0) +} diff --git a/src/bin/repl/helper.rs b/src/bin/repl/helper.rs new file mode 100644 index 00000000..0f2f2129 --- /dev/null +++ b/src/bin/repl/helper.rs @@ -0,0 +1,127 @@ +use super::PREFIX; +use ansi_term::Style; +use rustyline::{ + completion::{extract_word, Candidate, Completer}, + highlight::{Highlighter, MatchingBracketHighlighter}, + hint::Hinter, + validate::{ValidationContext, ValidationResult, Validator}, + Context, +}; +use rustyline_derive::Helper; +use std::borrow::Cow; + +#[derive(Helper)] +pub struct ReplHelper { + highlighter: MatchingBracketHighlighter, + commands: Vec<&'static str>, +} + +impl ReplHelper { + pub fn new(commands: Vec<&'static str>) -> Self { + Self { + commands, + highlighter: Default::default(), + } + } +} + +impl Highlighter for ReplHelper { + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + // TODO: Syntax highlighting. + self.highlighter.highlight(line, pos) + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + let hint = Style::new().dimmed().paint(hint); + Cow::Owned(hint.to_string()) + } + + fn highlight_char(&self, line: &str, pos: usize) -> bool { + self.highlighter.highlight_char(line, pos) + } +} + +impl Validator for ReplHelper { + fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { + let input = ctx.input(); + let mut stack = vec![]; + + for c in input.chars() { + match c { + '(' | '[' | '{' => stack.push(c), + ')' | ']' | '}' => match (stack.pop(), c) { + (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} + (_, _) => { + return Ok(ValidationResult::Invalid(Some( + "extra closing delimiter".to_string(), + ))); + } + }, + _ => continue, + } + } + + if stack.is_empty() { + Ok(ValidationResult::Valid(None)) + } else { + Ok(ValidationResult::Incomplete) + } + } +} + +impl Hinter for ReplHelper { + fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { + let start = &line[..pos]; + if !start.starts_with(PREFIX) { + return None; + } + let start = &start[PREFIX.len_utf8()..]; + self.commands + .iter() + .find(|cmd| cmd.starts_with(start)) + .map(|hint| String::from(&hint[start.len()..])) + } +} + +/// Wrapper around a `&'static str` to be used for completion candidates. +pub struct CompletionCandidate { + display: &'static str, +} + +impl Candidate for CompletionCandidate { + fn display(&self) -> &str { + self.display + } + + fn replacement(&self) -> &str { + self.display + } +} + +impl Completer for ReplHelper { + type Candidate = CompletionCandidate; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + let (idx, word) = extract_word(line, pos, None, &[]); + if !line.starts_with(PREFIX) { + return Ok((0, vec![])); + } + let word = word.trim_matches(PREFIX); + + let commands = self + .commands + .iter() + .filter(|cmd| cmd.starts_with(word)) + .map(|x| CompletionCandidate { display: x }) + .collect::>(); + + Ok((idx + 1, commands)) + } + + // TODO: Complete method names, types, etc. +} diff --git a/src/bin/repl/mod.rs b/src/bin/repl/mod.rs new file mode 100644 index 00000000..e03640c3 --- /dev/null +++ b/src/bin/repl/mod.rs @@ -0,0 +1,210 @@ +//! Repl implementation using [`rustyline`]. +//! +//! [`rustyline`]: https://docs.rs/rustyline + +use commands::default_commands; +use dirs_next::data_dir; +use helper::ReplHelper; +use hir::{Declaration, Expr}; +use rustyline::{error::ReadlineError, Cmd, CompletionType, Config, EditMode, Editor, KeyPress}; +use saltwater::{ + data, hir, initialize_jit_module, ir, types, CompileError, Locatable, Parser, + PreProcessorBuilder, PureAnalyzer, SyntaxError, Type, JIT, +}; +use std::{collections::HashMap, path::PathBuf}; +use types::ArrayType; + +mod commands; +mod helper; + +/// The prefix for commands inside the repl. +const PREFIX: char = ':'; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const PROMPT: &str = ">> "; + +macro_rules! execute { + ($fun:ident, $ty:path, $action:expr) => { + $action(unsafe { + let execute: unsafe extern "C" fn() -> $ty = std::mem::transmute($fun); + execute() + }); + }; +} + +pub struct Repl { + editor: Editor, + commands: HashMap<&'static str, fn(&mut Repl, &str)>, +} + +impl Repl { + pub fn new() -> Self { + let config = Config::builder() + .history_ignore_space(true) + .history_ignore_dups(false) + .completion_type(CompletionType::List) + .edit_mode(EditMode::Emacs) + .max_history_size(1000) + .tab_stop(4) + .build(); + let mut editor = Editor::with_config(config); + + let commands = default_commands(); + let helper = ReplHelper::new(commands.keys().copied().collect()); + editor.set_helper(Some(helper)); + + editor.bind_sequence(KeyPress::Up, Cmd::LineUpOrPreviousHistory(1)); + editor.bind_sequence(KeyPress::Down, Cmd::LineDownOrNextHistory(1)); + editor.bind_sequence(KeyPress::Tab, Cmd::Complete); + + Self { editor, commands } + } + + pub fn run(&mut self) -> rustyline::Result<()> { + self.load_history(); + + println!("Saltwater {}", VERSION); + println!(r#"Type "{}help" for more information."#, PREFIX); + let result = loop { + let line = self.editor.readline(PROMPT); + match line { + Ok(line) => self.process_line(line), + // Ctrl + c will abort the current line. + Err(ReadlineError::Interrupted) => continue, + // Ctrl + d will exit the repl. + Err(ReadlineError::Eof) => break Ok(()), + Err(err) => break Err(err), + } + }; + self.save_history(); + + result + } + + fn save_history(&self) -> Option<()> { + let path = Self::history_path()?; + self.editor.save_history(&path).ok() + } + + fn load_history(&mut self) -> Option<()> { + let path = Self::history_path()?; + self.editor.load_history(&path).ok() + } + + fn history_path() -> Option { + let mut history = data_dir()?; + history.push("saltwater_history"); + Some(history) + } + + fn process_line(&mut self, line: String) { + self.editor.add_history_entry(line.clone()); + + let line = line.trim(); + if line.starts_with(PREFIX) { + let name = line.split(' ').next().unwrap(); + + match self.commands.get(&name[1..]) { + Some(action) => action(self, &line[name.len()..]), + None => println!("unknown command '{}'", name), + } + } else { + match self.execute_code(line) { + Ok(_) => {} + Err(err) => { + // TODO: Proper error reporting + println!("error: {}", err.data); + } + } + } + } + + fn execute_code(&mut self, code: &str) -> Result<(), CompileError> { + let module = initialize_jit_module(); + + let expr = analyze_expr(code)?; + let expr_ty = expr.ctype.clone(); + let decl = wrap_expr(expr); + let module = ir::compile(module, vec![decl], false).0?; + + let mut jit = JIT::from(module); + jit.finalize(); + let fun = jit + .get_compiled_function("execute") + .expect("this is not good."); + + match expr_ty { + Type::Short(signed) => execute!(fun, i16, |x| match signed { + true => println!("=> {}", x), + false => println!("=> {}", x as u16), + }), + Type::Int(signed) => execute!(fun, i32, |x| match signed { + true => println!("=> {}", x), + false => println!("=> {}", x as u32), + }), + Type::Long(signed) => execute!(fun, i64, |x| match signed { + true => println!("=> {}", x), + false => println!("=> {}", x as u64), + }), + Type::Float => execute!(fun, f32, |f| println!("=> {}", f)), + Type::Double => execute!(fun, f64, |f| println!("=> {}", f)), + + Type::Char(_) => execute!(fun, char, |c| println!("=> {}", c)), + Type::Bool => execute!(fun, bool, |b| println!("=> {}", b)), + Type::Void => unsafe { + let execute: unsafe extern "C" fn() = std::mem::transmute(fun); + execute() + }, + + // TODO: Implement execution for more types + ty => println!("error: expression returns unsupported type: {:?}", ty), + }; + Ok(()) + } +} + +/// Takes an expression and wraps it into a `execute` function that looks like the following: +/// +/// ``` +/// execute() { +/// return ; +/// } +/// ``` +fn wrap_expr(expr: Expr) -> Locatable { + let fun = hir::Variable { + ctype: types::Type::Function(types::FunctionType { + return_type: Box::new(expr.ctype.clone()), + params: vec![], + varargs: false, + }), + storage_class: data::StorageClass::Extern, + qualifiers: Default::default(), + id: "execute".into(), + }; + + let span = expr.location; + let return_stmt = span.with(hir::StmtType::Return(Some(expr))); + let init = hir::Initializer::FunctionBody(vec![return_stmt]); + let decl = hir::Declaration { + symbol: fun.insert(), + init: Some(init), + }; + span.with(decl) +} + +fn analyze_expr(code: &str) -> Result> { + let code = format!("{}\n", code).into_boxed_str(); + let cpp = PreProcessorBuilder::new(code).build(); + let mut parser = Parser::new(cpp, false); + let expr = parser.expr()?; + + let mut analyzer = PureAnalyzer::new(); + let expr = analyzer.expr(expr); + + // FIXME: error_handler is private so this doesn't work right now. + // Please review and propose a solution. + // if let Some(err) = analyzer.error_handler.pop_front() { + // return Err(err); + // } + + Ok(expr) +} diff --git a/src/main.rs b/src/bin/swcc.rs similarity index 99% rename from src/main.rs rename to src/bin/swcc.rs index d941b5d9..a072ea9c 100644 --- a/src/main.rs +++ b/src/bin/swcc.rs @@ -554,7 +554,8 @@ mod backtrace { #[cfg(feature = "salty")] fn play_scream() -> Result<(), ()> { - const SCREAM: &[u8] = include_bytes!("data/R2D2-Scream.ogg"); + const SCREAM: &[u8] = + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "data/R2D2-Scream.ogg")); let device = rodio::default_output_device().ok_or(())?; let source = rodio::Decoder::new(std::io::Cursor::new(SCREAM)).or(Err(()))?; rodio::play_raw(&device, rodio::source::Source::convert_samples(source)); diff --git a/src/bin/swcci.rs b/src/bin/swcci.rs new file mode 100644 index 00000000..be1741e6 --- /dev/null +++ b/src/bin/swcci.rs @@ -0,0 +1,12 @@ +mod repl; + +fn main() { + let mut repl = repl::Repl::new(); + match repl.run() { + Ok(_) => {} + Err(err) => { + println!("error: {}", err); + std::process::exit(1); + } + } +} diff --git a/src/data/hir.rs b/src/data/hir.rs index 5a15f946..909ad5ab 100644 --- a/src/data/hir.rs +++ b/src/data/hir.rs @@ -115,7 +115,8 @@ impl Symbol { } impl Variable { - pub(crate) fn insert(self) -> Symbol { + /// Inserts this `Variable` into the global symbol table. + pub fn insert(self) -> Symbol { SYMBOL_TABLE.with(|store| store.borrow_mut().insert(self)) } } diff --git a/src/ir/mod.rs b/src/ir/mod.rs index 920fe856..678d1b84 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -104,7 +104,7 @@ struct Compiler { } /// Compile a program from a high level IR to a Cranelift Module -pub(crate) fn compile( +pub fn compile( module: Module, program: Vec>, debug: bool, diff --git a/src/lib.rs b/src/lib.rs index 8aab4781..75681d03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ pub mod data; mod fold; pub mod intern; #[cfg(feature = "codegen")] -mod ir; +pub mod ir; mod lex; mod parse;