diff --git a/crates/compiler/parse/src/expr.rs b/crates/compiler/parse/src/expr.rs index c297a9b64c..8064f83a5c 100644 --- a/crates/compiler/parse/src/expr.rs +++ b/crates/compiler/parse/src/expr.rs @@ -59,13 +59,13 @@ pub struct ExprParseOptions { /// This is usually true, but false within list/record literals /// because the comma separating backpassing arguments conflicts /// with the comma separating literal elements - accept_multi_backpassing: bool, + pub accept_multi_backpassing: bool, /// Check for the `->` token, and raise an error if found /// This is usually true, but false in if-guards /// /// > Just foo if foo == 2 -> ... - check_for_arrow: bool, + pub check_for_arrow: bool, } impl Default for ExprParseOptions { @@ -1050,9 +1050,9 @@ fn parse_defs_end<'a>( } pub struct SingleDef<'a> { - type_or_value: Either, ValueDef<'a>>, - region: Region, - spaces_before: &'a [CommentOrNewline<'a>], + pub type_or_value: Either, ValueDef<'a>>, + pub region: Region, + pub spaces_before: &'a [CommentOrNewline<'a>], } fn parse_defs_expr<'a>( diff --git a/crates/repl_cli/src/colors.rs b/crates/repl_cli/src/colors.rs new file mode 100644 index 0000000000..c85abd181e --- /dev/null +++ b/crates/repl_cli/src/colors.rs @@ -0,0 +1,4 @@ +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"; diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index 29011c9fa8..d8a2e3dc22 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -1,18 +1,18 @@ +mod colors; +mod repl_state; + use bumpalo::Bump; use const_format::concatcp; use inkwell::context::Context; use libloading::Library; +use repl_state::ReplState; use roc_gen_llvm::llvm::build::LlvmBackendMode; use roc_intern::SingleThreadedInterner; use roc_mono::layout::Layout; use roc_types::subs::Subs; -use rustyline::highlight::{Highlighter, PromptInfo}; -use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; -use rustyline_derive::{Completer, Helper, Hinter}; -use std::borrow::Cow; -use std::io; use target_lexicon::Triple; +use colors::{BLUE, END_COL, PINK}; use roc_build::link::llvm_module_to_dylib; use roc_collections::all::MutSet; use roc_gen_llvm::llvm::externs::add_default_roc_externs; @@ -20,18 +20,9 @@ use roc_gen_llvm::{run_jit_function, run_jit_function_dynamic_type}; use roc_load::{EntryPoint, MonomorphizedModule}; use roc_mono::ir::OptLevel; use roc_parse::ast::Expr; -use roc_parse::parser::{EClosure, EExpr, Parser, SyntaxError}; -use roc_repl_eval::eval::jit_to_ast; -use roc_repl_eval::gen::{compile_to_mono, format_answer, ReplOutput}; use roc_repl_eval::{ReplApp, ReplAppMemory}; -use roc_reporting::report::DEFAULT_PALETTE; use roc_std::RocStr; use roc_target::TargetInfo; -use roc_types::pretty_print::{name_and_print_var, DebugPrint}; - -const BLUE: &str = "\u{001b}[36m"; -const PINK: &str = "\u{001b}[35m"; -const END_COL: &str = "\u{001b}[0m"; pub const WELCOME_MESSAGE: &str = concatcp!( "\n The rockin’ ", @@ -49,121 +40,9 @@ pub const WELCOME_MESSAGE: &str = concatcp!( // 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"; -// TODO add link to repl tutorial(does not yet exist). -pub const TIPS: &str = concatcp!( - 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\n" -); pub const PROMPT: &str = concatcp!("\n", BLUE, "»", END_COL, " "); pub const CONT_PROMPT: &str = concatcp!(BLUE, "…", END_COL, " "); -#[derive(Completer, Helper, Hinter)] -struct ReplHelper { - validator: InputValidator, - pending_src: String, -} - -impl ReplHelper { - pub(crate) fn new() -> ReplHelper { - ReplHelper { - validator: InputValidator::new(), - pending_src: String::new(), - } - } -} - -impl Highlighter for ReplHelper { - fn has_continuation_prompt(&self) -> bool { - true - } - - fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, - prompt: &'p str, - info: PromptInfo<'_>, - ) -> Cow<'b, str> { - if info.line_no() > 0 { - CONT_PROMPT.into() - } else { - prompt.into() - } - } -} - -impl Validator for ReplHelper { - fn validate( - &self, - ctx: &mut validate::ValidationContext, - ) -> rustyline::Result { - self.validator.validate(ctx) - } - - fn validate_while_typing(&self) -> bool { - self.validator.validate_while_typing() - } -} - -struct InputValidator {} - -impl InputValidator { - pub(crate) fn new() -> InputValidator { - InputValidator {} - } -} - -impl Validator for InputValidator { - fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { - if ctx.input().is_empty() { - Ok(ValidationResult::Incomplete) - } else { - let arena = bumpalo::Bump::new(); - let state = roc_parse::state::State::new(ctx.input().trim().as_bytes()); - let answer = match roc_parse::expr::toplevel_defs(0).parse(&arena, state) { - // Special case some syntax errors to allow for multi-line inputs - Err((_, EExpr::DefMissingFinalExpr(_), _)) - | Err((_, EExpr::DefMissingFinalExpr2(_, _), _)) - | Err((_, EExpr::Closure(EClosure::Body(_, _), _), _)) => { - Ok(ValidationResult::Incomplete) - } - Err((_, _, state)) => { - // It wasn't a valid top-level decl, so continue parsing it as an expr. - match roc_parse::expr::parse_loc_expr(0, &arena, state) { - // Special case some syntax errors to allow for multi-line inputs - Err((_, EExpr::DefMissingFinalExpr(_), _)) - | Err((_, EExpr::DefMissingFinalExpr2(_, _), _)) - | Err((_, EExpr::Closure(EClosure::Body(_, _), _), _)) => { - Ok(ValidationResult::Incomplete) - } - _ => Ok(ValidationResult::Valid(None)), - } - } - Ok(_) => Ok(ValidationResult::Valid(None)), - }; - - // This is necessary to extend the lifetime of `arena`; without it, - // we get a borrow checker error! - answer - } - } -} - struct CliApp { lib: Library, } @@ -348,80 +227,7 @@ pub fn mono_module_to_dylib<'a>( .map(|lib| (lib, main_fn_name, subs, layout_interner)) } -fn gen_and_eval_llvm<'a>( - src: &str, - target: Triple, - opt_level: OptLevel, -) -> Result> { - let arena = Bump::new(); - let target_info = TargetInfo::from(&target); - - let mut loaded = match compile_to_mono(&arena, src, target_info, DEFAULT_PALETTE) { - Ok(x) => x, - Err(prob_strings) => { - return Ok(ReplOutput::Problems(prob_strings)); - } - }; - - debug_assert_eq!(loaded.exposed_to_host.values.len(), 1); - let (main_fn_symbol, main_fn_var) = loaded.exposed_to_host.values.iter().next().unwrap(); - let main_fn_symbol = *main_fn_symbol; - let main_fn_var = *main_fn_var; - - // pretty-print the expr type string for later. - let expr_type_str = name_and_print_var( - main_fn_var, - &mut loaded.subs, - loaded.module_id, - &loaded.interns, - DebugPrint::NOTHING, - ); - - let (_, main_fn_layout) = match loaded.procedures.keys().find(|(s, _)| *s == main_fn_symbol) { - Some(layout) => *layout, - None => { - return Ok(ReplOutput::NoProblems { - expr: "".to_string(), - expr_type: expr_type_str, - }); - } - }; - - let interns = loaded.interns.clone(); - - let (lib, main_fn_name, subs, layout_interner) = - mono_module_to_dylib(&arena, target, loaded, opt_level).expect("we produce a valid Dylib"); - - let mut app = CliApp { lib }; - - let res_answer = jit_to_ast( - &arena, - &mut app, - main_fn_name, - main_fn_layout, - main_fn_var, - &subs, - &interns, - layout_interner.into_global().fork(), - target_info, - ); - - let formatted = format_answer(&arena, res_answer, expr_type_str); - Ok(formatted) -} - -fn eval_and_format<'a>(src: &str) -> Result> { - let format_output = |output| match output { - ReplOutput::NoProblems { expr, expr_type } => { - format!("\n{} {}:{} {}", expr, PINK, END_COL, expr_type) - } - ReplOutput::Problems(lines) => format!("\n{}\n", lines.join("\n\n")), - }; - - gen_and_eval_llvm(src, Triple::host(), OptLevel::Normal).map(format_output) -} - -pub fn main() -> io::Result<()> { +pub fn main() -> i32 { use rustyline::error::ReadlineError; use rustyline::Editor; @@ -430,9 +236,8 @@ pub fn main() -> io::Result<()> { // RUST_LOG=rustyline=debug cargo run repl 2> debug.log print!("{}{}", WELCOME_MESSAGE, SHORT_INSTRUCTIONS); - let mut prev_line_blank = false; - let mut editor = Editor::::new(); - let repl_helper = ReplHelper::new(); + let mut editor = Editor::::new(); + let repl_helper = ReplState::new(); editor.set_helper(Some(repl_helper)); loop { @@ -443,93 +248,33 @@ pub fn main() -> io::Result<()> { let trim_line = line.trim(); editor.add_history_entry(trim_line); - let pending_src = &mut editor - .helper_mut() - .expect("Editor helper was not set") - .pending_src; + let repl_helper = editor.helper_mut().expect("Editor helper was not set"); - match trim_line.to_lowercase().as_str() { - "" => { - if pending_src.is_empty() { - print!("\n{}", TIPS); - } else if prev_line_blank { - // After two blank lines in a row, give up and try parsing it - // even though it's going to fail. This way you don't get stuck. - match eval_and_format(pending_src.as_str()) { - Ok(output) => { - println!("{}", output); - } - Err(_) => { - // This seems to be unreachable in practice. - unreachable!(); - } - } + dbg!(&editor); - pending_src.clear(); - } else { - pending_src.push('\n'); - - prev_line_blank = true; - continue; // Skip the part where we reset prev_line_blank to false - } + match step_repl_state(repl_helper, trim_line) { + Ok(output) => { + print!("{}", output); } - ":help" => { - // TODO add link to repl tutorial(does not yet exist). - print!("\n{}", TIPS); - } - ":exit" | ":quit" | ":q" => { - break; - } - _ => { - let result = if pending_src.is_empty() { - eval_and_format(trim_line) - } else { - pending_src.push('\n'); - pending_src.push_str(trim_line); - - eval_and_format(pending_src.as_str()) - }; - - match result { - Ok(output) => { - println!("{}", output); - pending_src.clear(); - } - Err(_) => { - // This seems to be unreachable in practice. - unreachable!(); - } - } - } - } + Err(exit_code) => return exit_code, + }; } - Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); - break; + #[cfg(windows)] + Err(Readline::WindowResize) => { + // This is fine; just ignore it. } Err(ReadlineError::Eof) => { - // If we hit an eof, and we're allowed to keep going, - // append the str to the src we're building up and continue. - // (We only need to append it here if it was empty before; - // otherwise, we already appended it before calling eval_and_format.) - let pending_src = &mut editor - .helper_mut() - .expect("Editor helper was not set") - .pending_src; - - if pending_src.is_empty() { - pending_src.push_str(""); - } - break; + // End of input; we're done! + return 0; + } + Err(ReadlineError::Interrupted) => { + eprintln!("CTRL-C"); + return 1; } Err(err) => { eprintln!("REPL error: {:?}", err); - break; + return 1; } } - - prev_line_blank = false; } - - Ok(()) } diff --git a/crates/repl_cli/src/repl_state.rs b/crates/repl_cli/src/repl_state.rs new file mode 100644 index 0000000000..2c168eb9bc --- /dev/null +++ b/crates/repl_cli/src/repl_state.rs @@ -0,0 +1,354 @@ +use crate::colors::{BLUE, END_COL, PINK}; +use bumpalo::Bump; +use const_format::concatcp; +use roc_parse::ast::ValueDef; +use roc_parse::expr::{parse_single_def, ExprParseOptions}; +use roc_parse::parser::{EClosure, EExpr, Parser}; +use roc_parse::parser::{Either, SyntaxError}; +use roc_parse::state::State; +use roc_repl_eval::eval::jit_to_ast; +use roc_repl_eval::gen::{compile_to_mono, format_answer, ReplOutput}; +use roc_reporting::report::DEFAULT_PALETTE; +use roc_types::pretty_print::{name_and_print_var, DebugPrint}; +use rustyline::highlight::{Highlighter, PromptInfo}; +use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; +use rustyline_derive::{Completer, Helper, Hinter}; +use std::borrow::Cow; +use std::collections::LinkedList; + +// TODO add link to repl tutorial(does not yet exist). +pub const TIPS: &str = concatcp!( + 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\n" +); + +pub struct PastDef { + ident: String, + src: String, +} + +#[derive(Completer, Helper, Hinter)] +pub(crate) struct ReplState { + validator: InputValidator, + prev_line_blank: bool, + pending_src: String, + past_defs: LinkedList, +} + +impl ReplState { + pub fn new() -> Self { + Self { + validator: InputValidator::new(), + prev_line_blank: false, + pending_src: String::new(), + past_defs: Default::default(), + } + } + + fn step(&mut self, trim_line: &str) -> Result { + match trim_line.to_lowercase().as_str() { + "" => { + if self.pending_src.is_empty() { + self.prev_line_blank = false; + + return Ok(format!("\n{}\n", TIPS)); + } else if self.prev_line_blank { + // After two blank lines in a row, give up and try parsing it + // even though it's going to fail. This way you don't get stuck. + let src = self.pending_src.clone(); + + self.pending_src.clear(); + + self.eval_and_format(&src).map_err(|_| { + // This seems to be unreachable in practice. + unreachable!(); + }) + } else { + // The previous line wasn't blank, but there's some pending source. + // This could mean that, for example, you're writing a multiline `when` + // and want to add a blank line. No problem! Print a blank line and + // continue waiting for input. + // + // If the user presses enter again, next time prev_line_blank will be true + // and we'll try parsing the source as-is. + self.prev_line_blank = true; + + Ok("\n".to_string()) + } + } + ":help" => { + // TODO add link to repl tutorial(does not yet exist). + Ok(format!("\n{}\n", TIPS)) + } + ":exit" | ":quit" | ":q" => Err(0), + _ => self.eval_and_format(trim_line).map_err(|fail| { + todo!("gracefully report parse error in repl: {:?}", fail); + }), + } + } + + pub fn eval_and_format<'a>(&mut self, src: &str) -> Result> { + let src = if self.pending_src.is_empty() { + src + } else { + self.pending_src.push('\n'); + self.pending_src.push_str(src); + + self.pending_src.as_str() + }; + + dbg!(&src); + + // First, try to parse it as a Def. If that succeeds, record it in self and continue. + let src = match parse_single_def( + ExprParseOptions { + accept_multi_backpassing: true, + check_for_arrow: true, + }, + 0, + &Bump::new(), + State::new(src.as_bytes()), + ) { + Ok((_, Some(single_def), _)) => match single_def.type_or_value { + Either::First(type_def) => { + // Alias, Opaque, or Ability + todo!("handle Alias, Opaque, or Ability") + } + Either::Second(value_def) => match value_def { + ValueDef::Annotation(_, _) => { + // Needed to avoid a borrow error. + let src = src.to_string(); + + dbg!(&src); + + // We received a type annotation, like `x : Str` + // + // This might be the beginning of an AnnotatedBody, or it might be + // a standalone annotation. To find out, we need more input from the user. + self.pending_src.push_str(src.as_str()); + self.pending_src.push('\n'); + + // Return without clearing pending_src. + return Ok(String::new()); + } + ValueDef::Body(loc_pattern, loc_expr) + | ValueDef::AnnotatedBody { + body_pattern: loc_pattern, + body_expr: loc_expr, + .. + } => todo!("handle receiving a toplevel def of a value/function"), + 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?") + } + }, + }, + Ok(_) => src, + Err((_, eexpr, _)) => { + return Err(fail); + } + }; + + let answer = gen_and_eval_llvm( + src, + Triple::host(), + OptLevel::Normal, + "TODOval1".to_string(), + ) + .map(format_output); + + self.pending_src.clear(); + + answer + } + + /// Wrap the given expresssion in the appropriate past defs + fn wrapped_expr_src(&self, src: &str) -> String { + let mut buf = String::new(); + + for past_def in self.past_defs.iter() { + buf.push_str(past_def.src.as_str()); + buf.push('\n'); + } + + buf.push_str(src); + + buf + } +} + +struct InputValidator {} + +impl InputValidator { + pub fn new() -> InputValidator { + InputValidator {} + } +} + +impl Validator for InputValidator { + fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { + if ctx.input().is_empty() { + Ok(ValidationResult::Incomplete) + } else { + let arena = bumpalo::Bump::new(); + let state = roc_parse::state::State::new(ctx.input().trim().as_bytes()); + let answer = match roc_parse::expr::toplevel_defs(0).parse(&arena, state) { + // Special case some syntax errors to allow for multi-line inputs + Err((_, EExpr::DefMissingFinalExpr(_), _)) + | Err((_, EExpr::DefMissingFinalExpr2(_, _), _)) + | Err((_, EExpr::Closure(EClosure::Body(_, _), _), _)) => { + Ok(ValidationResult::Incomplete) + } + Err((_, _, state)) => { + // It wasn't a valid top-level decl, so continue parsing it as an expr. + match roc_parse::expr::parse_loc_expr(0, &arena, state) { + // Special case some syntax errors to allow for multi-line inputs + Err((_, EExpr::DefMissingFinalExpr(_), _)) + | Err((_, EExpr::DefMissingFinalExpr2(_, _), _)) + | Err((_, EExpr::Closure(EClosure::Body(_, _), _), _)) => { + Ok(ValidationResult::Incomplete) + } + _ => Ok(ValidationResult::Valid(None)), + } + } + Ok(_) => Ok(ValidationResult::Valid(None)), + }; + + // This is necessary to extend the lifetime of `arena`; without it, + // we get a borrow checker error! + answer + } + } +} + +impl Highlighter for ReplState { + fn has_continuation_prompt(&self) -> bool { + true + } + + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + info: PromptInfo<'_>, + ) -> Cow<'b, str> { + if info.line_no() > 0 { + CONT_PROMPT.into() + } else { + prompt.into() + } + } +} + +impl Validator for ReplState { + fn validate( + &self, + ctx: &mut validate::ValidationContext, + ) -> rustyline::Result { + self.validator.validate(ctx) + } + + fn validate_while_typing(&self) -> bool { + self.validator.validate_while_typing() + } +} + +fn format_output(output: ReplOutput) -> String { + match output { + ReplOutput::NoProblems { + expr, + expr_type, + val_name, + } => { + if expr.is_empty() { + // This means it was a type annotation or ability declaration; + // don't print anything! + String::new() + } else { + format!("\n{expr} {PINK}:{END_COL} {expr_type} {GREEN} # {val_name}") + } + } + ReplOutput::Problems(lines) => format!("\n{}\n", lines.join("\n\n")), + } +} + +fn gen_and_eval_llvm<'a>( + src: &str, + target: Triple, + opt_level: OptLevel, + val_name: String, +) -> Result> { + let arena = Bump::new(); + let target_info = TargetInfo::from(&target); + + let mut loaded = match compile_to_mono(&arena, src, target_info, DEFAULT_PALETTE) { + Ok(x) => x, + Err(prob_strings) => { + return Ok(ReplOutput::Problems(prob_strings)); + } + }; + + debug_assert_eq!(loaded.exposed_to_host.values.len(), 1); + let (main_fn_symbol, main_fn_var) = loaded.exposed_to_host.values.iter().next().unwrap(); + let main_fn_symbol = *main_fn_symbol; + let main_fn_var = *main_fn_var; + + // pretty-print the expr type string for later. + let expr_type_str = name_and_print_var( + main_fn_var, + &mut loaded.subs, + loaded.module_id, + &loaded.interns, + DebugPrint::NOTHING, + ); + + let (_, main_fn_layout) = match loaded.procedures.keys().find(|(s, _)| *s == main_fn_symbol) { + Some(layout) => *layout, + None => { + return Ok(ReplOutput::NoProblems { + expr: "".to_string(), + expr_type: expr_type_str, + val_name, + }); + } + }; + + let interns = loaded.interns.clone(); + + let (lib, main_fn_name, subs, layout_interner) = + mono_module_to_dylib(&arena, target, loaded, opt_level).expect("we produce a valid Dylib"); + + let mut app = CliApp { lib }; + + let res_answer = jit_to_ast( + &arena, + &mut app, + main_fn_name, + main_fn_layout, + main_fn_var, + &subs, + &interns, + layout_interner.into_global().fork(), + target_info, + ); + + Ok(format_answer(&arena, res_answer, expr_type_str, val_name)) +} diff --git a/crates/repl_eval/src/gen.rs b/crates/repl_eval/src/gen.rs index 0becb9fb10..43a95499ef 100644 --- a/crates/repl_eval/src/gen.rs +++ b/crates/repl_eval/src/gen.rs @@ -15,13 +15,18 @@ use crate::eval::ToAstProblem; pub enum ReplOutput { Problems(Vec), - NoProblems { expr: String, expr_type: String }, + NoProblems { + expr: String, + expr_type: String, + val_name: String, + }, } pub fn format_answer( arena: &Bump, res_answer: Result, expr_type_str: String, + val_name: String, ) -> ReplOutput { let mut expr = roc_fmt::Buf::new_in(arena); @@ -39,6 +44,7 @@ pub fn format_answer( ReplOutput::NoProblems { expr: expr.into_bump_str().to_string(), expr_type: expr_type_str, + val_name, } } diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index 9b37f6cf5e..7b1cccc1ff 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -272,8 +272,12 @@ pub async fn entrypoint_from_js(src: String) -> Result { // Transform the Expr to a string // `Result::Err` becomes a JS exception that will be caught and displayed - match format_answer(arena, res_answer, expr_type_str) { - ReplOutput::NoProblems { expr, expr_type } => Ok(format!("{} : {}", expr, expr_type)), + match format_answer(arena, res_answer, expr_type_str, val_name) { + ReplOutput::NoProblems { + expr, + expr_type, + val_name, + } => Ok(format!("{expr} : {expr_type} # {val_name}")), ReplOutput::Problems(lines) => Err(format!("\n{}\n", lines.join("\n\n"))), } }