Skip to content
This repository has been archived by the owner on Jun 3, 2021. It is now read-only.

Implement proper REPL #515

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jit = ["saltwater-codegen/jit"]
_test_headers = []

[workspace]
members = ["saltwater-repl"]

[[bin]]
name = "swcc"
Expand Down
38 changes: 38 additions & 0 deletions saltwater-codegen/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,44 @@ impl<B: Backend> Compiler<B> {

pub type Product = <cranelift_object::ObjectBackend as Backend>::Product;

/// Compiles a single declaration.
pub fn compile_decl<B: Backend>(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks copy/pasted from https://github.com/jyn514/saltwater/pull/515/files#diff-376fa271c6951618907ff944ff30e93aL368. Rather than duplicating the code, can you change that to call this function instead? That way they're consistent.

module: Module<B>,
decl: Locatable<Declaration>,
debug_asm: bool,
) -> Result<Module<B>, 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<B: Backend>(module: Module<B>, buf: &str, opt: Opt) -> Program<Module<B>> {
use saltwater_parser::{check_semantics, vec_deque};
Expand Down
9 changes: 9 additions & 0 deletions saltwater-parser/analyze/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ impl PureAnalyzer {
pub fn warnings(&mut self) -> VecDeque<CompileWarning> {
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<Locatable<Error>> {
Stupremee marked this conversation as resolved.
Show resolved Hide resolved
std::mem::take(&mut self.error_handler.errors)
}

// I type these a lot
#[inline(always)]
fn err(&mut self, e: SemanticError, l: Location) {
Expand Down
2 changes: 1 addition & 1 deletion saltwater-parser/data/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub type CompileWarning = Locatable<Warning>;
/// `Result`.
#[derive(Clone, Debug, PartialEq)]
pub struct ErrorHandler<T = Error> {
errors: VecDeque<Locatable<T>>,
pub errors: VecDeque<Locatable<T>>,
pub warnings: VecDeque<CompileWarning>,
}

Expand Down
18 changes: 18 additions & 0 deletions saltwater-repl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "saltwater-repl"
version = "0.11.0"
authors = ["Justus K <[email protected]>"]
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"
52 changes: 52 additions & 0 deletions saltwater-repl/commands.rs
Original file line number Diff line number Diff line change
@@ -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<Command> {
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::<Vec<_>>().join("|");
writeln!(
buf,
"{:>4}{}{:>4}{}",
crate::repl::PREFIX,
names,
"",
cmd.description
)?;
}
Ok::<String, std::fmt::Error>(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) {
Stupremee marked this conversation as resolved.
Show resolved Hide resolved
repl.save_history();
std::process::exit(0)
}
117 changes: 117 additions & 0 deletions saltwater-repl/helper.rs
Original file line number Diff line number Diff line change
@@ -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())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shame this requires a Cow specifically instead of any impl Display. Not sure if that works with traits though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea it is. It would save so many allocations

}

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<ValidationResult> {
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<String> {
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<Self::Candidate>)> {
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::<Vec<_>>();

Ok((idx + 1, commands))
}

// TODO: Complete method names, types, etc.
}
17 changes: 17 additions & 0 deletions saltwater-repl/main.rs
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, these errors are only sometimes showing up.

>> "a";
error: expression returns unsupported type: Array(Char(true), Fixed(2))
>> @
# I pressed ctrl+c
>> x = 1

>>

Copy link
Contributor Author

@Stupremee Stupremee Sep 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines only print the error that is returned by rustyline if something terminal related fails.
The input isn't valid because the Validator checks if the input is a valid expression (which fails because x is not declared, I need to fix that).

Imo it's better to remove the "only expressions are valid input" check because it can be really confusing

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but this isn't reporting the error at all, which is a bug. Do you know how to fix that? I'm not sure entirely where the syntax errors are bubbling up - maybe in execute_code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errors are reported in process_line

std::process::exit(1);
}
}
}
Loading