diff --git a/Cargo.toml b/Cargo.toml index aef6396b..995a5929 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "saltwater" version = "0.11.0" authors = [ - "Jynn Nelson ", + "Joshua Nelson ", "Graham Scheaffer ", "Wesley Norris ", "playX", @@ -47,6 +47,7 @@ jit = ["saltwater-codegen/jit"] _test_headers = [] [workspace] +members = ["saltwater-repl"] [[bin]] name = "swcc" diff --git a/LICENSE.txt b/LICENSE.txt index 5cedcb17..99f6d830 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2019-2020 Jynn Nelson. All rights reserved. +Copyright (c) 2019-2020 Joshua Nelson. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index c3db205f..b866917c 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,6 @@ saltwater: the part of the sea causing lots of rust A C compiler written in Rust, with a focus on good error messages. ---- - -_**This project is no longer maintained.**_ - ---- - ## Running `swcc` reads from standard in by default, so you can type in code directly. @@ -137,7 +131,7 @@ Hello, world! ```txt $ swcc --help swcc 0.9.0 -Jynn Nelson +Joshua Nelson A C compiler written in Rust, with a focus on good error messages. Homepage: https://github.com/jyn514/rcc/ diff --git a/saltwater-codegen/Cargo.toml b/saltwater-codegen/Cargo.toml index 5b78e2ff..f13f1e1d 100644 --- a/saltwater-codegen/Cargo.toml +++ b/saltwater-codegen/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "saltwater-codegen" version = "0.11.0" +authors = ["Joshua Nelson "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/saltwater-codegen/lib.rs b/saltwater-codegen/lib.rs index 0785ab6e..74b8b4a8 100644 --- a/saltwater-codegen/lib.rs +++ b/saltwater-codegen/lib.rs @@ -345,6 +345,44 @@ impl Compiler { pub type Product = ::Product; +/// Compiles a single declaration. +pub fn compile_decl( + module: Module, + decl: Locatable, + debug_asm: bool, +) -> Result, CompileError> { + let mut compiler = Compiler::new(module, debug_asm); + let mut err = None; + + let meta = decl.data.symbol.get(); + + if !matches!(meta.storage_class, StorageClass::Typedef) { + let current = match &meta.ctype { + Type::Function(func_type) => match decl.data.init { + Some(Initializer::FunctionBody(stmts)) => { + compiler.compile_func(decl.data.symbol, &func_type, stmts, decl.location) + } + None => compiler.declare_func(decl.data.symbol, false).map(|_| ()), + _ => unreachable!("functions can only be initialized by a FunctionBody"), + }, + Type::Void | Type::Error => unreachable!("parser let an incomplete type through"), + _ => { + if let Some(Initializer::FunctionBody(_)) = &decl.data.init { + unreachable!("only functions should have a function body") + } + compiler.store_static(decl.data.symbol, decl.data.init, decl.location) + } + }; + err = current.err(); + } + + if let Some(err) = err { + Err(err) + } else { + Ok(compiler.module) + } +} + /// Compile and return the declarations and warnings. pub fn compile(module: Module, buf: &str, opt: Opt) -> Program> { use saltwater_parser::{check_semantics, vec_deque}; diff --git a/saltwater-parser/Cargo.toml b/saltwater-parser/Cargo.toml index 0258881e..439c23de 100644 --- a/saltwater-parser/Cargo.toml +++ b/saltwater-parser/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "saltwater-parser" version = "0.11.0" +authors = ["Joshua Nelson "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/saltwater-parser/analyze/mod.rs b/saltwater-parser/analyze/mod.rs index 7df814c0..ae70b843 100644 --- a/saltwater-parser/analyze/mod.rs +++ b/saltwater-parser/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> { + std::mem::take(&mut self.error_handler.errors) + } + // I type these a lot #[inline(always)] fn err(&mut self, e: SemanticError, l: Location) { diff --git a/saltwater-parser/data/error.rs b/saltwater-parser/data/error.rs index 1bede8d1..939eca14 100644 --- a/saltwater-parser/data/error.rs +++ b/saltwater-parser/data/error.rs @@ -17,7 +17,7 @@ pub type CompileWarning = Locatable; /// `Result`. #[derive(Clone, Debug, PartialEq)] pub struct ErrorHandler { - errors: VecDeque>, + pub errors: VecDeque>, pub warnings: VecDeque, } diff --git a/saltwater-repl/Cargo.toml b/saltwater-repl/Cargo.toml new file mode 100644 index 00000000..903a02db --- /dev/null +++ b/saltwater-repl/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "saltwater-repl" +version = "0.11.0" +authors = ["Justus K "] +edition = "2018" + +[dependencies] +saltwater-codegen = { path = "../saltwater-codegen", features = ["jit"] } +saltwater-parser = { path = "../saltwater-parser" } +rustyline = "6.2" +rustyline-derive = "0.3" +dirs-next = "1.0" +owo-colors = "1.1" +hex = "0.4.2" + +[[bin]] +name = "swcci" +path = "main.rs" diff --git a/saltwater-repl/commands.rs b/saltwater-repl/commands.rs new file mode 100644 index 00000000..cf8ead9f --- /dev/null +++ b/saltwater-repl/commands.rs @@ -0,0 +1,52 @@ +use crate::repl::Repl; +use std::fmt::Write; + +pub struct Command { + pub names: &'static [&'static str], + pub description: &'static str, + pub action: fn(&mut Repl, &str), +} + +pub fn default_commands() -> Vec { + vec![ + Command { + names: &["help", "h"], + description: "Shows this help message", + action: help_command, + }, + Command { + names: &["quit", "q"], + description: "Quits the repl", + action: quit_command, + }, + ] +} + +pub fn generate_help_message(cmds: &[Command]) -> String { + let inner = || { + let mut buf = String::new(); + writeln!(buf, "Available commands:")?; + for cmd in cmds { + let names = cmd.names.iter().copied().collect::>().join("|"); + writeln!( + buf, + "{:>4}{}{:>4}{}", + crate::repl::PREFIX, + names, + "", + cmd.description + )?; + } + Ok::(buf) + }; + inner().expect("failed to generate help message") +} + +fn help_command(repl: &mut Repl, _args: &str) { + println!("{}", repl.help_message); +} + +fn quit_command(repl: &mut Repl, _args: &str) { + repl.save_history(); + std::process::exit(0) +} diff --git a/saltwater-repl/helper.rs b/saltwater-repl/helper.rs new file mode 100644 index 00000000..3d4e60d1 --- /dev/null +++ b/saltwater-repl/helper.rs @@ -0,0 +1,117 @@ +use crate::repl::PREFIX; +use owo_colors::OwoColorize; +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> { + Cow::Owned(hint.dimmed().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 { + if ctx.input().starts_with(crate::repl::PREFIX) { + if self.commands.contains(&&ctx.input()[1..]) { + return Ok(ValidationResult::Valid(None)); + } else { + return Ok(ValidationResult::Invalid(None)); + } + } + + let result = crate::repl::analyze_expr(ctx.input().to_string()); + if let Err(err) = result { + Ok(ValidationResult::Invalid(Some(err.data.to_string()))) + } else { + Ok(ValidationResult::Valid(None)) + } + } +} + +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/saltwater-repl/main.rs b/saltwater-repl/main.rs new file mode 100644 index 00000000..115d51eb --- /dev/null +++ b/saltwater-repl/main.rs @@ -0,0 +1,17 @@ +//! The repl implementation for saltwater. +#![deny(rust_2018_idioms)] + +mod commands; +mod helper; +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/saltwater-repl/repl.rs b/saltwater-repl/repl.rs new file mode 100644 index 00000000..564f2984 --- /dev/null +++ b/saltwater-repl/repl.rs @@ -0,0 +1,226 @@ +use crate::{ + commands::default_commands, commands::generate_help_message, commands::Command, + helper::ReplHelper, +}; +use dirs_next::data_dir; +use rustyline::{error::ReadlineError, Cmd, CompletionType, Config, EditMode, Editor, KeyPress}; +use saltwater_codegen::{compile_decl, initialize_jit_module, JIT}; +use saltwater_parser::{ + data, hir, hir::Declaration, hir::Expr, types, CompileError, Locatable, Parser, + PreProcessorBuilder, PureAnalyzer, Type, +}; +use std::path::PathBuf; + +#[cfg(not(target_pointer_width = "64"))] +compile_error!("only x86_64 is supported"); + +/// The prefix for commands inside the repl. +pub(crate) const PREFIX: char = ':'; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const PROMPT: &str = ">> "; + +/// Takes a function pointer and transmute it into a function +/// that returns the given type. Then executes it and returns the result. +/// +/// # Safety +/// +/// `$fun` has to be a valid pointer to a function that returns the `$ty`. +macro_rules! execute { + ($fun:ident, $ty:tt) => { + unsafe { + let execute = std::mem::transmute::<*const u8, unsafe extern "C" fn() -> $ty>($fun); + execute() + } + }; +} + +pub struct Repl { + editor: Editor, + commands: Vec, + /// Generated help message for all commands. + pub help_message: String, +} + +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 help_message = generate_help_message(&commands); + let helper = ReplHelper::new(commands.iter().flat_map(|cmd| cmd.names).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, + help_message, + } + } + + pub fn run(&mut self) -> rustyline::Result<()> { + self.load_history(); + + println!("Saltwater {}", VERSION); + println!("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 + } + + pub 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 trimmed_line = line.trim(); + if trimmed_line.starts_with(PREFIX) { + let name = trimmed_line.split(' ').next().unwrap(); + + match self + .commands + .iter() + .find(|cmd| cmd.names.contains(&&name[1..])) + { + Some(cmd) => { + let action = cmd.action; + action(self, &trimmed_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: String) -> 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 = compile_decl(module, decl, Default::default())?; + + let mut jit = JIT::from(module); + jit.finalize(); + let fun = jit + .get_compiled_function("execute") + .expect("wrap_expr should create a function named `execute`"); + + match expr_ty { + Type::Short(true) => println!("=> {}", execute!(fun, i16)), + Type::Short(false) => println!("=> {}", execute!(fun, u16)), + + Type::Int(true) => println!("=> {}", execute!(fun, i32)), + Type::Int(false) => println!("=> {}", execute!(fun, u32)), + + Type::Long(true) => println!("=> {}", execute!(fun, i64)), + Type::Long(false) => println!("=> {}", execute!(fun, u64)), + + Type::Float => println!("=> {}", execute!(fun, f32)), + Type::Double => println!("=> {}", execute!(fun, f64)), + + Type::Char(_) => { + let c = execute!(fun, u8); + if c.is_ascii() { + println!("=> {}", c); + } else { + println!("=> {}", hex::encode(&[c])); + } + } + Type::Bool => println!("=> {}", execute!(fun, bool)), + Type::Void => execute!(fun, ()), + + // TODO: Implement execution for more types + ty => println!("error: expression has an 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::Static, + 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) +} + +pub fn analyze_expr(mut code: String) -> Result> { + code.push('\n'); + 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); + + if let Some(err) = analyzer.errors().pop_front() { + return Err(err); + } + + Ok(expr) +} diff --git a/src/main.rs b/src/main.rs index 2eae89fb..d5ec1253 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ static WARNINGS: AtomicUsize = AtomicUsize::new(0); const HELP: &str = concat!( env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"), "\n", - "Jynn Nelson \n", + "Joshua Nelson \n", env!("CARGO_PKG_DESCRIPTION"), "\n", "Homepage: ", env!("CARGO_PKG_REPOSITORY"), "\n", "\n", @@ -428,7 +428,7 @@ fn error(msg: T, location: Location, file_db: &Files, colo let err = { use rand::Rng; - let name = std::env::var("USER").unwrap_or_else(|_| "programmer".into()); + let name = std::env::var("USER").unwrap_or("programmer".into()); shut_up = format!("Shut up, {}. I don't ever want to hear that kind of obvious garbage and idiocy from a developer again", name); let msgs = [ "you can't write code", diff --git a/tests/pre-commit.sh b/tests/pre-commit.sh index b1ad231d..d31dd0fd 100755 --- a/tests/pre-commit.sh +++ b/tests/pre-commit.sh @@ -1,5 +1,5 @@ #!/bin/sh set -ev cargo fmt --all -- --check -cargo clippy --all --all-features -- -D clippy::all -D unused-imports +cargo clippy --all -- -D clippy::all -D unused-imports cargo test --all --all-features diff --git a/tests/runner-tests/hello_world.c b/tests/runner-tests/hello_world.c index 4d5bc093..5bcf3424 100644 --- a/tests/runner-tests/hello_world.c +++ b/tests/runner-tests/hello_world.c @@ -1,4 +1,4 @@ -int puts(const char *); +#include int main() { puts("Hello, world!");