diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96963fed77..2162635633 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](devtools/signing.md). #### Commit signing on NixOS diff --git a/Cargo.lock b/Cargo.lock index a9da27e797..6785a19ed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2117,9 +2117,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]] @@ -2814,11 +2819,11 @@ dependencies = [ "roc_error_macros", "roc_gen_llvm", "roc_load", - "roc_module", "roc_mono", "roc_parse", "roc_region", "roc_repl_eval", + "roc_repl_ui", "roc_reporting", "roc_std", "roc_target", @@ -2885,6 +2890,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" @@ -2901,6 +2922,7 @@ dependencies = [ "roc_load", "roc_parse", "roc_repl_eval", + "roc_repl_ui", "roc_reporting", "roc_solve", "roc_target", @@ -4318,9 +4340,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", diff --git a/Cargo.toml b/Cargo.toml index cc46326f11..177bbe42e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,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 319bac9dec..7cbb4058b9 100644 --- a/crates/README.md +++ b/crates/README.md @@ -92,6 +92,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 6424b0e247..219c7a48b1 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -26,7 +26,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/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 = 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; diff --git a/crates/repl_cli/Cargo.toml b/crates/repl_cli/Cargo.toml index 147b887b70..8b0bfe91e2 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" } @@ -31,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/cli_gen.rs b/crates/repl_cli/src/cli_gen.rs index 0b8c322ca7..2d900f7828 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( + mut loaded: MonomorphizedModule<'_>, + 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,15 +47,10 @@ 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(); @@ -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); @@ -287,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/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 2b70075071..a3f7a89a99 100644 --- a/crates/repl_cli/src/lib.rs +++ b/crates/repl_cli/src/lib.rs @@ -1,29 +1,30 @@ //! Command Line Interface (CLI) functionality for the Read-Evaluate-Print-Loop (REPL). 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 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::{ + 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}; +use rustyline_derive::{Completer, Helper, Hinter}; +use target_lexicon::Triple; -pub const WELCOME_MESSAGE: &str = concatcp!( - "\n The rockin’ ", - BLUE, - "roc repl", - END_COL, - "\n", - PINK, - "────────────────────────", - END_COL, - "\n\n" -); +use crate::cli_gen::eval_llvm; -// 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"; +#[derive(Completer, Helper, Hinter, Default)] +pub struct ReplHelper { + validator: InputValidator, + state: ReplState, +} pub fn main() -> i32 { use rustyline::error::ReadlineError; @@ -34,9 +35,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 +48,34 @@ 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; - match repl_helper.step(&line, dimensions) { - Ok(output) => { + arena.reset(); + match repl_state.step(&arena, &line, target_info, DEFAULT_PALETTE) { + ReplAction::Eval { + opt_mono, + problems, + opt_var_name, + } => { + 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}"); } } - Err(exit_code) => return exit_code, - }; + ReplAction::Exit => { + return 0; + } + ReplAction::Help => { + println!("{TIPS}"); + } + ReplAction::Nothing => {} + } } #[cfg(windows)] Err(ReadlineError::WindowResize) => { @@ -76,3 +96,64 @@ pub fn main() -> i32 { } } } + +pub fn evaluate( + 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( + ANSI_STYLE_CODES, + opt_output, + problems, + opt_var_name, + dimensions, + ) +} + +#[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)) + } + } +} + +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() + } +} 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, diff --git a/crates/repl_test/Cargo.toml b/crates/repl_test/Cargo.toml index 4858f695a9..be2755ff1d 100644 --- a/crates/repl_test/Cargo.toml +++ b/crates/repl_test/Cargo.toml @@ -13,18 +13,23 @@ 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"] 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_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..04c45faeec 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)); + _ => { + panic!("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); + } + _ => { + panic!("Unexpected action: {:?}", action); + } + } } diff --git a/crates/repl_test/src/wasm.rs b/crates/repl_test/src/wasm.rs index 80c664863a..ab39f06792 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,33 @@ fn run(src: &'static str) -> Result { }; let len = Value::I32(src.len() as i32); - let wasm_ok: i32 = 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) - } + 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), Ok(expected.into())); + expect(input, expected); } #[allow(dead_code)] pub fn expect_failure(input: &'static str, expected: &str) { - assert_eq!(run(input), Err(expected.into())); + 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 $@ diff --git a/crates/repl_ui/Cargo.toml b/crates/repl_ui/Cargo.toml new file mode 100644 index 0000000000..bc5fec60ec --- /dev/null +++ b/crates/repl_ui/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "roc_repl_ui" +description = "UI 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..0d35b9daf4 --- /dev/null +++ b/crates/repl_ui/src/colors.rs @@ -0,0 +1,12 @@ +use roc_reporting::report::{StyleCodes, ANSI_STYLE_CODES, HTML_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; +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..d4ba4d8bb4 --- /dev/null +++ b/crates/repl_ui/src/lib.rs @@ -0,0 +1,202 @@ +//! 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; + +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" +); + +// 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", + if cfg!(target_family = "wasm") { + // In the web REPL, the :quit command doesn't make sense. Just close the browser tab! + // We use Shift-Enter for newlines because it's nicer than our workaround for Unix terminals (see below) + concatcp!( + BLUE, + " - ", + END_COL, + PINK, + "Shift-Enter", + END_COL, + " or ", + 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 Shift-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 +// 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_cli/src/repl_state.rs b/crates/repl_ui/src/repl_state.rs similarity index 64% rename from crates/repl_cli/src/repl_state.rs rename to crates/repl_ui/src/repl_state.rs index aa640bb2d8..5808d2c988 100644 --- a/crates/repl_cli/src/repl_state.rs +++ b/crates/repl_ui/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,39 +34,40 @@ impl Default for ReplState { } } +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +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(); - - match parse_src(&arena, line) { - ParseOutcome::Empty => Ok(TIPS.to_string()), - 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), - } - } - - pub fn eval_and_format(&mut self, src: &str, dimensions: Option<(usize, usize)>) -> String { - let arena = Bump::new(); + pub fn step<'a>( + &mut self, + arena: &'a Bump, + line: &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 @@ -122,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 { @@ -134,11 +87,11 @@ 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 - // cannnot be evaluated as expressions. - return String::new(); + // cannot be evaluated as expressions. + return ReplAction::Nothing; } ValueDef::Body( Loc { @@ -155,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. @@ -163,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, - &arena, + ident.len() + line.len() + 1, + arena, ); - buf.push_str(src); + buf.push_str(line); buf.push('\n'); buf.push_str(ident); @@ -214,36 +167,37 @@ 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 String::new(); + 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 (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 +220,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 +242,7 @@ impl ReplState { } #[derive(Debug, PartialEq)] -enum ParseOutcome<'a> { +pub enum ParseOutcome<'a> { ValueDef(ValueDef<'a>), TypeDef(TypeDef<'a>), Expr(Expr<'a>), @@ -295,7 +253,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 +414,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_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/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.) 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/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/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 } diff --git a/crates/repl_wasm/src/repl.rs b/crates/repl_wasm/src/repl.rs index 5aa5c29766..636df8b0da 100644 --- a/crates/repl_wasm/src/repl.rs +++ b/crates/repl_wasm/src/repl.rs @@ -1,5 +1,6 @@ use bumpalo::{collections::vec::Vec, Bump}; -use std::mem::size_of; +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; @@ -7,10 +8,14 @@ use roc_load::MonomorphizedModule; use roc_parse::ast::Expr; use roc_repl_eval::{ eval::jit_to_ast, - gen::{compile_to_mono, format_answer}, + gen::{format_answer, ReplOutput}, ReplApp, ReplAppMemory, }; -use roc_reporting::report::DEFAULT_PALETTE_HTML; +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}; @@ -18,6 +23,12 @@ 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()); +} + pub struct WasmReplApp<'a> { arena: &'a Bump, } @@ -164,51 +175,60 @@ 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 &[_]; + include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.wasm")) as &[_]; -#[cfg(windows)] -const PRE_LINKED_BINARY: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/pre_linked_binary.obj")) as &[_]; - -pub async fn entrypoint_from_js(src: String) -> Result { +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(); + // TODO: make this a global and reset it? let arena = &Bump::new(); // 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( - arena, - 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 - (_, 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"); - } + // 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) + }); - buf.push_str(message); - } - - return Err(buf); + // Perform the action the state machine asked for, and return the appropriate output string + match action { + ReplAction::Help => TIPS.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, + 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, @@ -225,7 +245,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, @@ -233,10 +253,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 { @@ -279,10 +296,16 @@ pub async fn entrypoint_from_js(src: String) -> Result { buffer }; - // 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:?}"))?; + // 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_type: String::new(), + }) + } + } let mut app = WasmReplApp { arena }; @@ -300,11 +323,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 }) } 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, diff --git a/devtools/signing.md b/devtools/signing.md new file mode 100644 index 0000000000..16eb465422 --- /dev/null +++ b/devtools/signing.md @@ -0,0 +1,71 @@ +# Commit Signing Guide + +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 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.asc +``` + +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 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/). + +Insert your Yubikey and check if it is mentioned in the output of `gpg --card-status` (powershell). + +Open powershell 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 + + 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.

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; diff --git a/www/public/repl/repl.js b/www/public/repl/repl.js index 91248858f7..0666c1549e 100644 --- a/www/public/repl/repl.js +++ b/www/public/repl/repl.js @@ -45,8 +45,17 @@ 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; + + // 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(); }); // ---------------------------------------------------------------------------- @@ -75,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; } @@ -86,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 { @@ -95,7 +110,7 @@ function onInputKeyup(event) { break; case ENTER: - if (!event.shiftKey) { + if (!event.shiftKey && !event.ctrlKey && !event.altKey) { onInputChange({ target: repl.elemSourceInput }); } break;