Create a new crate roc_repl_ui for shared CLI/web UI code

This commit is contained in:
Brian Carroll 2023-09-09 11:55:55 +01:00
parent dbf928bc46
commit 8f59ee9492
No known key found for this signature in database
GPG Key ID: 5C7B2EC4101703C0
13 changed files with 701 additions and 8 deletions

17
Cargo.lock generated
View File

@ -3854,7 +3854,6 @@ dependencies = [
"roc_error_macros",
"roc_gen_llvm",
"roc_load",
"roc_module",
"roc_mono",
"roc_parse",
"roc_region",
@ -3925,6 +3924,22 @@ dependencies = [
"tempfile",
]
[[package]]
name = "roc_repl_ui"
version = "0.0.1"
dependencies = [
"bumpalo",
"const_format",
"roc_collections",
"roc_load",
"roc_parse",
"roc_region",
"roc_repl_eval",
"roc_reporting",
"roc_target",
"unicode-segmentation",
]
[[package]]
name = "roc_repl_wasm"
version = "0.0.1"

View File

@ -15,6 +15,7 @@ members = [
"crates/repl_cli",
"crates/repl_eval",
"crates/repl_test",
"crates/repl_ui",
"crates/repl_wasm",
"crates/repl_expect",
"crates/roc_std",

View File

@ -104,6 +104,12 @@ Command Line Interface(CLI) functionality for the Read-Evaluate-Print-Loop (REPL
Provides the functionality for the REPL to evaluate Roc expressions.
## `repl_state/` - `roc_repl_state`
Implements the state machine the to handle user input for the REPL (CLI and web)
If the user enters an expression, like `x * 2`, check it evaluate it.
If the user enters a declaration, like `x = 123`, check it and remember it, but don't evaluate.
## `repl_expect/` - `roc_repl_expect`
Supports evaluating `expect` and printing contextual information when they fail.

View File

@ -28,7 +28,7 @@ run-wasm32 = ["roc_wasm_interp"]
# Compiling for a different target than the current machine can cause linker errors.
target-aarch64 = ["roc_build/target-aarch64", "roc_repl_cli/target-aarch64"]
target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
target-wasm32 = ["roc_build/target-wasm32", "roc_repl_cli/target-wasm32"]
target-wasm32 = ["roc_build/target-wasm32"]
target-x86 = ["roc_build/target-x86", "roc_repl_cli/target-x86"]
target-x86_64 = ["roc_build/target-x86_64", "roc_repl_cli/target-x86_64"]

View File

@ -11,7 +11,6 @@ version.workspace = true
# pipe target to roc_build
target-aarch64 = ["roc_build/target-aarch64"]
target-arm = ["roc_build/target-arm"]
target-wasm32 = ["roc_build/target-wasm32"]
target-x86 = ["roc_build/target-x86"]
target-x86_64 = ["roc_build/target-x86_64"]
@ -21,7 +20,6 @@ roc_builtins = { path = "../compiler/builtins" }
roc_collections = { path = "../compiler/collections" }
roc_gen_llvm = { path = "../compiler/gen_llvm" }
roc_load = { path = "../compiler/load" }
roc_module = { path = "../compiler/module" }
roc_mono = { path = "../compiler/mono" }
roc_parse = { path = "../compiler/parse" }
roc_region = { path = "../compiler/region" }

View File

@ -48,7 +48,8 @@ pub const TIPS: &str = concatcp!(
BLUE,
"val1",
END_COL,
" in future expressions.\n\nTips:\n\n",
" in future expressions.\n\n",
"Tips:\n\n",
BLUE,
" - ",
END_COL,

View File

@ -24,7 +24,7 @@ strip-ansi-escapes.workspace = true
default = ["target-aarch64", "target-x86_64", "target-wasm32"]
target-aarch64 = ["roc_build/target-aarch64", "roc_repl_cli/target-aarch64"]
target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
target-wasm32 = ["roc_build/target-wasm32", "roc_repl_cli/target-wasm32"]
target-wasm32 = ["roc_build/target-wasm32"]
target-x86 = ["roc_build/target-x86", "roc_repl_cli/target-x86"]
target-x86_64 = ["roc_build/target-x86_64", "roc_repl_cli/target-x86_64"]
wasm = ["target-wasm32"]

25
crates/repl_ui/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "roc_repl_ui"
description = "UI code for the Roc REPL, shared between CLI and WebAssembly versions."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
roc_collections = { path = "../compiler/collections" }
roc_load = { path = "../compiler/load" }
roc_parse = { path = "../compiler/parse" }
roc_region = { path = "../compiler/region" }
roc_repl_eval = { path = "../repl_eval" }
roc_reporting = { path = "../reporting" }
roc_target = { path = "../compiler/roc_target" }
bumpalo.workspace = true
const_format.workspace = true
unicode-segmentation.workspace = true
[lib]
name = "roc_repl_ui"
path = "src/lib.rs"

View File

@ -0,0 +1,16 @@
#[cfg(target_family = "wasm")]
use roc_reporting::report::{StyleCodes, HTML_STYLE_CODES};
#[cfg(not(target_family = "wasm"))]
use roc_reporting::report::{StyleCodes, ANSI_STYLE_CODES};
#[cfg(target_family = "wasm")]
const STYLE_CODES: StyleCodes = HTML_STYLE_CODES;
#[cfg(not(target_family = "wasm"))]
const STYLE_CODES: StyleCodes = ANSI_STYLE_CODES;
pub const BLUE: &str = STYLE_CODES.blue;
pub const PINK: &str = STYLE_CODES.magenta;
pub const GREEN: &str = STYLE_CODES.green;
pub const END_COL: &str = STYLE_CODES.reset;

201
crates/repl_ui/src/lib.rs Normal file
View File

@ -0,0 +1,201 @@
//! Command Line Interface (CLI) functionality for the Read-Evaluate-Print-Loop (REPL).
pub mod colors;
pub mod repl_state;
use bumpalo::Bump;
use colors::{BLUE, END_COL, GREEN, PINK};
use const_format::concatcp;
use repl_state::{parse_src, ParseOutcome};
use roc_parse::ast::{Expr, ValueDef};
use roc_repl_eval::gen::{Problems, ReplOutput};
use roc_reporting::report::StyleCodes;
pub const WELCOME_MESSAGE: &str = concatcp!(
"\n The rockin ",
BLUE,
"roc repl",
END_COL,
"\n",
PINK,
"────────────────────────",
END_COL,
"\n\n"
);
// Tips for the CLI REPL only (not the web REPL)
#[cfg(not(target_family = "wasm"))]
const TARGET_SPECIFIC_TIPS: &str = concatcp!(
"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"
);
// Tips for the web REPL only
// In the browser, you quit by closing the browser tab!
// And we are not stuck with weirdness like ctrl-v ctrl-j.
#[cfg(target_family = "wasm")]
const TARGET_SPECIFIC_TIPS: &str = concatcp!(
"Tips:\n\n",
BLUE,
" - ",
END_COL,
PINK,
"ctrl-Enter",
END_COL,
" makes a newline\n\n",
BLUE,
" - ",
END_COL,
":help"
);
// 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",
TARGET_SPECIFIC_TIPS
);
// 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, " ");
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,
}
}
pub fn format_output(
style_codes: StyleCodes,
opt_output: Option<ReplOutput>,
problems: Problems,
opt_var_name: Option<String>,
dimensions: Option<(usize, usize)>,
) -> String {
let mut buf = String::new();
for message in problems.errors.iter().chain(problems.warnings.iter()) {
if !buf.is_empty() {
buf.push_str("\n\n");
}
buf.push('\n');
buf.push_str(message);
buf.push('\n');
}
if let Some(ReplOutput { expr, expr_type }) = opt_output {
// If expr was empty, it was a type annotation or ability declaration;
// don't print anything!
//
// Also, for now we also don't print anything if there was a compile-time error.
// In the future, it would be great to run anyway and print useful output here!
if !expr.is_empty() && problems.errors.is_empty() {
const EXPR_TYPE_SEPARATOR: &str = " : "; // e.g. in "5 : Num *"
// Print the expr and its type
{
buf.push('\n');
buf.push_str(&expr);
buf.push_str(style_codes.magenta); // Color for the type separator
buf.push_str(EXPR_TYPE_SEPARATOR);
buf.push_str(style_codes.reset);
buf.push_str(&expr_type);
}
// Print var_name right-aligned on the last line of output.
if let Some(var_name) = opt_var_name {
use unicode_segmentation::UnicodeSegmentation;
const VAR_NAME_PREFIX: &str = " # "; // e.g. in " # val1"
const VAR_NAME_COLUMN_MAX: usize = 32; // Right-align the var_name at this column
let term_width = match dimensions {
Some((width, _)) => width.min(VAR_NAME_COLUMN_MAX),
None => VAR_NAME_COLUMN_MAX,
};
let expr_with_type = format!("{expr}{EXPR_TYPE_SEPARATOR}{expr_type}");
// Count graphemes because we care about what's *rendered* in the terminal
let last_line_len = expr_with_type
.split('\n')
.last()
.unwrap_or_default()
.graphemes(true)
.count();
let var_name_len =
var_name.graphemes(true).count() + VAR_NAME_PREFIX.graphemes(true).count();
let spaces_needed = if last_line_len + var_name_len > term_width {
buf.push('\n');
term_width - var_name_len
} else {
term_width - last_line_len - var_name_len
};
for _ in 0..spaces_needed {
buf.push(' ');
}
buf.push_str(style_codes.green);
buf.push_str(VAR_NAME_PREFIX);
buf.push_str(&var_name);
buf.push_str(style_codes.reset);
buf.push('\n');
}
}
}
buf
}

View File

@ -0,0 +1,431 @@
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,
}
}
}
}

View File

@ -180,7 +180,6 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
// Compile the app
let target_info = TargetInfo::default_wasm32();
let function_kind = roc_solve::FunctionKind::LambdaSet;
// TODO use this to filter out problems and warnings in wrapped defs.
// See the variable by the same name in the CLI REPL for how to do this!
let mono = match compile_to_mono(
@ -188,7 +187,6 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
std::iter::empty(),
&src,
target_info,
function_kind,
DEFAULT_PALETTE_HTML,
) {
(Some(m), problems) if problems.is_empty() => m, // TODO render problems and continue if possible

View File

@ -241,6 +241,7 @@ pub const DEFAULT_PALETTE: Palette = default_palette_from_style_codes(ANSI_STYLE
pub const DEFAULT_PALETTE_HTML: Palette = default_palette_from_style_codes(HTML_STYLE_CODES);
/// A machine-readable format for text styles (colors and other styles)
#[derive(Debug, PartialEq)]
pub struct StyleCodes {
pub red: &'static str,
pub green: &'static str,