mirror of
https://github.com/roc-lang/roc.git
synced 2024-09-11 18:58:24 +03:00
Remove duplicated code from roc_repl_cli and get tests compiling
This commit is contained in:
parent
8f59ee9492
commit
3923dad203
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -3062,9 +3062,14 @@ dependencies = [
|
||||
"roc_build",
|
||||
"roc_cli",
|
||||
"roc_repl_cli",
|
||||
"roc_repl_ui",
|
||||
"roc_reporting",
|
||||
"roc_target",
|
||||
"roc_test_utils",
|
||||
"roc_wasm_interp",
|
||||
"rustyline",
|
||||
"strip-ansi-escapes",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3858,6 +3863,7 @@ dependencies = [
|
||||
"roc_parse",
|
||||
"roc_region",
|
||||
"roc_repl_eval",
|
||||
"roc_repl_ui",
|
||||
"roc_reporting",
|
||||
"roc_std",
|
||||
"roc_target",
|
||||
|
@ -29,6 +29,7 @@ roc_std = { path = "../roc_std" }
|
||||
roc_target = { path = "../compiler/roc_target" }
|
||||
roc_types = { path = "../compiler/types" }
|
||||
roc_error_macros = { path = "../error_macros" }
|
||||
roc_repl_ui = { path = "../repl_ui" }
|
||||
|
||||
bumpalo.workspace = true
|
||||
const_format.workspace = true
|
||||
|
@ -1,4 +0,0 @@
|
||||
pub const BLUE: &str = "\u{001b}[36m";
|
||||
pub const PINK: &str = "\u{001b}[35m";
|
||||
pub const GREEN: &str = "\u{001b}[32m";
|
||||
pub const END_COL: &str = "\u{001b}[0m";
|
@ -1,18 +1,15 @@
|
||||
//! Command Line Interface (CLI) functionality for the Read-Evaluate-Print-Loop (REPL).
|
||||
mod cli_gen;
|
||||
mod colors;
|
||||
pub mod repl_state;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use bumpalo::Bump;
|
||||
use colors::{BLUE, END_COL, GREEN, PINK};
|
||||
use const_format::concatcp;
|
||||
use repl_state::ReplAction;
|
||||
use repl_state::{parse_src, ParseOutcome, ReplState};
|
||||
use roc_load::MonomorphizedModule;
|
||||
use roc_mono::ir::OptLevel;
|
||||
use roc_parse::ast::{Expr, ValueDef};
|
||||
use roc_repl_eval::gen::{Problems, ReplOutput};
|
||||
use roc_repl_ui::colors::{END_COL, GREEN, PINK};
|
||||
use roc_repl_ui::repl_state::{ReplAction, ReplState};
|
||||
use roc_repl_ui::{is_incomplete, CONT_PROMPT, PROMPT, SHORT_INSTRUCTIONS, TIPS, WELCOME_MESSAGE};
|
||||
use roc_reporting::report::DEFAULT_PALETTE;
|
||||
use roc_target::TargetInfo;
|
||||
use rustyline::highlight::{Highlighter, PromptInfo};
|
||||
@ -22,62 +19,6 @@ use target_lexicon::Triple;
|
||||
|
||||
use crate::cli_gen::eval_llvm;
|
||||
|
||||
pub const WELCOME_MESSAGE: &str = concatcp!(
|
||||
"\n The rockin’ ",
|
||||
BLUE,
|
||||
"roc repl",
|
||||
END_COL,
|
||||
"\n",
|
||||
PINK,
|
||||
"────────────────────────",
|
||||
END_COL,
|
||||
"\n\n"
|
||||
);
|
||||
|
||||
// TODO add link to repl tutorial(does not yet exist).
|
||||
pub const TIPS: &str = concatcp!(
|
||||
"\nEnter an expression to evaluate, or a definition (like ",
|
||||
BLUE,
|
||||
"x = 1",
|
||||
END_COL,
|
||||
") to use in future expressions.\n\nUnless there was a compile-time error, expressions get automatically named so you can refer to them later.\nFor example, if you see ",
|
||||
GREEN,
|
||||
"# val1",
|
||||
END_COL,
|
||||
" after an output, you can now refer to that expression as ",
|
||||
BLUE,
|
||||
"val1",
|
||||
END_COL,
|
||||
" in future expressions.\n\n",
|
||||
"Tips:\n\n",
|
||||
BLUE,
|
||||
" - ",
|
||||
END_COL,
|
||||
PINK,
|
||||
"ctrl-v",
|
||||
END_COL,
|
||||
" + ",
|
||||
PINK,
|
||||
"ctrl-j",
|
||||
END_COL,
|
||||
" makes a newline\n\n",
|
||||
BLUE,
|
||||
" - ",
|
||||
END_COL,
|
||||
":q to quit\n\n",
|
||||
BLUE,
|
||||
" - ",
|
||||
END_COL,
|
||||
":help"
|
||||
);
|
||||
|
||||
// For when nothing is entered in the repl
|
||||
// TODO add link to repl tutorial(does not yet exist).
|
||||
pub const SHORT_INSTRUCTIONS: &str = "Enter an expression, or :help, or :q to quit.\n\n";
|
||||
|
||||
pub const PROMPT: &str = concatcp!(BLUE, "»", END_COL, " ");
|
||||
pub const CONT_PROMPT: &str = concatcp!(BLUE, "…", END_COL, " ");
|
||||
|
||||
#[derive(Completer, Helper, Hinter, Default)]
|
||||
pub struct ReplHelper {
|
||||
validator: InputValidator,
|
||||
@ -120,10 +61,8 @@ pub fn main() -> i32 {
|
||||
problems,
|
||||
opt_var_name,
|
||||
} => {
|
||||
let opt_output =
|
||||
opt_mono.and_then(|mono| eval_llvm(mono, &target, OptLevel::Normal));
|
||||
let output = format_output(opt_output, problems, opt_var_name, dimensions);
|
||||
|
||||
let output =
|
||||
evaluate(opt_mono, problems, opt_var_name, &target, dimensions);
|
||||
if !output.is_empty() {
|
||||
println!("{output}");
|
||||
}
|
||||
@ -157,6 +96,17 @@ pub fn main() -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn evaluate<'a>(
|
||||
opt_mono: Option<MonomorphizedModule<'a>>,
|
||||
problems: Problems,
|
||||
opt_var_name: Option<String>,
|
||||
target: &Triple,
|
||||
dimensions: Option<(usize, usize)>,
|
||||
) -> String {
|
||||
let opt_output = opt_mono.and_then(|mono| eval_llvm(mono, target, OptLevel::Normal));
|
||||
format_output(opt_output, problems, opt_var_name, dimensions)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InputValidator {}
|
||||
|
||||
@ -170,32 +120,6 @@ impl Validator for InputValidator {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_incomplete(input: &str) -> bool {
|
||||
let arena = Bump::new();
|
||||
|
||||
match parse_src(&arena, input) {
|
||||
ParseOutcome::Incomplete => !input.ends_with('\n'),
|
||||
// Standalone annotations are default incomplete, because we can't know
|
||||
// whether they're about to annotate a body on the next line
|
||||
// (or if not, meaning they stay standalone) until you press Enter again!
|
||||
//
|
||||
// So it's Incomplete until you've pressed Enter again (causing the input to end in "\n")
|
||||
ParseOutcome::ValueDef(ValueDef::Annotation(_, _)) if !input.ends_with('\n') => true,
|
||||
ParseOutcome::Expr(Expr::When(_, _)) => {
|
||||
// There might be lots of `when` branches, so don't assume the user is done entering
|
||||
// them until they enter a blank line!
|
||||
!input.ends_with('\n')
|
||||
}
|
||||
ParseOutcome::Empty
|
||||
| ParseOutcome::Help
|
||||
| ParseOutcome::Exit
|
||||
| ParseOutcome::ValueDef(_)
|
||||
| ParseOutcome::TypeDef(_)
|
||||
| ParseOutcome::SyntaxErr
|
||||
| ParseOutcome::Expr(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for ReplHelper {
|
||||
fn has_continuation_prompt(&self) -> bool {
|
||||
true
|
||||
|
@ -1,431 +0,0 @@
|
||||
use bumpalo::Bump;
|
||||
use roc_collections::MutSet;
|
||||
use roc_load::MonomorphizedModule;
|
||||
use roc_parse::ast::{Expr, Pattern, TypeDef, TypeHeader, ValueDef};
|
||||
use roc_parse::expr::{parse_single_def, ExprParseOptions, SingleDef};
|
||||
use roc_parse::parser::Parser;
|
||||
use roc_parse::parser::{EClosure, EExpr, EPattern};
|
||||
use roc_parse::parser::{EWhen, Either};
|
||||
use roc_parse::state::State;
|
||||
use roc_parse::{join_alias_to_body, join_ann_to_body};
|
||||
use roc_region::all::Loc;
|
||||
use roc_repl_eval::gen::{compile_to_mono, Problems};
|
||||
use roc_reporting::report::Palette;
|
||||
use roc_target::TargetInfo;
|
||||
|
||||
/// The prefix we use for the automatic variable names we assign to each expr,
|
||||
/// e.g. if the prefix is "val" then the first expr you enter will be named "val1"
|
||||
pub const AUTO_VAR_PREFIX: &str = "val";
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct PastDef {
|
||||
ident: String,
|
||||
src: String,
|
||||
}
|
||||
|
||||
pub struct ReplState {
|
||||
past_defs: Vec<PastDef>,
|
||||
past_def_idents: MutSet<String>,
|
||||
last_auto_ident: u64,
|
||||
}
|
||||
|
||||
impl Default for ReplState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ReplAction<'a> {
|
||||
Eval {
|
||||
opt_mono: Option<MonomorphizedModule<'a>>,
|
||||
problems: Problems,
|
||||
opt_var_name: Option<String>,
|
||||
},
|
||||
Exit,
|
||||
Help,
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl ReplState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
past_defs: Default::default(),
|
||||
past_def_idents: Default::default(),
|
||||
last_auto_ident: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step<'a>(
|
||||
&mut self,
|
||||
arena: &'a Bump,
|
||||
line: &str,
|
||||
target_info: TargetInfo,
|
||||
palette: Palette,
|
||||
) -> ReplAction<'a> {
|
||||
match parse_src(&arena, line) {
|
||||
ParseOutcome::Empty | ParseOutcome::Help => ReplAction::Help,
|
||||
ParseOutcome::Expr(_)
|
||||
| ParseOutcome::ValueDef(_)
|
||||
| ParseOutcome::TypeDef(_)
|
||||
| ParseOutcome::SyntaxErr
|
||||
| ParseOutcome::Incomplete => self.next_action(arena, line, target_info, palette),
|
||||
ParseOutcome::Exit => ReplAction::Exit,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_action<'a>(
|
||||
&mut self,
|
||||
arena: &'a Bump,
|
||||
src: &str,
|
||||
target_info: TargetInfo,
|
||||
palette: Palette,
|
||||
) -> ReplAction<'a> {
|
||||
let pending_past_def;
|
||||
let mut opt_var_name;
|
||||
let src = match parse_src(&arena, src) {
|
||||
ParseOutcome::Expr(_) | ParseOutcome::Incomplete | ParseOutcome::SyntaxErr => {
|
||||
pending_past_def = None;
|
||||
// If it's a SyntaxErr (or Incomplete at this point, meaning it will
|
||||
// become a SyntaxErr as soon as we evaluate it),
|
||||
// proceed as normal and let the error reporting happen during eval.
|
||||
opt_var_name = None;
|
||||
|
||||
src
|
||||
}
|
||||
ParseOutcome::ValueDef(value_def) => {
|
||||
match value_def {
|
||||
ValueDef::Annotation(
|
||||
Loc {
|
||||
value: Pattern::Identifier(ident),
|
||||
..
|
||||
},
|
||||
_,
|
||||
) => {
|
||||
// Record the standalone type annotation for future use.
|
||||
self.add_past_def(ident.trim_end().to_string(), src.to_string());
|
||||
|
||||
// Return early without running eval, since standalone annotations
|
||||
// cannnot be evaluated as expressions.
|
||||
return ReplAction::Nothing;
|
||||
}
|
||||
ValueDef::Body(
|
||||
Loc {
|
||||
value: Pattern::Identifier(ident),
|
||||
..
|
||||
},
|
||||
_,
|
||||
)
|
||||
| ValueDef::AnnotatedBody {
|
||||
body_pattern:
|
||||
Loc {
|
||||
value: Pattern::Identifier(ident),
|
||||
..
|
||||
},
|
||||
..
|
||||
} => {
|
||||
pending_past_def = Some((ident.to_string(), src.to_string()));
|
||||
opt_var_name = Some(ident.to_string());
|
||||
|
||||
// Recreate the body of the def and then evaluate it as a lookup.
|
||||
// We do this so that any errors will get reported as part of this expr;
|
||||
// if we just did a lookup on the past def, then errors wouldn't get
|
||||
// reported because we filter out errors whose regions are in past defs.
|
||||
let mut buf = bumpalo::collections::string::String::with_capacity_in(
|
||||
ident.len() + src.len() + 1,
|
||||
&arena,
|
||||
);
|
||||
|
||||
buf.push_str(src);
|
||||
buf.push('\n');
|
||||
buf.push_str(ident);
|
||||
|
||||
buf.into_bump_str()
|
||||
}
|
||||
ValueDef::Annotation(_, _)
|
||||
| ValueDef::Body(_, _)
|
||||
| ValueDef::AnnotatedBody { .. } => {
|
||||
todo!("handle pattern other than identifier (which repl doesn't support)")
|
||||
}
|
||||
ValueDef::Dbg { .. } => {
|
||||
todo!("handle receiving a `dbg` - what should the repl do for that?")
|
||||
}
|
||||
ValueDef::Expect { .. } => {
|
||||
todo!("handle receiving an `expect` - what should the repl do for that?")
|
||||
}
|
||||
ValueDef::ExpectFx { .. } => {
|
||||
todo!("handle receiving an `expect-fx` - what should the repl do for that?")
|
||||
}
|
||||
}
|
||||
}
|
||||
ParseOutcome::TypeDef(TypeDef::Alias {
|
||||
header:
|
||||
TypeHeader {
|
||||
name: Loc { value: ident, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
| ParseOutcome::TypeDef(TypeDef::Opaque {
|
||||
header:
|
||||
TypeHeader {
|
||||
name: Loc { value: ident, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
| ParseOutcome::TypeDef(TypeDef::Ability {
|
||||
header:
|
||||
TypeHeader {
|
||||
name: Loc { value: ident, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
}) => {
|
||||
// Record the type for future use.
|
||||
self.add_past_def(ident.trim_end().to_string(), src.to_string());
|
||||
|
||||
// Return early without running eval, since none of these
|
||||
// can be evaluated as expressions.
|
||||
return ReplAction::Nothing;
|
||||
}
|
||||
ParseOutcome::Empty | ParseOutcome::Help | ParseOutcome::Exit => unreachable!(),
|
||||
};
|
||||
|
||||
// Record e.g. "val1" as a past def, unless our input was exactly the name of
|
||||
// an existing identifer (e.g. I just typed "val1" into the prompt - there's no
|
||||
// need to reassign "val1" to "val2" just because I wanted to see what its value was!)
|
||||
let (opt_mono, problems) =
|
||||
match opt_var_name.or_else(|| self.past_def_idents.get(src.trim()).cloned()) {
|
||||
Some(existing_ident) => {
|
||||
opt_var_name = Some(existing_ident);
|
||||
|
||||
compile_to_mono(
|
||||
&arena,
|
||||
self.past_defs.iter().map(|def| def.src.as_str()),
|
||||
src,
|
||||
target_info,
|
||||
palette,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
let (output, problems) = compile_to_mono(
|
||||
&arena,
|
||||
self.past_defs.iter().map(|def| def.src.as_str()),
|
||||
src,
|
||||
target_info,
|
||||
palette,
|
||||
);
|
||||
|
||||
// Don't persist defs that have compile errors
|
||||
if problems.errors.is_empty() {
|
||||
let var_name = format!("{AUTO_VAR_PREFIX}{}", self.next_auto_ident());
|
||||
let src = format!("{var_name} = {}", src.trim_end());
|
||||
|
||||
opt_var_name = Some(var_name.clone());
|
||||
|
||||
self.add_past_def(var_name, src);
|
||||
} else {
|
||||
opt_var_name = None;
|
||||
}
|
||||
|
||||
(output, problems)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((ident, src)) = pending_past_def {
|
||||
self.add_past_def(ident, src);
|
||||
}
|
||||
|
||||
ReplAction::Eval {
|
||||
opt_mono,
|
||||
problems,
|
||||
opt_var_name,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_auto_ident(&mut self) -> u64 {
|
||||
self.last_auto_ident += 1;
|
||||
self.last_auto_ident
|
||||
}
|
||||
|
||||
fn add_past_def(&mut self, ident: String, src: String) {
|
||||
let existing_idents = &mut self.past_def_idents;
|
||||
|
||||
existing_idents.insert(ident.clone());
|
||||
|
||||
self.past_defs.push(PastDef { ident, src });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseOutcome<'a> {
|
||||
ValueDef(ValueDef<'a>),
|
||||
TypeDef(TypeDef<'a>),
|
||||
Expr(Expr<'a>),
|
||||
Incomplete,
|
||||
SyntaxErr,
|
||||
Empty,
|
||||
Help,
|
||||
Exit,
|
||||
}
|
||||
|
||||
pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
|
||||
match line.trim().to_lowercase().as_str() {
|
||||
"" => ParseOutcome::Empty,
|
||||
":help" => ParseOutcome::Help,
|
||||
":exit" | ":quit" | ":q" => ParseOutcome::Exit,
|
||||
_ => {
|
||||
let src_bytes = line.as_bytes();
|
||||
|
||||
match roc_parse::expr::loc_expr(true).parse(arena, State::new(src_bytes), 0) {
|
||||
Ok((_, loc_expr, _)) => ParseOutcome::Expr(loc_expr.value),
|
||||
// Special case some syntax errors to allow for multi-line inputs
|
||||
Err((_, EExpr::Closure(EClosure::Body(_, _), _)))
|
||||
| Err((_, EExpr::When(EWhen::Pattern(EPattern::Start(_), _), _)))
|
||||
| Err((_, EExpr::Start(_)))
|
||||
| Err((_, EExpr::IndentStart(_))) => ParseOutcome::Incomplete,
|
||||
Err((_, EExpr::DefMissingFinalExpr(_)))
|
||||
| Err((_, EExpr::DefMissingFinalExpr2(_, _))) => {
|
||||
// This indicates that we had an attempted def; re-parse it as a single-line def.
|
||||
match parse_single_def(
|
||||
ExprParseOptions {
|
||||
accept_multi_backpassing: true,
|
||||
check_for_arrow: true,
|
||||
},
|
||||
0,
|
||||
arena,
|
||||
State::new(src_bytes),
|
||||
) {
|
||||
Ok((
|
||||
_,
|
||||
Some(SingleDef {
|
||||
type_or_value: Either::First(TypeDef::Alias { header, ann }),
|
||||
..
|
||||
}),
|
||||
state,
|
||||
)) => {
|
||||
// This *could* be an AnnotatedBody, e.g. in a case like this:
|
||||
//
|
||||
// UserId x : [UserId Int]
|
||||
// UserId x = UserId 42
|
||||
//
|
||||
// We optimistically parsed the first line as an alias; we might now
|
||||
// turn it into an annotation.
|
||||
match parse_single_def(
|
||||
ExprParseOptions {
|
||||
accept_multi_backpassing: true,
|
||||
check_for_arrow: true,
|
||||
},
|
||||
0,
|
||||
arena,
|
||||
state,
|
||||
) {
|
||||
Ok((
|
||||
_,
|
||||
Some(SingleDef {
|
||||
type_or_value:
|
||||
Either::Second(ValueDef::Body(loc_pattern, loc_def_expr)),
|
||||
region,
|
||||
spaces_before,
|
||||
}),
|
||||
_,
|
||||
)) if spaces_before.len() <= 1 => {
|
||||
// This was, in fact, an AnnotatedBody! Build and return it.
|
||||
let (value_def, _) = join_alias_to_body!(
|
||||
arena,
|
||||
loc_pattern,
|
||||
loc_def_expr,
|
||||
header,
|
||||
&ann,
|
||||
spaces_before,
|
||||
region
|
||||
);
|
||||
|
||||
ParseOutcome::ValueDef(value_def)
|
||||
}
|
||||
_ => {
|
||||
// This was not an AnnotatedBody, so return the alias.
|
||||
ParseOutcome::TypeDef(TypeDef::Alias { header, ann })
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
_,
|
||||
Some(SingleDef {
|
||||
type_or_value:
|
||||
Either::Second(ValueDef::Annotation(ann_pattern, ann_type)),
|
||||
..
|
||||
}),
|
||||
state,
|
||||
)) => {
|
||||
// This *could* be an AnnotatedBody, if the next line is a body.
|
||||
match parse_single_def(
|
||||
ExprParseOptions {
|
||||
accept_multi_backpassing: true,
|
||||
check_for_arrow: true,
|
||||
},
|
||||
0,
|
||||
arena,
|
||||
state,
|
||||
) {
|
||||
Ok((
|
||||
_,
|
||||
Some(SingleDef {
|
||||
type_or_value:
|
||||
Either::Second(ValueDef::Body(loc_pattern, loc_def_expr)),
|
||||
region,
|
||||
spaces_before,
|
||||
}),
|
||||
_,
|
||||
)) if spaces_before.len() <= 1 => {
|
||||
// Inlining this borrow makes clippy unhappy for some reason.
|
||||
let ann_pattern = &ann_pattern;
|
||||
|
||||
// This was, in fact, an AnnotatedBody! Build and return it.
|
||||
let (value_def, _) = join_ann_to_body!(
|
||||
arena,
|
||||
loc_pattern,
|
||||
loc_def_expr,
|
||||
ann_pattern,
|
||||
&ann_type,
|
||||
spaces_before,
|
||||
region
|
||||
);
|
||||
|
||||
ParseOutcome::ValueDef(value_def)
|
||||
}
|
||||
_ => {
|
||||
// This was not an AnnotatedBody, so return the standalone annotation.
|
||||
ParseOutcome::ValueDef(ValueDef::Annotation(
|
||||
ann_pattern,
|
||||
ann_type,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
_,
|
||||
Some(SingleDef {
|
||||
type_or_value: Either::First(type_def),
|
||||
..
|
||||
}),
|
||||
_,
|
||||
)) => ParseOutcome::TypeDef(type_def),
|
||||
Ok((
|
||||
_,
|
||||
Some(SingleDef {
|
||||
type_or_value: Either::Second(value_def),
|
||||
..
|
||||
}),
|
||||
_,
|
||||
)) => ParseOutcome::ValueDef(value_def),
|
||||
Ok((_, None, _)) => {
|
||||
todo!("TODO determine appropriate ParseOutcome for Ok(None)")
|
||||
}
|
||||
Err(_) => ParseOutcome::SyntaxErr,
|
||||
}
|
||||
}
|
||||
Err(_) => ParseOutcome::SyntaxErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,12 +13,17 @@ roc_cli = { path = "../cli" }
|
||||
[dev-dependencies]
|
||||
roc_build = { path = "../compiler/build" }
|
||||
roc_repl_cli = { path = "../repl_cli" }
|
||||
roc_repl_ui = { path = "../repl_ui" }
|
||||
roc_test_utils = { path = "../test_utils" }
|
||||
roc_wasm_interp = { path = "../wasm_interp" }
|
||||
roc_reporting = { path = "../reporting" }
|
||||
roc_target = { path = "../compiler/roc_target" }
|
||||
|
||||
bumpalo.workspace = true
|
||||
indoc.workspace = true
|
||||
strip-ansi-escapes.workspace = true
|
||||
target-lexicon.workspace = true
|
||||
rustyline.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["target-aarch64", "target-x86_64", "target-wasm32"]
|
||||
|
@ -3,7 +3,7 @@ use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, ExitStatus, Stdio};
|
||||
|
||||
use roc_repl_cli::{SHORT_INSTRUCTIONS, WELCOME_MESSAGE};
|
||||
use roc_repl_ui::{SHORT_INSTRUCTIONS, WELCOME_MESSAGE};
|
||||
use roc_test_utils::assert_multiline_str_eq;
|
||||
|
||||
const ERROR_MESSAGE_START: char = '─';
|
||||
|
@ -1,5 +1,12 @@
|
||||
use bumpalo::Bump;
|
||||
use indoc::indoc;
|
||||
use roc_repl_cli::repl_state::{is_incomplete, ReplState, TIPS};
|
||||
use roc_repl_cli::{evaluate, ReplHelper};
|
||||
use roc_repl_ui::is_incomplete;
|
||||
use roc_repl_ui::repl_state::{ReplAction, ReplState};
|
||||
use roc_reporting::report::DEFAULT_PALETTE;
|
||||
use roc_target::TargetInfo;
|
||||
use rustyline::Editor;
|
||||
use target_lexicon::Triple;
|
||||
|
||||
// These are tests of the REPL state machine. They work without actually
|
||||
// running the CLI, and without using rustyline, and instead verify
|
||||
@ -7,27 +14,27 @@ use roc_repl_cli::repl_state::{is_incomplete, ReplState, TIPS};
|
||||
|
||||
#[test]
|
||||
fn one_plus_one() {
|
||||
complete("1 + 1", &mut ReplState::new(), Ok(("2 : Num *", "val1")));
|
||||
complete("1 + 1", &mut ReplState::new(), "2 : Num *", "val1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_expr_names() {
|
||||
let mut state = ReplState::new();
|
||||
|
||||
complete("2 * 3", &mut state, Ok(("6 : Num *", "val1")));
|
||||
complete("4 - 1", &mut state, Ok(("3 : Num *", "val2")));
|
||||
complete("val1 + val2", &mut state, Ok(("9 : Num *", "val3")));
|
||||
complete("1 + (val2 * val3)", &mut state, Ok(("28 : Num *", "val4")));
|
||||
complete("2 * 3", &mut state, "6 : Num *", "val1");
|
||||
complete("4 - 1", &mut state, "3 : Num *", "val2");
|
||||
complete("val1 + val2", &mut state, "9 : Num *", "val3");
|
||||
complete("1 + (val2 * val3)", &mut state, "28 : Num *", "val4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_defs() {
|
||||
let mut state = ReplState::new();
|
||||
|
||||
complete("x = 5", &mut state, Ok(("5 : Num *", "x")));
|
||||
complete("7 - 3", &mut state, Ok(("4 : Num *", "val1")));
|
||||
complete("y = 6", &mut state, Ok(("6 : Num *", "y")));
|
||||
complete("val1 + x + y", &mut state, Ok(("15 : Num *", "val2")));
|
||||
complete("x = 5", &mut state, "5 : Num *", "x");
|
||||
complete("7 - 3", &mut state, "4 : Num *", "val1");
|
||||
complete("y = 6", &mut state, "6 : Num *", "y");
|
||||
complete("val1 + x + y", &mut state, "15 : Num *", "val2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -38,7 +45,7 @@ fn annotated_body() {
|
||||
|
||||
input.push_str("t = A");
|
||||
|
||||
complete(&input, &mut ReplState::new(), Ok(("A : [A, B, C]", "t")));
|
||||
complete(&input, &mut ReplState::new(), "A : [A, B, C]", "t");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -53,7 +60,7 @@ fn exhaustiveness_problem() {
|
||||
|
||||
input.push_str("t = A");
|
||||
|
||||
complete(&input, &mut state, Ok(("A : [A, B, C]", "t")));
|
||||
complete(&input, &mut state, "A : [A, B, C]", "t");
|
||||
}
|
||||
|
||||
// Run a `when` on it that isn't exhaustive
|
||||
@ -88,7 +95,11 @@ fn exhaustiveness_problem() {
|
||||
#[test]
|
||||
fn tips() {
|
||||
assert!(!is_incomplete(""));
|
||||
assert_eq!(ReplState::new().step("", None), Ok(TIPS.to_string()));
|
||||
let arena = Bump::new();
|
||||
let target = Triple::host();
|
||||
let target_info = TargetInfo::from(&target);
|
||||
let action = ReplState::default().step(&arena, "", target_info, DEFAULT_PALETTE);
|
||||
assert!(matches!(action, ReplAction::Help));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -98,35 +109,49 @@ fn standalone_annotation() {
|
||||
|
||||
incomplete(&mut input);
|
||||
assert!(!is_incomplete(&input));
|
||||
assert_eq!(state.step(&input, None), Ok(String::new()));
|
||||
let arena = Bump::new();
|
||||
let target = Triple::host();
|
||||
let target_info = TargetInfo::from(&target);
|
||||
let action = state.step(&arena, &input, target_info, DEFAULT_PALETTE);
|
||||
assert!(matches!(action, ReplAction::Nothing));
|
||||
}
|
||||
|
||||
/// validate and step the given input, then check the Result vs the output
|
||||
/// with ANSI escape codes stripped.
|
||||
fn complete(input: &str, state: &mut ReplState, expected_step_result: Result<(&str, &str), i32>) {
|
||||
fn complete(input: &str, state: &mut ReplState, expected_start: &str, expected_end: &str) {
|
||||
assert!(!is_incomplete(input));
|
||||
let arena = Bump::new();
|
||||
let target = Triple::host();
|
||||
let target_info = TargetInfo::from(&target);
|
||||
let action = state.step(&arena, input, target_info, DEFAULT_PALETTE);
|
||||
let repl_helper = ReplHelper::default();
|
||||
let mut editor = Editor::<ReplHelper>::new();
|
||||
editor.set_helper(Some(repl_helper));
|
||||
let dimensions = editor.dimensions();
|
||||
|
||||
match state.step(input, None) {
|
||||
Ok(string) => {
|
||||
match action {
|
||||
ReplAction::Eval {
|
||||
opt_mono,
|
||||
problems,
|
||||
opt_var_name,
|
||||
} => {
|
||||
let string = evaluate(opt_mono, problems, opt_var_name, &target, dimensions);
|
||||
let escaped =
|
||||
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let comment_index = escaped.rfind('#').unwrap_or(escaped.len());
|
||||
|
||||
assert_eq!(
|
||||
expected_step_result.map(|(starts_with, _)| starts_with),
|
||||
Ok(escaped[0..comment_index].trim())
|
||||
);
|
||||
assert_eq!(expected_start, (escaped[0..comment_index].trim()));
|
||||
|
||||
assert_eq!(
|
||||
expected_step_result.map(|(_, ends_with)| ends_with),
|
||||
expected_end,
|
||||
// +1 because we want to skip over the '#' itself
|
||||
Ok(escaped[comment_index + 1..].trim())
|
||||
(escaped[comment_index + 1..].trim())
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
assert_eq!(expected_step_result, Err(err));
|
||||
_ => {
|
||||
assert!(false, "Unexpected action: {:?}", action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,10 +168,29 @@ fn incomplete(input: &mut String) {
|
||||
/// with ANSI escape codes stripped.
|
||||
fn error(input: &str, state: &mut ReplState, expected_step_result: String) {
|
||||
assert!(!is_incomplete(input));
|
||||
let arena = Bump::new();
|
||||
let target = Triple::host();
|
||||
let target_info = TargetInfo::from(&target);
|
||||
let action = state.step(&arena, input, target_info, DEFAULT_PALETTE);
|
||||
let repl_helper = ReplHelper::default();
|
||||
let mut editor = Editor::<ReplHelper>::new();
|
||||
editor.set_helper(Some(repl_helper));
|
||||
let dimensions = editor.dimensions();
|
||||
|
||||
let escaped = state.step(input, None).map(|string| {
|
||||
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap()).unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(Ok(expected_step_result), escaped);
|
||||
match action {
|
||||
ReplAction::Eval {
|
||||
opt_mono,
|
||||
problems,
|
||||
opt_var_name,
|
||||
} => {
|
||||
let string = evaluate(opt_mono, problems, opt_var_name, &target, dimensions);
|
||||
let escaped =
|
||||
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(expected_step_result, escaped);
|
||||
}
|
||||
_ => {
|
||||
assert!(false, "Unexpected action: {:?}", action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "roc_repl_ui"
|
||||
description = "UI code for the Roc REPL, shared between CLI and WebAssembly versions."
|
||||
description = "UI for the Roc REPL, shared between CLI and WebAssembly versions."
|
||||
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! Command Line Interface (CLI) functionality for the Read-Evaluate-Print-Loop (REPL).
|
||||
//! UI functionality, shared between CLI and web, for the Read-Evaluate-Print-Loop (REPL).
|
||||
pub mod colors;
|
||||
pub mod repl_state;
|
||||
|
||||
|
@ -34,6 +34,7 @@ impl Default for ReplState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReplAction<'a> {
|
||||
Eval {
|
||||
opt_mono: Option<MonomorphizedModule<'a>>,
|
||||
|
Loading…
Reference in New Issue
Block a user