From 463157fa70ec286d723814e62438e4d4c2cc04f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 00:04:02 +0000 Subject: [PATCH 01/28] Bump webpki from 0.22.0 to 0.22.1 Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1. - [Commits](https://github.com/briansmith/webpki/commits) --- updated-dependencies: - dependency-name: webpki dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdd7d5a49f..0af37a6402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5164,7 +5164,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 0.1.10", "rand", "static_assertions", ] @@ -5572,9 +5572,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" dependencies = [ "ring", "untrusted", From 3a586f57cb72c549d271134290c834d1cd104b8c Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:13:57 +0200 Subject: [PATCH 02/28] added signing transfer instructions --- CONTRIBUTING.md | 12 +------ devtools/signing.md | 80 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 devtools/signing.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96963fed77..3c7e758972 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,17 +53,7 @@ This command will generate the documentation in the [`generated-docs`](generated ### Commit signing -- All your commits need to be signed [to prevent impersonation](https://dev.to/martiliones/how-i-got-linus-torvalds-in-my-contributors-on-github-3k4g): -- If you don't have signing set up on your device and you only want to change a single file, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files). This will sign your commit automatically. -- For multi-file or complex changes you will want to set up signing on your device: - 1. If you have a Yubikey, follow [guide 1](https://dev.to/paulmicheli/using-your-yubikey-to-get-started-with-gpg-3h4k), [guide 2](https://dev.to/paulmicheli/using-your-yubikey-for-signed-git-commits-4l73) and skip the steps below. - 2. [Make a key to sign your commits.](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) - 3. [Configure git to use your key.](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key) - 4. Make git sign your commits automatically: - - ```sh - git config --global commit.gpgsign true - ``` +All your commits need to be signed [to prevent impersonation](https://dev.to/martiliones/how-i-got-linus-torvalds-in-my-contributors-on-github-3k4g). Check out our guide for commit signing [here](devtools/signing.md). #### Commit signing on NixOS diff --git a/devtools/signing.md b/devtools/signing.md new file mode 100644 index 0000000000..5ff9548c63 --- /dev/null +++ b/devtools/signing.md @@ -0,0 +1,80 @@ +# Commit Signing Guide + +If you don't have signing set up on your device and you only want to change a single file, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files). This will sign your commit automatically. + +For multi-file or complex changes you will want to set up signing on your device. +Follow along with the subsection below that applies to you. + +If your situation is not listed below, consider adding your steps to help out others. + +## Setting up commit signing for the first time + +If you have a Yubikey, and use macOS or Linux, follow [guide 1](https://dev.to/paulmicheli/using-your-yubikey-to-get-started-with-gpg-3h4k) and [guide 2](https://dev.to/paulmicheli/using-your-yubikey-for-signed-git-commits-4l73). +For windows with a Yubikey, follow [this guide](https://scatteredcode.net/signing-git-commits-using-yubikey-on-windows/). + +Without a Yubikey: + 1. [Make a key to sign your commits.](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) + 2. [Configure git to use your key.](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key) + 3. Make git sign your commits automatically: + + ```sh + git config --global commit.gpgsign true + ``` + +## Transferring existing key from Linux to Windows + +### With Yubikey + +This explanation was based on the steps outlined [here](https://scatteredcode.net/signing-git-commits-using-yubikey-on-windows/). + +On linux, run: +``` +gpg --list-keys --keyid-format SHORT | grep ^pub +gpg --export --armor [Your_Key_ID] > public.key +``` + +Copy the public.key file to windows. + +Download and install [Gpg4win](https://www.gpg4win.org/get-gpg4win.html). + +Open the program Kleopatra (installed with gpg4win) and go to Smartcards. +You should see your Yubikey there, it should also say something like `failed to find public key locally`. Click the import button, change the available file types at the bottom right to `Any files` and open the `public.key` file you created earlier. +Close Kleopatra. + +Install the `YubiKey Minidriver for 64-bit systems – Windows Installer` from [here](https://www.yubico.com/support/download/smart-card-drivers-tools/). + +Insert your Yubikey and check if it is mentioned in the output of `gpg --card-status` (command line). If not, execute these powershell commands: +``` +New-Item "$env:APPDATA\gnupg\scdaemon.conf" -Type File +notepad "$env:APPDATA\gnupg\scdaemon.conf" +``` +open `Device Manager`, click `View`>`Show Hidden Devices`, expand `Software Devices`, you will see something like `Yubico YubiKey OTP+FIDO+CCID 0`, copy your version of this string. + +Type `reader-port "Yubico YubiKey OTP+FIDO+CCID 0"` (use your exact string) into notepad and save the file. + +Reboot your pc. + +Open the `Git Bash` program and execute: +``` +git config --global gpg.program "c:\Program Files (x86)\GnuPG\bin\gpg.exe" +git config --global commit.gpgsign true +gpg --list-secret-keys --keyid-format LONG +``` +The last command will show your keyid. On the line that says [SC], copy the id. +In the example below the id is 683AB68D867FEB5C +``` +sec> rsa4096/683AB68D867FEB5C 2020-02-02 [SC] [expires: 2022-02-02] +``` + +Tell git your keyid: +``` +>git config --global user.signingkey YOUR_KEY_ID_HERE +``` + +That's it! + +### Without Yubikey + +TODO + + \ No newline at end of file From 236392b06088696d9e3d2f58780fc46697a02048 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:18:50 +0200 Subject: [PATCH 03/28] removed git bash mentions --- devtools/signing.md | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/devtools/signing.md b/devtools/signing.md index 5ff9548c63..5a2feab22d 100644 --- a/devtools/signing.md +++ b/devtools/signing.md @@ -43,18 +43,9 @@ Close Kleopatra. Install the `YubiKey Minidriver for 64-bit systems – Windows Installer` from [here](https://www.yubico.com/support/download/smart-card-drivers-tools/). -Insert your Yubikey and check if it is mentioned in the output of `gpg --card-status` (command line). If not, execute these powershell commands: -``` -New-Item "$env:APPDATA\gnupg\scdaemon.conf" -Type File -notepad "$env:APPDATA\gnupg\scdaemon.conf" -``` -open `Device Manager`, click `View`>`Show Hidden Devices`, expand `Software Devices`, you will see something like `Yubico YubiKey OTP+FIDO+CCID 0`, copy your version of this string. +Insert your Yubikey and check if it is mentioned in the output of `gpg --card-status` (powershell). -Type `reader-port "Yubico YubiKey OTP+FIDO+CCID 0"` (use your exact string) into notepad and save the file. - -Reboot your pc. - -Open the `Git Bash` program and execute: +Open powershell and execute: ``` git config --global gpg.program "c:\Program Files (x86)\GnuPG\bin\gpg.exe" git config --global commit.gpgsign true From e30f2e58a1b5c7936b428edf2c3ec0621bfc03eb Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:30:24 +0200 Subject: [PATCH 04/28] small corrections Signed-off-by: Anton-4 <17049058+Anton-4@users.noreply.github.com> --- devtools/signing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/devtools/signing.md b/devtools/signing.md index 5a2feab22d..79304d91fd 100644 --- a/devtools/signing.md +++ b/devtools/signing.md @@ -1,8 +1,8 @@ # Commit Signing Guide -If you don't have signing set up on your device and you only want to change a single file, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files). This will sign your commit automatically. +If you don't have signing set up on your device and you only want to make simple changes, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files) for single file changes or [github's online VSCode editor](https://docs.github.com/en/codespaces/the-githubdev-web-based-editor#opening-the-githubdev-editor) for multi-file changes. These tools will sign your commit automatically. -For multi-file or complex changes you will want to set up signing on your device. +For complex changes you will want to set up signing on your device. Follow along with the subsection below that applies to you. If your situation is not listed below, consider adding your steps to help out others. @@ -51,7 +51,7 @@ git config --global gpg.program "c:\Program Files (x86)\GnuPG\bin\gpg.exe" git config --global commit.gpgsign true gpg --list-secret-keys --keyid-format LONG ``` -The last command will show your keyid. On the line that says [SC], copy the id. +The last command will show your keyid. On the line that says `[SC]`, copy the id. In the example below the id is 683AB68D867FEB5C ``` sec> rsa4096/683AB68D867FEB5C 2020-02-02 [SC] [expires: 2022-02-02] @@ -68,4 +68,4 @@ That's it! TODO - \ No newline at end of file + From dbf928bc461b6c7d71d32197973ea106d0b48668 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 8 Sep 2023 15:37:16 -0400 Subject: [PATCH 05/28] Reorganize ReplState logic for cli/wasm compat --- crates/repl_cli/src/cli_gen.rs | 69 ++------ crates/repl_cli/src/lib.rs | 258 +++++++++++++++++++++++++-- crates/repl_cli/src/repl_state.rs | 285 ++++++------------------------ crates/repl_eval/src/gen.rs | 3 +- 4 files changed, 315 insertions(+), 300 deletions(-) diff --git a/crates/repl_cli/src/cli_gen.rs b/crates/repl_cli/src/cli_gen.rs index 0b8c322ca7..061124fd68 100644 --- a/crates/repl_cli/src/cli_gen.rs +++ b/crates/repl_cli/src/cli_gen.rs @@ -7,49 +7,26 @@ use roc_error_macros::internal_error; use roc_gen_llvm::llvm::build::LlvmBackendMode; use roc_gen_llvm::llvm::externs::add_default_roc_externs; use roc_gen_llvm::{run_jit_function, run_jit_function_dynamic_type}; -use roc_load::{EntryPoint, FunctionKind, MonomorphizedModule}; +use roc_load::{EntryPoint, MonomorphizedModule}; use roc_mono::ir::OptLevel; use roc_mono::layout::STLayoutInterner; use roc_parse::ast::Expr; use roc_repl_eval::eval::jit_to_ast; -use roc_repl_eval::gen::{compile_to_mono, format_answer, Problems, ReplOutput}; +use roc_repl_eval::gen::{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}; use roc_types::subs::Subs; use target_lexicon::Triple; -pub fn gen_and_eval_llvm<'a, I: Iterator>( - defs: I, - src: &str, - target: Triple, +pub fn eval_llvm<'a>( + mut loaded: MonomorphizedModule<'a>, + target: &Triple, opt_level: OptLevel, -) -> (Option, Problems) { +) -> Option { let arena = Bump::new(); - let target_info = TargetInfo::from(&target); - let function_kind = FunctionKind::LambdaSet; - - let mut loaded; - let problems; - - match compile_to_mono( - &arena, - defs, - src, - target_info, - function_kind, - DEFAULT_PALETTE, - ) { - (Some(mono), probs) => { - loaded = mono; - problems = probs; - } - (None, probs) => { - return (None, probs); - } - }; + let target_info = TargetInfo::from(target); debug_assert_eq!(loaded.exposed_to_host.top_level_values.len(), 1); let (main_fn_symbol, main_fn_var) = loaded @@ -70,20 +47,15 @@ pub fn gen_and_eval_llvm<'a, I: Iterator>( DebugPrint::NOTHING, ); - let (_, main_fn_layout) = match loaded.procedures.keys().find(|(s, _)| *s == main_fn_symbol) { - Some(layout) => *layout, - None => { - let empty_vec: Vec = Vec::new(); // rustc can't infer the type of this Vec. - debug_assert_ne!(problems.errors, empty_vec, "Got no errors but also no valid layout for the generated main function in the repl!"); - - return (None, problems); - } - }; + let (_, main_fn_layout) = *loaded + .procedures + .keys() + .find(|(s, _)| *s == main_fn_symbol)?; 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"); + mono_module_to_dylib(&arena, &target, loaded, opt_level).expect("we produce a valid Dylib"); let mut app = CliApp { lib }; @@ -100,13 +72,10 @@ pub fn gen_and_eval_llvm<'a, I: Iterator>( ); let expr_str = format_answer(&arena, expr).to_string(); - ( - Some(ReplOutput { - expr: expr_str, - expr_type: expr_type_str, - }), - problems, - ) + Some(ReplOutput { + expr: expr_str, + expr_type: expr_type_str, + }) } struct CliApp { @@ -191,11 +160,11 @@ impl ReplAppMemory for CliMemory { fn mono_module_to_dylib<'a>( arena: &'a Bump, - target: Triple, + target: &Triple, loaded: MonomorphizedModule<'a>, opt_level: OptLevel, ) -> Result<(libloading::Library, &'a str, Subs, STLayoutInterner<'a>), libloading::Error> { - let target_info = TargetInfo::from(&target); + let target_info = TargetInfo::from(target); let MonomorphizedModule { procedures, @@ -210,7 +179,7 @@ fn mono_module_to_dylib<'a>( let context = Context::create(); let builder = context.create_builder(); let module = arena.alloc(roc_gen_llvm::llvm::build::module_from_builtins( - &target, &context, "", + target, &context, "", )); let module = arena.alloc(module); diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index 2b70075071..324663b411 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -3,11 +3,24 @@ mod cli_gen; mod colors; pub mod repl_state; -use colors::{BLUE, END_COL, PINK}; -use const_format::concatcp; -use repl_state::ReplState; +use std::borrow::Cow; -use crate::repl_state::PROMPT; +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_mono::ir::OptLevel; +use roc_parse::ast::{Expr, ValueDef}; +use roc_repl_eval::gen::{Problems, ReplOutput}; +use roc_reporting::report::DEFAULT_PALETTE; +use roc_target::TargetInfo; +use rustyline::highlight::{Highlighter, PromptInfo}; +use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; +use rustyline_derive::{Completer, Helper, Hinter}; +use target_lexicon::Triple; + +use crate::cli_gen::eval_llvm; pub const WELCOME_MESSAGE: &str = concatcp!( "\n The rockin’ ", @@ -21,10 +34,55 @@ pub const WELCOME_MESSAGE: &str = concatcp!( "\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\nTips:\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, + state: ReplState, +} + pub fn main() -> i32 { use rustyline::error::ReadlineError; use rustyline::Editor; @@ -34,9 +92,12 @@ pub fn main() -> i32 { // RUST_LOG=rustyline=debug cargo run repl 2> debug.log print!("{WELCOME_MESSAGE}{SHORT_INSTRUCTIONS}"); - let mut editor = Editor::::new(); - let repl_helper = ReplState::new(); + let mut editor = Editor::::new(); + let repl_helper = ReplHelper::default(); editor.set_helper(Some(repl_helper)); + let target = Triple::host(); + let target_info = TargetInfo::from(&target); + let mut arena = Bump::new(); loop { match editor.readline(PROMPT) { @@ -44,18 +105,36 @@ pub fn main() -> i32 { editor.add_history_entry(line.trim()); let dimensions = editor.dimensions(); - let repl_helper = editor.helper_mut().expect("Editor helper was not set"); + let repl_state = &mut editor + .helper_mut() + .expect("Editor helper was not set") + .state; + + arena.reset(); + match repl_state.step(&arena, &line, target_info, DEFAULT_PALETTE) { + // If there was no output, don't print a blank line! + // (This happens for something like a type annotation.) + ReplAction::Eval { + opt_mono, + 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); - match repl_helper.step(&line, dimensions) { - Ok(output) => { - // If there was no output, don't print a blank line! - // (This happens for something like a type annotation.) if !output.is_empty() { println!("{output}"); } } - Err(exit_code) => return exit_code, - }; + ReplAction::Exit => { + return 0; + } + ReplAction::Help => { + println!("{TIPS}"); + } + ReplAction::Nothing => {} + } } #[cfg(windows)] Err(ReadlineError::WindowResize) => { @@ -76,3 +155,156 @@ pub fn main() -> i32 { } } } + +#[derive(Default)] +struct InputValidator {} + +impl Validator for InputValidator { + fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { + if is_incomplete(ctx.input()) { + Ok(ValidationResult::Incomplete) + } else { + Ok(ValidationResult::Valid(None)) + } + } +} + +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 + } + + 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() + } +} + +fn format_output( + opt_output: Option, + problems: Problems, + opt_var_name: Option, + 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(PINK); // Color for the type separator + buf.push_str(EXPR_TYPE_SEPARATOR); + buf.push_str(END_COL); + 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(GREEN); + buf.push_str(VAR_NAME_PREFIX); + buf.push_str(&var_name); + buf.push_str(END_COL); + buf.push('\n'); + } + } + } + + buf +} diff --git a/crates/repl_cli/src/repl_state.rs b/crates/repl_cli/src/repl_state.rs index aa640bb2d8..755e9f64cf 100644 --- a/crates/repl_cli/src/repl_state.rs +++ b/crates/repl_cli/src/repl_state.rs @@ -1,9 +1,6 @@ -use crate::cli_gen::gen_and_eval_llvm; -use crate::colors::{BLUE, END_COL, GREEN, PINK}; use bumpalo::Bump; -use const_format::concatcp; use roc_collections::MutSet; -use roc_mono::ir::OptLevel; +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; @@ -12,65 +9,20 @@ 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::{Problems, ReplOutput}; -use rustyline::highlight::{Highlighter, PromptInfo}; -use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; -use rustyline_derive::{Completer, Helper, Hinter}; -use std::borrow::Cow; -use target_lexicon::Triple; - -pub const PROMPT: &str = concatcp!(BLUE, "»", END_COL, " "); -pub const CONT_PROMPT: &str = concatcp!(BLUE, "…", END_COL, " "); +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"; - -// 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\nTips:\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" -); - #[derive(Debug, Clone, PartialEq)] struct PastDef { ident: String, src: String, } -#[derive(Completer, Helper, Hinter)] pub struct ReplState { - validator: InputValidator, past_defs: Vec, past_def_idents: MutSet, last_auto_ident: u64, @@ -82,36 +34,51 @@ impl Default for ReplState { } } +pub enum ReplAction<'a> { + Eval { + opt_mono: Option>, + problems: Problems, + opt_var_name: Option, + }, + Exit, + Help, + Nothing, +} + impl ReplState { pub fn new() -> Self { Self { - validator: InputValidator::new(), past_defs: Default::default(), past_def_idents: Default::default(), last_auto_ident: 0, } } - pub fn step(&mut self, line: &str, dimensions: Option<(usize, usize)>) -> Result { - let arena = Bump::new(); - + 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 => Ok(TIPS.to_string()), + ParseOutcome::Empty | ParseOutcome::Help => ReplAction::Help, ParseOutcome::Expr(_) | ParseOutcome::ValueDef(_) | ParseOutcome::TypeDef(_) | ParseOutcome::SyntaxErr - | ParseOutcome::Incomplete => Ok(self.eval_and_format(line, dimensions)), - ParseOutcome::Help => { - // TODO add link to repl tutorial(does not yet exist). - Ok(TIPS.to_string()) - } - ParseOutcome::Exit => Err(0), + | ParseOutcome::Incomplete => self.next_action(arena, line, target_info, palette), + ParseOutcome::Exit => ReplAction::Exit, } } - pub fn eval_and_format(&mut self, src: &str, dimensions: Option<(usize, usize)>) -> String { - let arena = Bump::new(); + 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) { @@ -138,7 +105,7 @@ impl ReplState { // Return early without running eval, since standalone annotations // cannnot be evaluated as expressions. - return String::new(); + return ReplAction::Nothing; } ValueDef::Body( Loc { @@ -218,7 +185,7 @@ impl ReplState { // Return early without running eval, since none of these // can be evaluated as expressions. - return String::new(); + return ReplAction::Nothing; } ParseOutcome::Empty | ParseOutcome::Help | ParseOutcome::Exit => unreachable!(), }; @@ -226,24 +193,26 @@ impl ReplState { // 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 (output, problems) = + 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); - gen_and_eval_llvm( + compile_to_mono( + &arena, self.past_defs.iter().map(|def| def.src.as_str()), src, - Triple::host(), - OptLevel::Normal, + target_info, + palette, ) } None => { - let (output, problems) = gen_and_eval_llvm( + let (output, problems) = compile_to_mono( + &arena, self.past_defs.iter().map(|def| def.src.as_str()), src, - Triple::host(), - OptLevel::Normal, + target_info, + palette, ); // Don't persist defs that have compile errors @@ -266,7 +235,11 @@ impl ReplState { self.add_past_def(ident, src); } - format_output(output, problems, opt_var_name, dimensions) + ReplAction::Eval { + opt_mono, + problems, + opt_var_name, + } } fn next_auto_ident(&mut self) -> u64 { @@ -284,7 +257,7 @@ impl ReplState { } #[derive(Debug, PartialEq)] -enum ParseOutcome<'a> { +pub enum ParseOutcome<'a> { ValueDef(ValueDef<'a>), TypeDef(TypeDef<'a>), Expr(Expr<'a>), @@ -295,7 +268,7 @@ enum ParseOutcome<'a> { Exit, } -fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> { +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, @@ -456,161 +429,3 @@ fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> { } } } - -struct InputValidator {} - -impl InputValidator { - pub fn new() -> InputValidator { - InputValidator {} - } -} - -impl Validator for InputValidator { - fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { - if is_incomplete(ctx.input()) { - Ok(ValidationResult::Incomplete) - } else { - Ok(ValidationResult::Valid(None)) - } - } -} - -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 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( - opt_output: Option, - problems: Problems, - opt_var_name: Option, - 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(PINK); // Color for the type separator - buf.push_str(EXPR_TYPE_SEPARATOR); - buf.push_str(END_COL); - 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(GREEN); - buf.push_str(VAR_NAME_PREFIX); - buf.push_str(&var_name); - buf.push_str(END_COL); - buf.push('\n'); - } - } - } - - buf -} diff --git a/crates/repl_eval/src/gen.rs b/crates/repl_eval/src/gen.rs index 830d056f68..658db41dd5 100644 --- a/crates/repl_eval/src/gen.rs +++ b/crates/repl_eval/src/gen.rs @@ -50,7 +50,6 @@ pub fn compile_to_mono<'a, 'i, I: Iterator>( defs: I, expr: &str, target_info: TargetInfo, - function_kind: FunctionKind, palette: Palette, ) -> (Option>, Problems) { let filename = PathBuf::from(""); @@ -64,7 +63,7 @@ pub fn compile_to_mono<'a, 'i, I: Iterator>( RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), LoadConfig { target_info, - function_kind, + function_kind: FunctionKind::LambdaSet, render: roc_reporting::report::RenderTarget::ColorTerminal, palette, threading: Threading::Single, From 2cf4f65d97de094e379fdada694791d7dbd94f5b Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Fri, 8 Sep 2023 16:02:38 -0700 Subject: [PATCH 06/28] Update to wyhash final 4 This update makes wyhash bad seed resist. In old version of wyhash certain seeds would ruin the randomness. Changes applied can be are based off of this diff: https://github.com/wangyi-fudan/wyhash/compare/a5995b9..77e50f2 --- crates/compiler/builtins/roc/Dict.roc | 106 ++++++++++++++------------ 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/crates/compiler/builtins/roc/Dict.roc b/crates/compiler/builtins/roc/Dict.roc index 75d8e2882e..49ac595fab 100644 --- a/crates/compiler/builtins/roc/Dict.roc +++ b/crates/compiler/builtins/roc/Dict.roc @@ -1005,12 +1005,12 @@ expect # We have decided not to expose the standard roc hashing algorithm. # This is to avoid external dependence and the need for versioning. -# The current implementation is a form of [Wyhash final3](https://github.com/wangyi-fudan/wyhash/blob/a5995b98ebfa7bd38bfadc0919326d2e7aabb805/wyhash.h). +# The current implementation is a form of [Wyhash final4](https://github.com/wangyi-fudan/wyhash/blob/77e50f267fbc7b8e2d09f2d455219adb70ad4749/wyhash.h). # It is 64bit and little endian specific currently. # TODO: wyhash is slow for large keys, use something like cityhash if the keys are too long. # TODO: Add a builtin to distinguish big endian systems and change loading orders. # TODO: Switch out Wymum on systems with slow 128bit multiplication. -LowLevelHasher := { originalSeed : U64, state : U64 } implements [ +LowLevelHasher := { initializedSeed : U64, state : U64 } implements [ Hasher { addBytes, addU8, @@ -1036,14 +1036,30 @@ createLowLevelHasher = \seedOpt -> when seedOpt is PseudoRandSeed -> pseudoSeed {} WithSeed s -> s - @LowLevelHasher { originalSeed: seed, state: seed } + @LowLevelHasher { initializedSeed: initSeed seed, state: seed } combineState : LowLevelHasher, { a : U64, b : U64, seed : U64, length : U64 } -> LowLevelHasher -combineState = \@LowLevelHasher { originalSeed, state }, { a, b, seed, length } -> - tmp = wymix (Num.bitwiseXor wyp1 a) (Num.bitwiseXor seed b) - hash = wymix (Num.bitwiseXor wyp1 length) tmp +combineState = \@LowLevelHasher { initializedSeed, state }, { a, b, seed, length } -> + mum = + a + |> Num.bitwiseXor wyp1 + |> wymum (Num.bitwiseXor b seed) + nexta = + mum.lower + |> Num.bitwiseXor wyp0 + |> Num.bitwiseXor length + nextb = + mum.upper + |> Num.bitwiseXor wyp1 + hash = wymix nexta nextb - @LowLevelHasher { originalSeed, state: wymix state hash } + @LowLevelHasher { initializedSeed, state: wymix state hash } + +initSeed = \seed -> + seed + |> Num.bitwiseXor wyp0 + |> wymix wyp1 + |> Num.bitwiseXor seed complete = \@LowLevelHasher { state } -> state @@ -1052,8 +1068,7 @@ complete = \@LowLevelHasher { state } -> state # like using the output of the last hash as the seed to the current hash. # I am simply not sure the tradeoffs here. Theoretically this method is more sound. # Either way, the performance will be similar and we can change this later. -addU8 = \@LowLevelHasher { originalSeed, state }, u8 -> - seed = Num.bitwiseXor originalSeed wyp0 +addU8 = \@LowLevelHasher { initializedSeed, state }, u8 -> p0 = Num.toU64 u8 a = Num.shiftLeftBy p0 16 @@ -1061,10 +1076,9 @@ addU8 = \@LowLevelHasher { originalSeed, state }, u8 -> |> Num.bitwiseOr p0 b = 0 - combineState (@LowLevelHasher { originalSeed, state }) { a, b, seed, length: 1 } + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 1 } -addU16 = \@LowLevelHasher { originalSeed, state }, u16 -> - seed = Num.bitwiseXor originalSeed wyp0 +addU16 = \@LowLevelHasher { initializedSeed, state }, u16 -> p0 = Num.bitwiseAnd u16 0xFF |> Num.toU64 p1 = Num.shiftRightZfBy u16 8 |> Num.toU64 a = @@ -1073,26 +1087,23 @@ addU16 = \@LowLevelHasher { originalSeed, state }, u16 -> |> Num.bitwiseOr p1 b = 0 - combineState (@LowLevelHasher { originalSeed, state }) { a, b, seed, length: 2 } + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 2 } -addU32 = \@LowLevelHasher { originalSeed, state }, u32 -> - seed = Num.bitwiseXor originalSeed wyp0 +addU32 = \@LowLevelHasher { initializedSeed, state }, u32 -> p0 = Num.toU64 u32 a = Num.shiftLeftBy p0 32 |> Num.bitwiseOr p0 - combineState (@LowLevelHasher { originalSeed, state }) { a, b: a, seed, length: 4 } + combineState (@LowLevelHasher { initializedSeed, state }) { a, b: a, seed: initializedSeed, length: 4 } -addU64 = \@LowLevelHasher { originalSeed, state }, u64 -> - seed = Num.bitwiseXor originalSeed wyp0 +addU64 = \@LowLevelHasher { initializedSeed, state }, u64 -> p0 = Num.bitwiseAnd 0xFFFF_FFFF u64 p1 = Num.shiftRightZfBy u64 32 a = Num.shiftLeftBy p0 32 |> Num.bitwiseOr p1 b = Num.shiftLeftBy p1 32 |> Num.bitwiseOr p0 - combineState (@LowLevelHasher { originalSeed, state }) { a, b, seed, length: 8 } + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 8 } -addU128 = \@LowLevelHasher { originalSeed, state }, u128 -> - seed = Num.bitwiseXor originalSeed wyp0 +addU128 = \@LowLevelHasher { initializedSeed, state }, u128 -> lower = u128 |> Num.toU64 upper = Num.shiftRightZfBy u128 64 |> Num.toU64 p0 = Num.bitwiseAnd 0xFFFF_FFFF lower @@ -1102,12 +1113,11 @@ addU128 = \@LowLevelHasher { originalSeed, state }, u128 -> a = Num.shiftLeftBy p0 32 |> Num.bitwiseOr p2 b = Num.shiftLeftBy p3 32 |> Num.bitwiseOr p1 - combineState (@LowLevelHasher { originalSeed, state }) { a, b, seed, length: 16 } + combineState (@LowLevelHasher { initializedSeed, state }) { a, b, seed: initializedSeed, length: 16 } addBytes : LowLevelHasher, List U8 -> LowLevelHasher -addBytes = \@LowLevelHasher { originalSeed, state }, list -> +addBytes = \@LowLevelHasher { initializedSeed, state }, list -> length = List.len list - seed = Num.bitwiseXor originalSeed wyp0 abs = if length <= 16 then if length >= 4 then @@ -1117,17 +1127,17 @@ addBytes = \@LowLevelHasher { originalSeed, state }, list -> (wyr4 list (Num.subWrap length 4) |> Num.shiftLeftBy 32) |> Num.bitwiseOr (wyr4 list (Num.subWrap length 4 |> Num.subWrap x)) - { a, b, seed } + { a, b, seed: initializedSeed } else if length > 0 then - { a: wyr3 list 0 length, b: 0, seed } + { a: wyr3 list 0 length, b: 0, seed: initializedSeed } else - { a: 0, b: 0, seed } + { a: 0, b: 0, seed: initializedSeed } else if length <= 48 then - hashBytesHelper16 seed list 0 length + hashBytesHelper16 initializedSeed list 0 length else - hashBytesHelper48 seed seed seed list 0 length + hashBytesHelper48 initializedSeed initializedSeed initializedSeed list 0 length - combineState (@LowLevelHasher { originalSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length } + combineState (@LowLevelHasher { initializedSeed, state }) { a: abs.a, b: abs.b, seed: abs.seed, length: Num.toU64 length } hashBytesHelper48 : U64, U64, U64, List U8, Nat, Nat -> { a : U64, b : U64, seed : U64 } hashBytesHelper48 = \seed, see1, see2, list, index, remaining -> @@ -1240,7 +1250,7 @@ expect |> addBytes [] |> complete - hash == 0x1C3F_F8BF_07F9_B0B3 + hash == 0xD59C59757DBBE6B3 expect hash = @@ -1248,7 +1258,7 @@ expect |> addBytes [0x42] |> complete - hash == 0x8F9F_0A1E_E06F_0D52 + hash == 0x38CE03D0E61AF963 expect hash = @@ -1256,7 +1266,7 @@ expect |> addU8 0x42 |> complete - hash == 0x8F9F_0A1E_E06F_0D52 + hash == 0x38CE03D0E61AF963 expect hash = @@ -1264,7 +1274,7 @@ expect |> addBytes [0xFF, 0xFF] |> complete - hash == 0x86CC_8B71_563F_F084 + hash == 0xE1CB2FA0D6A64113 expect hash = @@ -1272,7 +1282,7 @@ expect |> addU16 0xFFFF |> complete - hash == 0x86CC_8B71_563F_F084 + hash == 0xE1CB2FA0D6A64113 expect hash = @@ -1280,7 +1290,7 @@ expect |> addBytes [0x36, 0xA7] |> complete - hash == 0xD1A5_0F24_2536_84F8 + hash == 0x26B8319EDAF81B15 expect hash = @@ -1288,7 +1298,7 @@ expect |> addU16 0xA736 |> complete - hash == 0xD1A5_0F24_2536_84F8 + hash == 0x26B8319EDAF81B15 expect hash = @@ -1296,7 +1306,7 @@ expect |> addBytes [0x00, 0x00, 0x00, 0x00] |> complete - hash == 0x3762_ACB1_7604_B541 + hash == 0xA187D7CA074F9EE7 expect hash = @@ -1304,7 +1314,7 @@ expect |> addU32 0x0000_0000 |> complete - hash == 0x3762_ACB1_7604_B541 + hash == 0xA187D7CA074F9EE7 expect hash = @@ -1312,7 +1322,7 @@ expect |> addBytes [0xA9, 0x2F, 0xEE, 0x21] |> complete - hash == 0x20F3_3FD7_D32E_C7A9 + hash == 0xA499EFE4C1454D09 expect hash = @@ -1320,7 +1330,7 @@ expect |> addU32 0x21EE_2FA9 |> complete - hash == 0x20F3_3FD7_D32E_C7A9 + hash == 0xA499EFE4C1454D09 expect hash = @@ -1328,7 +1338,7 @@ expect |> addBytes [0x5D, 0x66, 0xB1, 0x8F, 0x68, 0x44, 0xC7, 0x03, 0xE1, 0xDD, 0x23, 0x34, 0xBB, 0x9A, 0x42, 0xA7] |> complete - hash == 0xA16F_DDAA_C167_74C7 + hash == 0xDD39A206AED64C73 expect hash = @@ -1336,7 +1346,7 @@ expect |> addU128 0xA742_9ABB_3423_DDE1_03C7_4468_8FB1_665D |> complete - hash == 0xA16F_DDAA_C167_74C7 + hash == 0xDD39A206AED64C73 expect hash = @@ -1344,7 +1354,7 @@ expect |> Hash.hashStrBytes "abcdefghijklmnopqrstuvwxyz" |> complete - hash == 0xBEE0_A8FD_E990_D285 + hash == 0x51C59DF5B1D15F40 expect hash = @@ -1352,7 +1362,7 @@ expect |> Hash.hashStrBytes "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |> complete - hash == 0xB3C5_8528_9D82_A6EF + hash == 0xD8D0A129D97A4E95 expect hash = @@ -1360,7 +1370,7 @@ expect |> Hash.hashStrBytes "1234567890123456789012345678901234567890123456789012345678901234567890" |> complete - hash == 0xDB6B_7997_7A55_BA03 + hash == 0x8188065B44FB4AAA expect hash = @@ -1368,7 +1378,7 @@ expect |> addBytes (List.repeat 0x77 100) |> complete - hash == 0x171F_EEE2_B764_8E5E + hash == 0x47A2A606EADF3378 # Note, had to specify u8 in the lists below to avoid ability type resolution error. # Apparently it won't pick the default integer. @@ -1378,7 +1388,7 @@ expect |> Hash.hashUnordered [8u8, 82u8, 3u8, 8u8, 24u8] List.walk |> complete - hash == 0x999F_B530_3529_F17D + hash == 0xB2E8254C08F16B20 expect hash1 = From e6f3b019189f2ca6c80aa753df8dcd509ba74c07 Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Fri, 8 Sep 2023 18:28:33 -0700 Subject: [PATCH 07/28] update mono tests --- crates/compiler/test_mono/generated/dict.txt | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/compiler/test_mono/generated/dict.txt b/crates/compiler/test_mono/generated/dict.txt index dc6e7bcd26..d5a7ec6742 100644 --- a/crates/compiler/test_mono/generated/dict.txt +++ b/crates/compiler/test_mono/generated/dict.txt @@ -1,28 +1,28 @@ -procedure Dict.1 (Dict.556): - let Dict.565 : List {[], []} = Array []; - let Dict.572 : U64 = 0i64; - let Dict.573 : U64 = 8i64; - let Dict.566 : List U64 = CallByName List.11 Dict.572 Dict.573; - let Dict.569 : I8 = CallByName Dict.39; - let Dict.570 : U64 = 8i64; - let Dict.567 : List I8 = CallByName List.11 Dict.569 Dict.570; - let Dict.568 : U64 = 0i64; - let Dict.564 : {List {[], []}, List U64, List I8, U64} = Struct {Dict.565, Dict.566, Dict.567, Dict.568}; - ret Dict.564; +procedure Dict.1 (Dict.554): + let Dict.563 : List {[], []} = Array []; + let Dict.570 : U64 = 0i64; + let Dict.571 : U64 = 8i64; + let Dict.564 : List U64 = CallByName List.11 Dict.570 Dict.571; + let Dict.567 : I8 = CallByName Dict.39; + let Dict.568 : U64 = 8i64; + let Dict.565 : List I8 = CallByName List.11 Dict.567 Dict.568; + let Dict.566 : U64 = 0i64; + let Dict.562 : {List {[], []}, List U64, List I8, U64} = Struct {Dict.563, Dict.564, Dict.565, Dict.566}; + ret Dict.562; procedure Dict.39 (): - let Dict.571 : I8 = -128i64; - ret Dict.571; + let Dict.569 : I8 = -128i64; + ret Dict.569; -procedure Dict.4 (Dict.562): - let Dict.100 : U64 = StructAtIndex 3 Dict.562; - let #Derived_gen.8 : List {[], []} = StructAtIndex 0 Dict.562; +procedure Dict.4 (Dict.560): + let Dict.101 : U64 = StructAtIndex 3 Dict.560; + let #Derived_gen.8 : List {[], []} = StructAtIndex 0 Dict.560; dec #Derived_gen.8; - let #Derived_gen.7 : List U64 = StructAtIndex 1 Dict.562; + let #Derived_gen.7 : List U64 = StructAtIndex 1 Dict.560; dec #Derived_gen.7; - let #Derived_gen.6 : List I8 = StructAtIndex 2 Dict.562; + let #Derived_gen.6 : List I8 = StructAtIndex 2 Dict.560; dec #Derived_gen.6; - ret Dict.100; + ret Dict.101; procedure List.11 (List.124, List.125): let List.536 : List I8 = CallByName List.68 List.125; From 8f59ee9492e602029a5394b29c1961f2f6423a27 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 11:55:55 +0100 Subject: [PATCH 08/28] Create a new crate roc_repl_ui for shared CLI/web UI code --- Cargo.lock | 17 +- Cargo.toml | 1 + crates/README.md | 6 + crates/cli/Cargo.toml | 2 +- crates/repl_cli/Cargo.toml | 2 - crates/repl_cli/src/lib.rs | 3 +- crates/repl_test/Cargo.toml | 2 +- crates/repl_ui/Cargo.toml | 25 ++ crates/repl_ui/src/colors.rs | 16 ++ crates/repl_ui/src/lib.rs | 201 ++++++++++++++ crates/repl_ui/src/repl_state.rs | 431 +++++++++++++++++++++++++++++++ crates/repl_wasm/src/repl.rs | 2 - crates/reporting/src/report.rs | 1 + 13 files changed, 701 insertions(+), 8 deletions(-) create mode 100644 crates/repl_ui/Cargo.toml create mode 100644 crates/repl_ui/src/colors.rs create mode 100644 crates/repl_ui/src/lib.rs create mode 100644 crates/repl_ui/src/repl_state.rs diff --git a/Cargo.lock b/Cargo.lock index bdd7d5a49f..0ae2a711ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 76e0f62e83..83efa6883c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/crates/README.md b/crates/README.md index db2dc86d80..af10feae99 100644 --- a/crates/README.md +++ b/crates/README.md @@ -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. diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 771b3e1ebd..fe13bbba3b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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"] diff --git a/crates/repl_cli/Cargo.toml b/crates/repl_cli/Cargo.toml index 147b887b70..c85c3fd169 100644 --- a/crates/repl_cli/Cargo.toml +++ b/crates/repl_cli/Cargo.toml @@ -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" } diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index 324663b411..14d55cde4f 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -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, diff --git a/crates/repl_test/Cargo.toml b/crates/repl_test/Cargo.toml index 4858f695a9..c172616a5f 100644 --- a/crates/repl_test/Cargo.toml +++ b/crates/repl_test/Cargo.toml @@ -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"] diff --git a/crates/repl_ui/Cargo.toml b/crates/repl_ui/Cargo.toml new file mode 100644 index 0000000000..f57a1b683a --- /dev/null +++ b/crates/repl_ui/Cargo.toml @@ -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" diff --git a/crates/repl_ui/src/colors.rs b/crates/repl_ui/src/colors.rs new file mode 100644 index 0000000000..937e3b185f --- /dev/null +++ b/crates/repl_ui/src/colors.rs @@ -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; diff --git a/crates/repl_ui/src/lib.rs b/crates/repl_ui/src/lib.rs new file mode 100644 index 0000000000..1169d0beba --- /dev/null +++ b/crates/repl_ui/src/lib.rs @@ -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, + problems: Problems, + opt_var_name: Option, + 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 +} diff --git a/crates/repl_ui/src/repl_state.rs b/crates/repl_ui/src/repl_state.rs new file mode 100644 index 0000000000..755e9f64cf --- /dev/null +++ b/crates/repl_ui/src/repl_state.rs @@ -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, + past_def_idents: MutSet, + last_auto_ident: u64, +} + +impl Default for ReplState { + fn default() -> Self { + Self::new() + } +} + +pub enum ReplAction<'a> { + Eval { + opt_mono: Option>, + problems: Problems, + opt_var_name: Option, + }, + 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, + } + } + } +} diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index 5aa5c29766..b1703985d6 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -180,7 +180,6 @@ pub async fn entrypoint_from_js(src: String) -> Result { // 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 { 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 diff --git a/crates/reporting/src/report.rs b/crates/reporting/src/report.rs index 504c563635..ee30c30c6a 100644 --- a/crates/reporting/src/report.rs +++ b/crates/reporting/src/report.rs @@ -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, From 3923dad2038edf0b4bc0dd108347a7c2b32e67fa Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 13:01:31 +0100 Subject: [PATCH 09/28] Remove duplicated code from roc_repl_cli and get tests compiling --- Cargo.lock | 6 + crates/repl_cli/Cargo.toml | 1 + crates/repl_cli/src/colors.rs | 4 - crates/repl_cli/src/lib.rs | 110 ++------ crates/repl_cli/src/repl_state.rs | 431 ------------------------------ crates/repl_test/Cargo.toml | 5 + crates/repl_test/src/cli.rs | 2 +- crates/repl_test/src/state.rs | 104 ++++--- crates/repl_ui/Cargo.toml | 2 +- crates/repl_ui/src/lib.rs | 2 +- crates/repl_ui/src/repl_state.rs | 1 + 11 files changed, 107 insertions(+), 561 deletions(-) delete mode 100644 crates/repl_cli/src/colors.rs delete mode 100644 crates/repl_cli/src/repl_state.rs diff --git a/Cargo.lock b/Cargo.lock index 0ae2a711ba..cb436c9d70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/repl_cli/Cargo.toml b/crates/repl_cli/Cargo.toml index c85c3fd169..8b0bfe91e2 100644 --- a/crates/repl_cli/Cargo.toml +++ b/crates/repl_cli/Cargo.toml @@ -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 diff --git a/crates/repl_cli/src/colors.rs b/crates/repl_cli/src/colors.rs deleted file mode 100644 index c85abd181e..0000000000 --- a/crates/repl_cli/src/colors.rs +++ /dev/null @@ -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"; diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index 14d55cde4f..2e7fef7f69 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -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>, + problems: Problems, + opt_var_name: Option, + 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 diff --git a/crates/repl_cli/src/repl_state.rs b/crates/repl_cli/src/repl_state.rs deleted file mode 100644 index 755e9f64cf..0000000000 --- a/crates/repl_cli/src/repl_state.rs +++ /dev/null @@ -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, - past_def_idents: MutSet, - last_auto_ident: u64, -} - -impl Default for ReplState { - fn default() -> Self { - Self::new() - } -} - -pub enum ReplAction<'a> { - Eval { - opt_mono: Option>, - problems: Problems, - opt_var_name: Option, - }, - 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, - } - } - } -} diff --git a/crates/repl_test/Cargo.toml b/crates/repl_test/Cargo.toml index c172616a5f..be2755ff1d 100644 --- a/crates/repl_test/Cargo.toml +++ b/crates/repl_test/Cargo.toml @@ -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"] diff --git a/crates/repl_test/src/cli.rs b/crates/repl_test/src/cli.rs index 314d31ad86..0755a7ec09 100644 --- a/crates/repl_test/src/cli.rs +++ b/crates/repl_test/src/cli.rs @@ -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 = '─'; diff --git a/crates/repl_test/src/state.rs b/crates/repl_test/src/state.rs index 348b7c7180..c6edc7f813 100644 --- a/crates/repl_test/src/state.rs +++ b/crates/repl_test/src/state.rs @@ -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::::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::::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); + } + } } diff --git a/crates/repl_ui/Cargo.toml b/crates/repl_ui/Cargo.toml index f57a1b683a..bc5fec60ec 100644 --- a/crates/repl_ui/Cargo.toml +++ b/crates/repl_ui/Cargo.toml @@ -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 diff --git a/crates/repl_ui/src/lib.rs b/crates/repl_ui/src/lib.rs index 1169d0beba..e4b22f6684 100644 --- a/crates/repl_ui/src/lib.rs +++ b/crates/repl_ui/src/lib.rs @@ -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; diff --git a/crates/repl_ui/src/repl_state.rs b/crates/repl_ui/src/repl_state.rs index 755e9f64cf..a370461bb8 100644 --- a/crates/repl_ui/src/repl_state.rs +++ b/crates/repl_ui/src/repl_state.rs @@ -34,6 +34,7 @@ impl Default for ReplState { } } +#[derive(Debug)] pub enum ReplAction<'a> { Eval { opt_mono: Option>, From 8eaa79e72c6d776a5fc04a907bc438fb8995d006 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 14:00:42 +0100 Subject: [PATCH 10/28] clippy & spellcheck --- crates/repl_ui/src/repl_state.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/repl_ui/src/repl_state.rs b/crates/repl_ui/src/repl_state.rs index a370461bb8..e9320408a0 100644 --- a/crates/repl_ui/src/repl_state.rs +++ b/crates/repl_ui/src/repl_state.rs @@ -35,6 +35,7 @@ impl Default for ReplState { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum ReplAction<'a> { Eval { opt_mono: Option>, @@ -62,7 +63,7 @@ impl ReplState { target_info: TargetInfo, palette: Palette, ) -> ReplAction<'a> { - match parse_src(&arena, line) { + match parse_src(arena, line) { ParseOutcome::Empty | ParseOutcome::Help => ReplAction::Help, ParseOutcome::Expr(_) | ParseOutcome::ValueDef(_) @@ -82,7 +83,7 @@ impl ReplState { ) -> ReplAction<'a> { let pending_past_def; let mut opt_var_name; - let src = match parse_src(&arena, src) { + 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 @@ -105,7 +106,7 @@ impl ReplState { 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. + // cannot be evaluated as expressions. return ReplAction::Nothing; } ValueDef::Body( @@ -132,7 +133,7 @@ impl ReplState { // 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, + arena, ); buf.push_str(src); @@ -200,7 +201,7 @@ impl ReplState { opt_var_name = Some(existing_ident); compile_to_mono( - &arena, + arena, self.past_defs.iter().map(|def| def.src.as_str()), src, target_info, @@ -209,7 +210,7 @@ impl ReplState { } None => { let (output, problems) = compile_to_mono( - &arena, + arena, self.past_defs.iter().map(|def| def.src.as_str()), src, target_info, From 4367e357e6d5c3396671e88c86c33972ca873215 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 14:20:15 +0100 Subject: [PATCH 11/28] Neater approach for Wasm compile-time options --- crates/repl_ui/src/colors.rs | 16 +++---- crates/repl_ui/src/lib.rs | 87 +++++++++++++++++------------------- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/crates/repl_ui/src/colors.rs b/crates/repl_ui/src/colors.rs index 937e3b185f..0d35b9daf4 100644 --- a/crates/repl_ui/src/colors.rs +++ b/crates/repl_ui/src/colors.rs @@ -1,14 +1,10 @@ -#[cfg(target_family = "wasm")] -use roc_reporting::report::{StyleCodes, HTML_STYLE_CODES}; +use roc_reporting::report::{StyleCodes, ANSI_STYLE_CODES, 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; +const STYLE_CODES: StyleCodes = if cfg!(target_family = "wasm") { + HTML_STYLE_CODES +} else { + ANSI_STYLE_CODES +}; pub const BLUE: &str = STYLE_CODES.blue; pub const PINK: &str = STYLE_CODES.magenta; diff --git a/crates/repl_ui/src/lib.rs b/crates/repl_ui/src/lib.rs index e4b22f6684..92dfdbf403 100644 --- a/crates/repl_ui/src/lib.rs +++ b/crates/repl_ui/src/lib.rs @@ -1,4 +1,5 @@ //! UI functionality, shared between CLI and web, for the Read-Evaluate-Print-Loop (REPL). +// We don't do anything here related to the terminal (doesn't exist on the web) or LLVM (too big for the web). pub mod colors; pub mod repl_state; @@ -22,50 +23,6 @@ pub const WELCOME_MESSAGE: &str = concatcp!( "\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 ", @@ -81,7 +38,47 @@ pub const TIPS: &str = concatcp!( "val1", END_COL, " in future expressions.\n\n", - TARGET_SPECIFIC_TIPS + "Tips:\n\n", + if cfg!(target_family = "wasm") { + // In the web REPL, the :quit command doesn't make sense. Just close the browser tab! + // We use ctrl-Enter for newlines because it's nicer than our workaround for Unix terminals (see below) + concatcp!( + BLUE, + " - ", + END_COL, + PINK, + "ctrl-Enter", + END_COL, + " makes a newline\n\n", + BLUE, + " - ", + END_COL, + ":help" + ) + } else { + // We use ctrl-v + ctrl-j for newlines because on Unix, terminals cannot distinguish between ctrl-Enter and Enter + 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" + ) + } ); // For when nothing is entered in the repl From fc7b831285faeb38f3f6a4f090add4ae0cd1505d Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 14:45:14 +0100 Subject: [PATCH 12/28] remove some more duplicated code (format_output) --- crates/repl_cli/src/lib.rs | 92 ++------------------------------------ 1 file changed, 4 insertions(+), 88 deletions(-) diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index 2e7fef7f69..8f88c26c8b 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -6,11 +6,10 @@ use std::borrow::Cow; use bumpalo::Bump; use roc_load::MonomorphizedModule; use roc_mono::ir::OptLevel; -use roc_repl_eval::gen::{Problems, ReplOutput}; -use roc_repl_ui::colors::{END_COL, GREEN, PINK}; +use roc_repl_eval::gen::Problems; 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_repl_ui::{is_incomplete, format_output, CONT_PROMPT, PROMPT, SHORT_INSTRUCTIONS, TIPS, WELCOME_MESSAGE}; +use roc_reporting::report::{DEFAULT_PALETTE, ANSI_STYLE_CODES}; use roc_target::TargetInfo; use rustyline::highlight::{Highlighter, PromptInfo}; use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; @@ -104,7 +103,7 @@ pub fn evaluate<'a>( 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) + format_output(ANSI_STYLE_CODES, opt_output, problems, opt_var_name, dimensions) } #[derive(Default)] @@ -150,86 +149,3 @@ impl Validator for ReplHelper { self.validator.validate_while_typing() } } - -fn format_output( - opt_output: Option, - problems: Problems, - opt_var_name: Option, - 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(PINK); // Color for the type separator - buf.push_str(EXPR_TYPE_SEPARATOR); - buf.push_str(END_COL); - 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(GREEN); - buf.push_str(VAR_NAME_PREFIX); - buf.push_str(&var_name); - buf.push_str(END_COL); - buf.push('\n'); - } - } - } - - buf -} From aa0e9758457d41f659c0f6509492e95bc31a7eff Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 15:03:50 +0100 Subject: [PATCH 13/28] clippy --- Cargo.lock | 1 + crates/repl_cli/src/cli_gen.rs | 8 +++--- crates/repl_cli/src/lib.rs | 4 +-- crates/repl_wasm/Cargo.toml | 1 + crates/repl_wasm/build.rs | 10 ++------ crates/repl_wasm/src/repl.rs | 47 ++++++---------------------------- 6 files changed, 18 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb436c9d70..fe3d16389e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3962,6 +3962,7 @@ dependencies = [ "roc_load", "roc_parse", "roc_repl_eval", + "roc_repl_ui", "roc_reporting", "roc_solve", "roc_target", diff --git a/crates/repl_cli/src/cli_gen.rs b/crates/repl_cli/src/cli_gen.rs index 061124fd68..2d900f7828 100644 --- a/crates/repl_cli/src/cli_gen.rs +++ b/crates/repl_cli/src/cli_gen.rs @@ -20,8 +20,8 @@ use roc_types::pretty_print::{name_and_print_var, DebugPrint}; use roc_types::subs::Subs; use target_lexicon::Triple; -pub fn eval_llvm<'a>( - mut loaded: MonomorphizedModule<'a>, +pub fn eval_llvm( + mut loaded: MonomorphizedModule<'_>, target: &Triple, opt_level: OptLevel, ) -> Option { @@ -55,7 +55,7 @@ pub fn eval_llvm<'a>( 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"); + mono_module_to_dylib(&arena, target, loaded, opt_level).expect("we produce a valid Dylib"); let mut app = CliApp { lib }; @@ -256,6 +256,6 @@ fn mono_module_to_dylib<'a>( internal_error!("Errors defining module:\n{}", errors.to_string()); } - llvm_module_to_dylib(env.module, &target, opt_level) + llvm_module_to_dylib(env.module, target, opt_level) .map(|lib| (lib, main_fn_name, subs, layout_interner)) } diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index 8f88c26c8b..f6c502a77a 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -95,8 +95,8 @@ pub fn main() -> i32 { } } -pub fn evaluate<'a>( - opt_mono: Option>, +pub fn evaluate( + opt_mono: Option>, problems: Problems, opt_var_name: Option, target: &Triple, diff --git a/crates/repl_wasm/Cargo.toml b/crates/repl_wasm/Cargo.toml index a6c4eed509..4718b680a9 100644 --- a/crates/repl_wasm/Cargo.toml +++ b/crates/repl_wasm/Cargo.toml @@ -31,6 +31,7 @@ roc_gen_wasm = { path = "../compiler/gen_wasm" } roc_load = { path = "../compiler/load" } roc_parse = { path = "../compiler/parse" } roc_repl_eval = { path = "../repl_eval" } +roc_repl_ui = { path = "../repl_ui" } roc_reporting = { path = "../reporting" } roc_solve = { path = "../compiler/solve" } roc_target = { path = "../compiler/roc_target" } diff --git a/crates/repl_wasm/build.rs b/crates/repl_wasm/build.rs index 150eb81c4e..3dceb7cf89 100644 --- a/crates/repl_wasm/build.rs +++ b/crates/repl_wasm/build.rs @@ -6,12 +6,6 @@ use wasi_libc_sys::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; const PLATFORM_FILENAME: &str = "repl_platform"; -#[cfg(not(windows))] -const OBJECT_EXTENSION: &str = "o"; - -#[cfg(windows)] -const OBJECT_EXTENSION: &str = "obj"; - fn main() { println!("cargo:rerun-if-changed=build.rs"); let source_path = format!("src/{PLATFORM_FILENAME}.c"); @@ -26,7 +20,7 @@ fn main() { let mut pre_linked_binary_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()); pre_linked_binary_path.extend(["pre_linked_binary"]); - pre_linked_binary_path.set_extension(OBJECT_EXTENSION); + pre_linked_binary_path.set_extension("wasm"); let builtins_host_tempfile = roc_bitcode::host_wasm_tempfile() .expect("failed to write host builtins object to tempfile"); @@ -65,7 +59,7 @@ fn zig_executable() -> String { fn build_wasm_platform(out_dir: &str, source_path: &str) -> PathBuf { let mut platform_obj = PathBuf::from(out_dir).join(PLATFORM_FILENAME); - platform_obj.set_extension(OBJECT_EXTENSION); + platform_obj.set_extension("wasm"); Command::new(zig_executable()) .args([ diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index b1703985d6..4738feeeed 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -1,16 +1,12 @@ use bumpalo::{collections::vec::Vec, Bump}; -use std::mem::size_of; +use std::{cell::RefCell, mem::size_of}; use roc_collections::all::MutSet; use roc_gen_wasm::wasm32_result; use roc_load::MonomorphizedModule; use roc_parse::ast::Expr; -use roc_repl_eval::{ - eval::jit_to_ast, - gen::{compile_to_mono, format_answer}, - ReplApp, ReplAppMemory, -}; -use roc_reporting::report::DEFAULT_PALETTE_HTML; +use roc_repl_eval::{eval::jit_to_ast, gen::format_answer, ReplApp, ReplAppMemory}; +use roc_repl_ui::repl_state::{ReplAction, ReplState}; use roc_target::TargetInfo; use roc_types::pretty_print::{name_and_print_var, DebugPrint}; @@ -18,6 +14,10 @@ use crate::{js_create_app, js_get_result_and_memory, js_run_app}; const WRAPPER_NAME: &str = "wrapper"; +std::thread_local! { + static REPL_STATE: RefCell = RefCell::new(ReplState::new()); +} + pub struct WasmReplApp<'a> { arena: &'a Bump, } @@ -164,13 +164,8 @@ impl<'a> ReplApp<'a> for WasmReplApp<'a> { } } -#[cfg(not(windows))] const PRE_LINKED_BINARY: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.o")) as &[_]; - -#[cfg(windows)] -const PRE_LINKED_BINARY: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.obj")) as &[_]; + include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.wasm")) as &[_]; pub async fn entrypoint_from_js(src: String) -> Result { #[cfg(feature = "console_error_panic_hook")] @@ -180,32 +175,6 @@ pub async fn entrypoint_from_js(src: String) -> Result { // Compile the app let target_info = TargetInfo::default_wasm32(); - // 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( - arena, - std::iter::empty(), - &src, - target_info, - DEFAULT_PALETTE_HTML, - ) { - (Some(m), problems) if problems.is_empty() => m, // TODO render problems and continue if possible - (_, problems) => { - // TODO always report these, but continue if possible with the MonomorphizedModule if we have one. - let mut buf = String::new(); - - // Join all the errors and warnings together with blank lines. - for message in problems.errors.iter().chain(problems.warnings.iter()) { - if !buf.is_empty() { - buf.push_str("\n\n"); - } - - buf.push_str(message); - } - - return Err(buf); - } - }; let MonomorphizedModule { module_id, From f007d5c33ab98d7fec9214633554fc9be4cf99d2 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 15:47:22 +0100 Subject: [PATCH 14/28] Integrate UI state machine into Web REPL --- crates/repl_wasm/src/externs_js.rs | 2 +- crates/repl_wasm/src/repl.rs | 77 +++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/crates/repl_wasm/src/externs_js.rs b/crates/repl_wasm/src/externs_js.rs index 9baaa8b633..72ad6c20d1 100644 --- a/crates/repl_wasm/src/externs_js.rs +++ b/crates/repl_wasm/src/externs_js.rs @@ -30,6 +30,6 @@ macro_rules! console_log { /// The browser only has an async API to generate a Wasm module from bytes /// wasm_bindgen manages the interaction between Rust Futures and JS Promises #[wasm_bindgen] -pub async fn entrypoint_from_js(src: String) -> Result { +pub async fn entrypoint_from_js(src: String) -> String { crate::repl::entrypoint_from_js(src).await } diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index 4738feeeed..ea5535950f 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -1,12 +1,21 @@ use bumpalo::{collections::vec::Vec, Bump}; +use roc_reporting::report::{DEFAULT_PALETTE_HTML, HTML_STYLE_CODES}; use std::{cell::RefCell, mem::size_of}; use roc_collections::all::MutSet; use roc_gen_wasm::wasm32_result; use roc_load::MonomorphizedModule; use roc_parse::ast::Expr; -use roc_repl_eval::{eval::jit_to_ast, gen::format_answer, ReplApp, ReplAppMemory}; -use roc_repl_ui::repl_state::{ReplAction, ReplState}; +use roc_repl_eval::{ + eval::jit_to_ast, + gen::{format_answer, ReplOutput}, + ReplApp, ReplAppMemory, +}; +use roc_repl_ui::{ + format_output, + repl_state::{ReplAction, ReplState}, + TIPS, +}; use roc_target::TargetInfo; use roc_types::pretty_print::{name_and_print_var, DebugPrint}; @@ -167,15 +176,50 @@ impl<'a> ReplApp<'a> for WasmReplApp<'a> { const PRE_LINKED_BINARY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.wasm")) as &[_]; -pub async fn entrypoint_from_js(src: String) -> Result { +pub async fn entrypoint_from_js(src: String) -> String { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); + // TODO: make this a global and reset it? let arena = &Bump::new(); // Compile the app let target_info = TargetInfo::default_wasm32(); + let res_action = REPL_STATE.with(|repl_state| { + let mut repl_state = repl_state.borrow_mut(); + repl_state.step(arena, &src, target_info, DEFAULT_PALETTE_HTML) + }); + + match res_action { + ReplAction::Help => TIPS.to_string(), + ReplAction::Exit | ReplAction::Nothing => String::new(), + ReplAction::Eval { + opt_mono, + problems, + opt_var_name, + } => { + let opt_output = match opt_mono { + Some(mono) => eval_wasm(arena, target_info, mono).await, + None => None, + }; + let dimensions = None; // TODO: we could get the window dimensions from JS... + format_output( + HTML_STYLE_CODES, + opt_output, + problems, + opt_var_name, + dimensions, + ) + } + } +} + +async fn eval_wasm<'a>( + arena: &'a Bump, + target_info: TargetInfo, + mono: MonomorphizedModule<'a>, +) -> Option { let MonomorphizedModule { module_id, procedures, @@ -192,7 +236,7 @@ pub async fn entrypoint_from_js(src: String) -> Result { let main_fn_var = *main_fn_var; // pretty-print the expr type string for later. - let expr_type_str = name_and_print_var( + let expr_type = name_and_print_var( main_fn_var, &mut subs, module_id, @@ -200,10 +244,7 @@ pub async fn entrypoint_from_js(src: String) -> Result { DebugPrint::NOTHING, ); - let (_, main_fn_layout) = match procedures.keys().find(|(s, _)| *s == main_fn_symbol) { - Some(layout) => *layout, - None => return Ok(format!(" : {expr_type_str}")), - }; + let (_, main_fn_layout) = *procedures.keys().find(|(s, _)| *s == main_fn_symbol)?; let app_module_bytes = { let env = roc_gen_wasm::Env { @@ -247,9 +288,16 @@ pub async fn entrypoint_from_js(src: String) -> Result { }; // Send the compiled binary out to JS and create an executable instance from it - js_create_app(&app_module_bytes) - .await - .map_err(|js| format!("{js:?}"))?; + let create_result = js_create_app(&app_module_bytes).await; + match create_result { + Ok(()) => {} + Err(js_exception) => { + return Some(ReplOutput { + expr: format!("{js_exception:?}"), + expr_type: String::new(), + }) + } + } let mut app = WasmReplApp { arena }; @@ -267,11 +315,8 @@ pub async fn entrypoint_from_js(src: String) -> Result { target_info, ); - let var_name = String::new(); // TODO turn this into something like " # val1" - // Transform the Expr to a string - // `Result::Err` becomes a JS exception that will be caught and displayed - let expr = format_answer(arena, res_answer); + let expr = format_answer(arena, res_answer).to_string(); - Ok(format!("{expr} : {expr_type_str}{var_name}")) + Some(ReplOutput { expr, expr_type }) } From adc97a00994566f9b84afe4e28843abe30a71e9e Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 16:00:01 +0100 Subject: [PATCH 15/28] Renaming and comments --- crates/repl_wasm/src/repl.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index ea5535950f..448120047c 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -186,14 +186,17 @@ pub async fn entrypoint_from_js(src: String) -> String { // Compile the app let target_info = TargetInfo::default_wasm32(); - let res_action = REPL_STATE.with(|repl_state| { - let mut repl_state = repl_state.borrow_mut(); + // On the web, we keep the REPL state in a global variable, because `main` is not in our Rust code! + // We return back to JS after every line of input. `main` is in the browser engine, running the JS event loop. + let res_action = REPL_STATE.with(|repl_state_cell| { + let mut repl_state = repl_state_cell.borrow_mut(); repl_state.step(arena, &src, target_info, DEFAULT_PALETTE_HTML) }); match res_action { ReplAction::Help => TIPS.to_string(), - ReplAction::Exit | ReplAction::Nothing => String::new(), + ReplAction::Exit => ":quit does not work on the web! You can close the browser tab though! Thanks for trying Roc!".to_string(), + ReplAction::Nothing => String::new(), ReplAction::Eval { opt_mono, problems, From 6a47a9911456907290e6bd4dcff905bd48267f0d Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 16:03:54 +0100 Subject: [PATCH 16/28] formatting & clippy --- crates/repl_cli/src/lib.rs | 14 +++++++++++--- crates/repl_test/src/state.rs | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index f6c502a77a..d4641c028f 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -8,8 +8,10 @@ use roc_load::MonomorphizedModule; use roc_mono::ir::OptLevel; use roc_repl_eval::gen::Problems; use roc_repl_ui::repl_state::{ReplAction, ReplState}; -use roc_repl_ui::{is_incomplete, format_output, CONT_PROMPT, PROMPT, SHORT_INSTRUCTIONS, TIPS, WELCOME_MESSAGE}; -use roc_reporting::report::{DEFAULT_PALETTE, ANSI_STYLE_CODES}; +use roc_repl_ui::{ + format_output, is_incomplete, CONT_PROMPT, PROMPT, SHORT_INSTRUCTIONS, TIPS, WELCOME_MESSAGE, +}; +use roc_reporting::report::{ANSI_STYLE_CODES, DEFAULT_PALETTE}; use roc_target::TargetInfo; use rustyline::highlight::{Highlighter, PromptInfo}; use rustyline::validate::{self, ValidationContext, ValidationResult, Validator}; @@ -103,7 +105,13 @@ pub fn evaluate( dimensions: Option<(usize, usize)>, ) -> String { let opt_output = opt_mono.and_then(|mono| eval_llvm(mono, target, OptLevel::Normal)); - format_output(ANSI_STYLE_CODES, opt_output, problems, opt_var_name, dimensions) + format_output( + ANSI_STYLE_CODES, + opt_output, + problems, + opt_var_name, + dimensions, + ) } #[derive(Default)] diff --git a/crates/repl_test/src/state.rs b/crates/repl_test/src/state.rs index c6edc7f813..04c45faeec 100644 --- a/crates/repl_test/src/state.rs +++ b/crates/repl_test/src/state.rs @@ -151,7 +151,7 @@ fn complete(input: &str, state: &mut ReplState, expected_start: &str, expected_e ); } _ => { - assert!(false, "Unexpected action: {:?}", action); + panic!("Unexpected action: {:?}", action); } } } @@ -190,7 +190,7 @@ fn error(input: &str, state: &mut ReplState, expected_step_result: String) { assert_eq!(expected_step_result, escaped); } _ => { - assert!(false, "Unexpected action: {:?}", action); + panic!("Unexpected action: {:?}", action); } } } From ee2731b1528698cc028c358155d617a0714d1c8b Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 16:19:03 +0100 Subject: [PATCH 17/28] Remove warning on web REPL page --- www/public/repl/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/www/public/repl/index.html b/www/public/repl/index.html index fdb8c971a6..b71fc2c87b 100644 --- a/www/public/repl/index.html +++ b/www/public/repl/index.html @@ -33,7 +33,6 @@ > -

⚠️ This web REPL misses some features that are available in the CLI (roc repl) like defining variables without a final expression, which will result in the "Missing Final Expression" error.

From 03b3b34e3d950ef23bbdcb5202624b19fc911143 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 16:53:47 +0100 Subject: [PATCH 18/28] Reduce font size on the web REPL --- www/public/repl/repl.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/public/repl/repl.css b/www/public/repl/repl.css index 6905b7cd59..4b48a72178 100644 --- a/www/public/repl/repl.css +++ b/www/public/repl/repl.css @@ -6,7 +6,7 @@ body { background-color: #222; color: #ccc; font-family: sans-serif; - font-size: 18px; + font-size: 12px; } .body-wrapper { display: flex; From 7dfd4ada48078ee2b97ed1dfea52cb85397b3257 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 16:54:28 +0100 Subject: [PATCH 19/28] Support both Shift-Enter and Ctrl-Enter for multi-line input --- crates/repl_ui/src/lib.rs | 10 +++++++--- www/public/repl/repl.js | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/repl_ui/src/lib.rs b/crates/repl_ui/src/lib.rs index 92dfdbf403..d4ba4d8bb4 100644 --- a/crates/repl_ui/src/lib.rs +++ b/crates/repl_ui/src/lib.rs @@ -41,13 +41,17 @@ pub const TIPS: &str = concatcp!( "Tips:\n\n", if cfg!(target_family = "wasm") { // In the web REPL, the :quit command doesn't make sense. Just close the browser tab! - // We use ctrl-Enter for newlines because it's nicer than our workaround for Unix terminals (see below) + // We use Shift-Enter for newlines because it's nicer than our workaround for Unix terminals (see below) concatcp!( BLUE, " - ", END_COL, PINK, - "ctrl-Enter", + "Shift-Enter", + END_COL, + " or ", + PINK, + "Ctrl-Enter", END_COL, " makes a newline\n\n", BLUE, @@ -56,7 +60,7 @@ pub const TIPS: &str = concatcp!( ":help" ) } else { - // We use ctrl-v + ctrl-j for newlines because on Unix, terminals cannot distinguish between ctrl-Enter and Enter + // We use ctrl-v + ctrl-j for newlines because on Unix, terminals cannot distinguish between Shift-Enter and Enter concatcp!( BLUE, " - ", diff --git a/www/public/repl/repl.js b/www/public/repl/repl.js index 91248858f7..901b825a03 100644 --- a/www/public/repl/repl.js +++ b/www/public/repl/repl.js @@ -45,7 +45,7 @@ roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then((instance) => { repl.elemHistory.querySelector("#loading-message").remove(); repl.elemSourceInput.disabled = false; repl.elemSourceInput.placeholder = - "Type some Roc code and press Enter. (Use Shift+Enter for multi-line input)"; + "Type some Roc code and press Enter. (Use Shift-Enter or Ctrl-Enter for multi-line input)"; repl.compiler = instance; }); @@ -95,7 +95,7 @@ function onInputKeyup(event) { break; case ENTER: - if (!event.shiftKey) { + if (!event.shiftKey && !event.ctrlKey && !event.altKey) { onInputChange({ target: repl.elemSourceInput }); } break; From 50980f4b353eb1d785781223be645554a436a169 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 16:55:14 +0100 Subject: [PATCH 20/28] Show :help text on loading the web REPL --- www/public/repl/repl.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/public/repl/repl.js b/www/public/repl/repl.js index 901b825a03..1d7dea6882 100644 --- a/www/public/repl/repl.js +++ b/www/public/repl/repl.js @@ -47,6 +47,11 @@ roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then((instance) => { repl.elemSourceInput.placeholder = "Type some Roc code and press Enter. (Use Shift-Enter or Ctrl-Enter for multi-line input)"; repl.compiler = instance; + + // Show the help text by providing fake input, and then removing it! + repl.inputQueue.push(":help"); + processInputQueue(); + document.querySelector(".input").remove(); }); // ---------------------------------------------------------------------------- From 01a4a4d3569d808bac2bed4039b13f0016254b4c Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 17:17:03 +0100 Subject: [PATCH 21/28] Improve README instructions for web REPL local dev --- crates/repl_wasm/README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/repl_wasm/README.md b/crates/repl_wasm/README.md index ded22d4a41..7094aa8ac7 100644 --- a/crates/repl_wasm/README.md +++ b/crates/repl_wasm/README.md @@ -24,20 +24,29 @@ You should run it from the project root directory. ```bash crates/repl_wasm/build-www.sh -cp crates/repl_wasm/build/* www/build/repl/ ``` -### 2. Run a local HTTP server +### 3. Make symlinks to the generated Wasm and JS +```bash +cd www/public +ln -s ../../../crates/repl_wasm/build/roc_repl_wasm_bg.wasm +ln -s ../../../crates/repl_wasm/build/roc_repl_wasm.js +``` +These symlinks are ignored by Git. +This is slightly different from how we do the production build but it makes development easy. +You can directly edit files like repl.js and just reload your browser to see changes. + +### 4. Run a local HTTP server Browsers won't load .wasm files over the `file://` protocol, so you need to serve the files in `./www/build/` from a local web server. Any server will do, but this example should work on any system that has Python 3 installed: ```bash -cd www/build +cd www/public python3 -m http.server ``` -### 3. Open your browser +### 5. Open your browser You should be able to find the Roc REPL at (or whatever port your web server mentioned when it started up.) From e37d0c45c67748bac7777eea3961dd3dd4cc1c73 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sat, 9 Sep 2023 17:17:36 +0100 Subject: [PATCH 22/28] Fix types in web REPL tests --- crates/repl_test/src/wasm.rs | 20 ++++++-------------- crates/repl_wasm/src/externs_test.rs | 9 ++------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/crates/repl_test/src/wasm.rs b/crates/repl_test/src/wasm.rs index 80c664863a..1ec6f44e37 100644 --- a/crates/repl_test/src/wasm.rs +++ b/crates/repl_test/src/wasm.rs @@ -122,7 +122,7 @@ impl<'a> ImportDispatcher for CompilerDispatcher<'a> { } } -fn run(src: &'static str) -> Result { +fn run(src: &'static str) -> String { let arena = Bump::new(); let mut instance = { @@ -140,27 +140,19 @@ fn run(src: &'static str) -> Result { }; let len = Value::I32(src.len() as i32); - let wasm_ok: i32 = instance + assert!(instance .call_export("entrypoint_from_test", [len]) .unwrap() - .unwrap() - .expect_i32() - .unwrap(); - let answer_str = instance.import_dispatcher.answer.to_owned(); - - if wasm_ok == 0 { - Err(answer_str) - } else { - Ok(answer_str) - } + .is_none()); + instance.import_dispatcher.answer.to_owned() } #[allow(dead_code)] pub fn expect_success(input: &'static str, expected: &str) { - assert_eq!(run(input), Ok(expected.into())); + assert_eq!(run(input), expected.to_string()); } #[allow(dead_code)] pub fn expect_failure(input: &'static str, expected: &str) { - assert_eq!(run(input), Err(expected.into())); + assert_eq!(run(input), expected.to_string()); } diff --git a/crates/repl_wasm/src/externs_test.rs b/crates/repl_wasm/src/externs_test.rs index 4076958a00..f9c6b02c2c 100644 --- a/crates/repl_wasm/src/externs_test.rs +++ b/crates/repl_wasm/src/externs_test.rs @@ -30,19 +30,14 @@ pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize { /// - Synchronous API, to avoid the need to run an async executor across the Wasm/native boundary. /// - Uses an extra callback to allocate & copy the input string (in the browser version, wasm_bindgen does this) #[no_mangle] -pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool { +pub extern "C" fn entrypoint_from_test(src_len: usize) { let mut src_buffer = std::vec![0; src_len]; let src = unsafe { test_copy_input_string(src_buffer.as_mut_ptr()); String::from_utf8_unchecked(src_buffer) }; let result_async = crate::repl::entrypoint_from_js(src); - let result = executor::block_on(result_async); - let ok = result.is_ok(); - - let output = result.unwrap_or_else(|s| s); + let output = executor::block_on(result_async); unsafe { test_copy_output_string(output.as_ptr(), output.len()) } - - ok } From c65d638c1da0f7759ec6b832d2c815d97a166049 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:02:03 +0200 Subject: [PATCH 23/28] Apply Brendan's review suggestions Co-authored-by: Brendan Hansknecht Signed-off-by: Anton-4 <17049058+Anton-4@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- devtools/signing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c7e758972..2162635633 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,7 +53,7 @@ This command will generate the documentation in the [`generated-docs`](generated ### Commit signing -All your commits need to be signed [to prevent impersonation](https://dev.to/martiliones/how-i-got-linus-torvalds-in-my-contributors-on-github-3k4g). Check out our guide for commit signing [here](devtools/signing.md). +All your commits need to be signed [to prevent impersonation](https://dev.to/martiliones/how-i-got-linus-torvalds-in-my-contributors-on-github-3k4g). Check out [our guide for commit signing](devtools/signing.md). #### Commit signing on NixOS diff --git a/devtools/signing.md b/devtools/signing.md index 79304d91fd..1297b142de 100644 --- a/devtools/signing.md +++ b/devtools/signing.md @@ -41,7 +41,7 @@ Open the program Kleopatra (installed with gpg4win) and go to Smartcards. You should see your Yubikey there, it should also say something like `failed to find public key locally`. Click the import button, change the available file types at the bottom right to `Any files` and open the `public.key` file you created earlier. Close Kleopatra. -Install the `YubiKey Minidriver for 64-bit systems – Windows Installer` from [here](https://www.yubico.com/support/download/smart-card-drivers-tools/). +Install the [YubiKey Minidriver for 64-bit systems – Windows Installer](https://www.yubico.com/support/download/smart-card-drivers-tools/). Insert your Yubikey and check if it is mentioned in the output of `gpg --card-status` (powershell). From e11294cc7eee5a4b3dff81b7c7d723d443721e51 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:03:14 +0200 Subject: [PATCH 24/28] .key > .asc This simplifies the later import into kleopatra. Signed-off-by: Anton-4 <17049058+Anton-4@users.noreply.github.com> --- devtools/signing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devtools/signing.md b/devtools/signing.md index 1297b142de..16eb465422 100644 --- a/devtools/signing.md +++ b/devtools/signing.md @@ -30,15 +30,15 @@ This explanation was based on the steps outlined [here](https://scatteredcode.ne On linux, run: ``` gpg --list-keys --keyid-format SHORT | grep ^pub -gpg --export --armor [Your_Key_ID] > public.key +gpg --export --armor [Your_Key_ID] > public.asc ``` -Copy the public.key file to windows. +Copy the public.asc file to windows. Download and install [Gpg4win](https://www.gpg4win.org/get-gpg4win.html). Open the program Kleopatra (installed with gpg4win) and go to Smartcards. -You should see your Yubikey there, it should also say something like `failed to find public key locally`. Click the import button, change the available file types at the bottom right to `Any files` and open the `public.key` file you created earlier. +You should see your Yubikey there, it should also say something like `failed to find public key locally`. Click the import button and open the `public.asc` file you created earlier. Close Kleopatra. Install the [YubiKey Minidriver for 64-bit systems – Windows Installer](https://www.yubico.com/support/download/smart-card-drivers-tools/). From a99f9fee14e845a6caf94b9b93b53728e69e4637 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sun, 10 Sep 2023 08:26:56 +0100 Subject: [PATCH 25/28] Fix wasm REPL tests --- crates/repl_test/src/wasm.rs | 26 ++++++++++++++++++++------ crates/repl_test/test_wasm.sh | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/repl_test/src/wasm.rs b/crates/repl_test/src/wasm.rs index 1ec6f44e37..ab39f06792 100644 --- a/crates/repl_test/src/wasm.rs +++ b/crates/repl_test/src/wasm.rs @@ -140,19 +140,33 @@ fn run(src: &'static str) -> String { }; let len = Value::I32(src.len() as i32); - assert!(instance - .call_export("entrypoint_from_test", [len]) - .unwrap() - .is_none()); + instance.call_export("entrypoint_from_test", [len]).unwrap(); instance.import_dispatcher.answer.to_owned() } #[allow(dead_code)] pub fn expect_success(input: &'static str, expected: &str) { - assert_eq!(run(input), expected.to_string()); + expect(input, expected); } #[allow(dead_code)] pub fn expect_failure(input: &'static str, expected: &str) { - assert_eq!(run(input), expected.to_string()); + expect(input, expected); +} + +#[allow(dead_code)] +pub fn expect(input: &'static str, expected: &str) { + let raw_output = run(input); + + // We need to get rid of HTML tags, and we can be quite specific about it! + // If we ever write more complex test cases, we might need regex here. + let without_html = raw_output + .replace(" : ", " : ") + .replace(" # val1", ""); + + // Whitespace that was originally in front of the `# val1` is now at the end, + // and there's other whitespace at both ends too. Trim it all. + let clean_output = without_html.trim(); + + assert_eq!(clean_output, expected); } diff --git a/crates/repl_test/test_wasm.sh b/crates/repl_test/test_wasm.sh index 1e36648ac1..bbc8374d28 100755 --- a/crates/repl_test/test_wasm.sh +++ b/crates/repl_test/test_wasm.sh @@ -12,4 +12,4 @@ set -euxo pipefail RUSTFLAGS="" cargo build --locked --profile release-with-lto --target wasm32-wasi -p roc_repl_wasm --no-default-features --features wasi_test # Build & run the test code on *native* target, not WebAssembly -cargo test --locked --release -p repl_test --features wasm +cargo test --locked --release -p repl_test --features wasm $@ From 034482101fc112d9dcd2f59d8b7128bfaa7c08f6 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sun, 10 Sep 2023 11:26:29 +0100 Subject: [PATCH 26/28] Remove initial :help from web REPL history + fix edge cases --- www/public/repl/repl.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/www/public/repl/repl.js b/www/public/repl/repl.js index 1d7dea6882..0666c1549e 100644 --- a/www/public/repl/repl.js +++ b/www/public/repl/repl.js @@ -48,9 +48,13 @@ roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then((instance) => { "Type some Roc code and press Enter. (Use Shift-Enter or Ctrl-Enter for multi-line input)"; repl.compiler = instance; - // Show the help text by providing fake input, and then removing it! + // Show the help text by providing fake input repl.inputQueue.push(":help"); processInputQueue(); + + // Remove the fake input + repl.inputHistory.shift(); + repl.inputHistoryIndex = 0; document.querySelector(".input").remove(); }); @@ -80,6 +84,9 @@ function onInputKeyup(event) { switch (keyCode) { case UP: + if (repl.inputHistory.length === 0) { + return; + } if (repl.inputHistoryIndex == repl.inputHistory.length - 1) { repl.inputStash = el.value; } @@ -91,6 +98,9 @@ function onInputKeyup(event) { break; case DOWN: + if (repl.inputHistory.length === 0) { + return; + } if (repl.inputHistoryIndex === repl.inputHistory.length - 1) { setInput(repl.inputStash); } else { From ead3c33eb83f223ce0272944765c60195ea7c727 Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sun, 10 Sep 2023 11:28:29 +0100 Subject: [PATCH 27/28] Tweak comments and factoring --- crates/repl_cli/src/lib.rs | 4 ++-- crates/repl_wasm/src/repl.rs | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/repl_cli/src/lib.rs b/crates/repl_cli/src/lib.rs index d4641c028f..a3f7a89a99 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -55,8 +55,6 @@ pub fn main() -> i32 { arena.reset(); match repl_state.step(&arena, &line, target_info, DEFAULT_PALETTE) { - // If there was no output, don't print a blank line! - // (This happens for something like a type annotation.) ReplAction::Eval { opt_mono, problems, @@ -64,6 +62,8 @@ pub fn main() -> i32 { } => { let output = evaluate(opt_mono, problems, opt_var_name, &target, dimensions); + // If there was no output, don't print a blank line! + // (This happens for something like a type annotation.) if !output.is_empty() { println!("{output}"); } diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index 448120047c..636df8b0da 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -23,6 +23,8 @@ use crate::{js_create_app, js_get_result_and_memory, js_run_app}; const WRAPPER_NAME: &str = "wrapper"; +// On the web, we keep the REPL state in a global variable, because `main` is not in our Rust code! +// We return back to JS after every line of input. `main` is in the browser engine, running the JS event loop. std::thread_local! { static REPL_STATE: RefCell = RefCell::new(ReplState::new()); } @@ -177,6 +179,8 @@ const PRE_LINKED_BINARY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.wasm")) as &[_]; pub async fn entrypoint_from_js(src: String) -> String { + // If our Rust code panics, redirect the error message to JS console.error + // Also, our JS code overrides console.error to display the error message text (including stack trace) in the REPL output. #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); @@ -186,16 +190,18 @@ pub async fn entrypoint_from_js(src: String) -> String { // Compile the app let target_info = TargetInfo::default_wasm32(); - // On the web, we keep the REPL state in a global variable, because `main` is not in our Rust code! - // We return back to JS after every line of input. `main` is in the browser engine, running the JS event loop. - let res_action = REPL_STATE.with(|repl_state_cell| { + // Advance the REPL state machine + let action = REPL_STATE.with(|repl_state_cell| { let mut repl_state = repl_state_cell.borrow_mut(); repl_state.step(arena, &src, target_info, DEFAULT_PALETTE_HTML) }); - match res_action { + // Perform the action the state machine asked for, and return the appropriate output string + match action { ReplAction::Help => TIPS.to_string(), - ReplAction::Exit => ":quit does not work on the web! You can close the browser tab though! Thanks for trying Roc!".to_string(), + ReplAction::Exit => { + "To exit the web version of the REPL, just close the browser tab!".to_string() + } ReplAction::Nothing => String::new(), ReplAction::Eval { opt_mono, @@ -290,13 +296,12 @@ async fn eval_wasm<'a>( buffer }; - // Send the compiled binary out to JS and create an executable instance from it - let create_result = js_create_app(&app_module_bytes).await; - match create_result { + // Send the compiled binary out to JS, which will asynchronously create an executable WebAssembly instance + match js_create_app(&app_module_bytes).await { Ok(()) => {} Err(js_exception) => { return Some(ReplOutput { - expr: format!("{js_exception:?}"), + expr: format!("{js_exception:?}"), expr_type: String::new(), }) } From b8e66d08de75635f6a6c8f13698c62d2ca7cc23a Mon Sep 17 00:00:00 2001 From: Brian Carroll Date: Sun, 10 Sep 2023 18:36:41 +0100 Subject: [PATCH 28/28] Don't parse twice in ReplState::step --- crates/repl_ui/src/repl_state.rs | 35 ++++++++------------------------ 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/crates/repl_ui/src/repl_state.rs b/crates/repl_ui/src/repl_state.rs index e9320408a0..5808d2c988 100644 --- a/crates/repl_ui/src/repl_state.rs +++ b/crates/repl_ui/src/repl_state.rs @@ -62,28 +62,12 @@ impl ReplState { 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) { + let src: &str = match parse_src(arena, line) { + ParseOutcome::Empty | ParseOutcome::Help => return ReplAction::Help, + ParseOutcome::Exit => return ReplAction::Exit, ParseOutcome::Expr(_) | ParseOutcome::Incomplete | ParseOutcome::SyntaxErr => { pending_past_def = None; // If it's a SyntaxErr (or Incomplete at this point, meaning it will @@ -91,7 +75,7 @@ impl ReplState { // proceed as normal and let the error reporting happen during eval. opt_var_name = None; - src + line } ParseOutcome::ValueDef(value_def) => { match value_def { @@ -103,7 +87,7 @@ impl ReplState { _, ) => { // Record the standalone type annotation for future use. - self.add_past_def(ident.trim_end().to_string(), src.to_string()); + self.add_past_def(ident.trim_end().to_string(), line.to_string()); // Return early without running eval, since standalone annotations // cannot be evaluated as expressions. @@ -124,7 +108,7 @@ impl ReplState { }, .. } => { - pending_past_def = Some((ident.to_string(), src.to_string())); + pending_past_def = Some((ident.to_string(), line.to_string())); opt_var_name = Some(ident.to_string()); // Recreate the body of the def and then evaluate it as a lookup. @@ -132,11 +116,11 @@ impl ReplState { // 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, + ident.len() + line.len() + 1, arena, ); - buf.push_str(src); + buf.push_str(line); buf.push('\n'); buf.push_str(ident); @@ -183,13 +167,12 @@ impl ReplState { .. }) => { // Record the type for future use. - self.add_past_def(ident.trim_end().to_string(), src.to_string()); + self.add_past_def(ident.trim_end().to_string(), line.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