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 1 commit
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
16 changes: 16 additions & 0 deletions saltwater-repl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "saltwater-repl"
version = "0.1.0"
Stupremee marked this conversation as resolved.
Show resolved Hide resolved
authors = ["Justus K <[email protected]>"]
edition = "2018"

[dependencies]
saltwater-codegen = { path = "../saltwater-codegen", features = ["jit"] }
rustyline = "6.2"
rustyline-derive = "0.3"
dirs-next = "1.0"
owo-colors = "1.1"

[[bin]]
name = "swcci"
path = "src/main.rs"
27 changes: 27 additions & 0 deletions saltwater-repl/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::repl::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
Stupremee marked this conversation as resolved.
Show resolved Hide resolved
}

fn help_command(_repl: &mut Repl, _args: &str) {
print!(
"\
Available commands:
{p}help|h Shows this message
{p}quit|q Quits the repl
",
p = crate::repl::PREFIX
);
Stupremee marked this conversation as resolved.
Show resolved Hide resolved
}

fn quit_command(repl: &mut Repl, _args: &str) {
repl.save_history();
std::process::exit(0)
}
126 changes: 126 additions & 0 deletions saltwater-repl/src/helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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<ValidationResult> {
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)
}
Stupremee marked this conversation as resolved.
Show resolved Hide resolved
}
}

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/src/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)]
jyn514 marked this conversation as resolved.
Show resolved Hide resolved

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);
}
}
}
95 changes: 95 additions & 0 deletions saltwater-repl/src/repl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use crate::{commands::default_commands, helper::ReplHelper};
use dirs_next::data_dir;
use rustyline::{error::ReadlineError, Cmd, CompletionType, Config, EditMode, Editor, KeyPress};
use std::{collections::HashMap, path::PathBuf};

/// The prefix for commands inside the repl.
pub(crate) const PREFIX: char = ':';
const VERSION: &str = env!("CARGO_PKG_VERSION");
const PROMPT: &str = ">> ";

pub struct Repl {
editor: Editor<ReplHelper>,
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
}

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<PathBuf> {
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 {
self.execute_code(line);
}
}

fn execute_code(&mut self, _code: &str) {
todo!()
}
}