Merge branch 'main' of github.com:roc-lang/roc into remove-editor

This commit is contained in:
Anton-4 2023-09-11 17:38:32 +02:00
commit 38bd84d603
No known key found for this signature in database
GPG Key ID: 0971D718C0A9B937
32 changed files with 814 additions and 542 deletions

View File

@ -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

28
Cargo.lock generated
View File

@ -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",

View File

@ -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",

View File

@ -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.

View File

@ -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"]

View File

@ -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 =

View File

@ -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;

View File

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

View File

@ -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<Item = &'a str>>(
defs: I,
src: &str,
target: Triple,
pub fn eval_llvm(
mut loaded: MonomorphizedModule<'_>,
target: &Triple,
opt_level: OptLevel,
) -> (Option<ReplOutput>, Problems) {
) -> Option<ReplOutput> {
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<Item = &'a str>>(
DebugPrint::NOTHING,
);
let (_, main_fn_layout) = match loaded.procedures.keys().find(|(s, _)| *s == main_fn_symbol) {
Some(layout) => *layout,
None => {
let empty_vec: Vec<String> = 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<Item = &'a str>>(
);
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))
}

View File

@ -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";

View File

@ -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 {
// <RUN WITH:> RUST_LOG=rustyline=debug cargo run repl 2> debug.log
print!("{WELCOME_MESSAGE}{SHORT_INSTRUCTIONS}");
let mut editor = Editor::<ReplState>::new();
let repl_helper = ReplState::new();
let mut editor = Editor::<ReplHelper>::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<MonomorphizedModule<'_>>,
problems: Problems,
opt_var_name: Option<String>,
target: &Triple,
dimensions: Option<(usize, usize)>,
) -> String {
let opt_output = opt_mono.and_then(|mono| eval_llvm(mono, target, OptLevel::Normal));
format_output(
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<ValidationResult> {
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<validate::ValidationResult> {
self.validator.validate(ctx)
}
fn validate_while_typing(&self) -> bool {
self.validator.validate_while_typing()
}
}

View File

@ -50,7 +50,6 @@ pub fn compile_to_mono<'a, 'i, I: Iterator<Item = &'i str>>(
defs: I,
expr: &str,
target_info: TargetInfo,
function_kind: FunctionKind,
palette: Palette,
) -> (Option<MonomorphizedModule<'a>>, Problems) {
let filename = PathBuf::from("");
@ -64,7 +63,7 @@ pub fn compile_to_mono<'a, 'i, I: Iterator<Item = &'i str>>(
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,

View File

@ -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"]

View File

@ -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 = '─';

View File

@ -1,5 +1,12 @@
use bumpalo::Bump;
use indoc::indoc;
use roc_repl_cli::repl_state::{is_incomplete, ReplState, TIPS};
use roc_repl_cli::{evaluate, ReplHelper};
use roc_repl_ui::is_incomplete;
use roc_repl_ui::repl_state::{ReplAction, ReplState};
use roc_reporting::report::DEFAULT_PALETTE;
use roc_target::TargetInfo;
use rustyline::Editor;
use target_lexicon::Triple;
// These are tests of the REPL state machine. They work without actually
// running the CLI, and without using rustyline, and instead verify
@ -7,27 +14,27 @@ use roc_repl_cli::repl_state::{is_incomplete, ReplState, TIPS};
#[test]
fn one_plus_one() {
complete("1 + 1", &mut ReplState::new(), Ok(("2 : Num *", "val1")));
complete("1 + 1", &mut ReplState::new(), "2 : Num *", "val1");
}
#[test]
fn generated_expr_names() {
let mut state = ReplState::new();
complete("2 * 3", &mut state, Ok(("6 : Num *", "val1")));
complete("4 - 1", &mut state, Ok(("3 : Num *", "val2")));
complete("val1 + val2", &mut state, Ok(("9 : Num *", "val3")));
complete("1 + (val2 * val3)", &mut state, Ok(("28 : Num *", "val4")));
complete("2 * 3", &mut state, "6 : Num *", "val1");
complete("4 - 1", &mut state, "3 : Num *", "val2");
complete("val1 + val2", &mut state, "9 : Num *", "val3");
complete("1 + (val2 * val3)", &mut state, "28 : Num *", "val4");
}
#[test]
fn persisted_defs() {
let mut state = ReplState::new();
complete("x = 5", &mut state, Ok(("5 : Num *", "x")));
complete("7 - 3", &mut state, Ok(("4 : Num *", "val1")));
complete("y = 6", &mut state, Ok(("6 : Num *", "y")));
complete("val1 + x + y", &mut state, Ok(("15 : Num *", "val2")));
complete("x = 5", &mut state, "5 : Num *", "x");
complete("7 - 3", &mut state, "4 : Num *", "val1");
complete("y = 6", &mut state, "6 : Num *", "y");
complete("val1 + x + y", &mut state, "15 : Num *", "val2");
}
#[test]
@ -38,7 +45,7 @@ fn annotated_body() {
input.push_str("t = A");
complete(&input, &mut ReplState::new(), Ok(("A : [A, B, C]", "t")));
complete(&input, &mut ReplState::new(), "A : [A, B, C]", "t");
}
#[test]
@ -53,7 +60,7 @@ fn exhaustiveness_problem() {
input.push_str("t = A");
complete(&input, &mut state, Ok(("A : [A, B, C]", "t")));
complete(&input, &mut state, "A : [A, B, C]", "t");
}
// Run a `when` on it that isn't exhaustive
@ -88,7 +95,11 @@ fn exhaustiveness_problem() {
#[test]
fn tips() {
assert!(!is_incomplete(""));
assert_eq!(ReplState::new().step("", None), Ok(TIPS.to_string()));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = ReplState::default().step(&arena, "", target_info, DEFAULT_PALETTE);
assert!(matches!(action, ReplAction::Help));
}
#[test]
@ -98,35 +109,49 @@ fn standalone_annotation() {
incomplete(&mut input);
assert!(!is_incomplete(&input));
assert_eq!(state.step(&input, None), Ok(String::new()));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = state.step(&arena, &input, target_info, DEFAULT_PALETTE);
assert!(matches!(action, ReplAction::Nothing));
}
/// validate and step the given input, then check the Result vs the output
/// with ANSI escape codes stripped.
fn complete(input: &str, state: &mut ReplState, expected_step_result: Result<(&str, &str), i32>) {
fn complete(input: &str, state: &mut ReplState, expected_start: &str, expected_end: &str) {
assert!(!is_incomplete(input));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = state.step(&arena, input, target_info, DEFAULT_PALETTE);
let repl_helper = ReplHelper::default();
let mut editor = Editor::<ReplHelper>::new();
editor.set_helper(Some(repl_helper));
let dimensions = editor.dimensions();
match state.step(input, None) {
Ok(string) => {
match action {
ReplAction::Eval {
opt_mono,
problems,
opt_var_name,
} => {
let string = evaluate(opt_mono, problems, opt_var_name, &target, dimensions);
let escaped =
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap())
.unwrap();
let comment_index = escaped.rfind('#').unwrap_or(escaped.len());
assert_eq!(
expected_step_result.map(|(starts_with, _)| starts_with),
Ok(escaped[0..comment_index].trim())
);
assert_eq!(expected_start, (escaped[0..comment_index].trim()));
assert_eq!(
expected_step_result.map(|(_, ends_with)| ends_with),
expected_end,
// +1 because we want to skip over the '#' itself
Ok(escaped[comment_index + 1..].trim())
(escaped[comment_index + 1..].trim())
);
}
Err(err) => {
assert_eq!(expected_step_result, Err(err));
_ => {
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::<ReplHelper>::new();
editor.set_helper(Some(repl_helper));
let dimensions = editor.dimensions();
let escaped = state.step(input, None).map(|string| {
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap()).unwrap()
});
assert_eq!(Ok(expected_step_result), escaped);
match action {
ReplAction::Eval {
opt_mono,
problems,
opt_var_name,
} => {
let string = evaluate(opt_mono, problems, opt_var_name, &target, dimensions);
let escaped =
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap())
.unwrap();
assert_eq!(expected_step_result, escaped);
}
_ => {
panic!("Unexpected action: {:?}", action);
}
}
}

View File

@ -122,7 +122,7 @@ impl<'a> ImportDispatcher for CompilerDispatcher<'a> {
}
}
fn run(src: &'static str) -> Result<String, String> {
fn run(src: &'static str) -> String {
let arena = Bump::new();
let mut instance = {
@ -140,27 +140,33 @@ fn run(src: &'static str) -> Result<String, String> {
};
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("<span class='color-magenta'> : </span>", " : ")
.replace("<span class='color-green'> # val1</span>", "");
// 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);
}

View File

@ -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 $@

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

@ -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"

View File

@ -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;

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

@ -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<ReplOutput>,
problems: Problems,
opt_var_name: Option<String>,
dimensions: Option<(usize, usize)>,
) -> String {
let mut buf = String::new();
for message in problems.errors.iter().chain(problems.warnings.iter()) {
if !buf.is_empty() {
buf.push_str("\n\n");
}
buf.push('\n');
buf.push_str(message);
buf.push('\n');
}
if let Some(ReplOutput { expr, expr_type }) = opt_output {
// If expr was empty, it was a type annotation or ability declaration;
// don't print anything!
//
// Also, for now we also don't print anything if there was a compile-time error.
// In the future, it would be great to run anyway and print useful output here!
if !expr.is_empty() && problems.errors.is_empty() {
const EXPR_TYPE_SEPARATOR: &str = " : "; // e.g. in "5 : Num *"
// Print the expr and its type
{
buf.push('\n');
buf.push_str(&expr);
buf.push_str(style_codes.magenta); // Color for the type separator
buf.push_str(EXPR_TYPE_SEPARATOR);
buf.push_str(style_codes.reset);
buf.push_str(&expr_type);
}
// Print var_name right-aligned on the last line of output.
if let Some(var_name) = opt_var_name {
use unicode_segmentation::UnicodeSegmentation;
const VAR_NAME_PREFIX: &str = " # "; // e.g. in " # val1"
const VAR_NAME_COLUMN_MAX: usize = 32; // Right-align the var_name at this column
let term_width = match dimensions {
Some((width, _)) => width.min(VAR_NAME_COLUMN_MAX),
None => VAR_NAME_COLUMN_MAX,
};
let expr_with_type = format!("{expr}{EXPR_TYPE_SEPARATOR}{expr_type}");
// Count graphemes because we care about what's *rendered* in the terminal
let last_line_len = expr_with_type
.split('\n')
.last()
.unwrap_or_default()
.graphemes(true)
.count();
let var_name_len =
var_name.graphemes(true).count() + VAR_NAME_PREFIX.graphemes(true).count();
let spaces_needed = if last_line_len + var_name_len > term_width {
buf.push('\n');
term_width - var_name_len
} else {
term_width - last_line_len - var_name_len
};
for _ in 0..spaces_needed {
buf.push(' ');
}
buf.push_str(style_codes.green);
buf.push_str(VAR_NAME_PREFIX);
buf.push_str(&var_name);
buf.push_str(style_codes.reset);
buf.push('\n');
}
}
}
buf
}

View File

@ -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<PastDef>,
past_def_idents: MutSet<String>,
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<MonomorphizedModule<'a>>,
problems: Problems,
opt_var_name: Option<String>,
},
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<String, i32> {
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<ValidationResult> {
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<validate::ValidationResult> {
self.validator.validate(ctx)
}
fn validate_while_typing(&self) -> bool {
self.validator.validate_while_typing()
}
}
fn format_output(
opt_output: Option<ReplOutput>,
problems: Problems,
opt_var_name: Option<String>,
dimensions: Option<(usize, usize)>,
) -> String {
let mut buf = String::new();
for message in problems.errors.iter().chain(problems.warnings.iter()) {
if !buf.is_empty() {
buf.push_str("\n\n");
}
buf.push('\n');
buf.push_str(message);
buf.push('\n');
}
if let Some(ReplOutput { expr, expr_type }) = opt_output {
// If expr was empty, it was a type annotation or ability declaration;
// don't print anything!
//
// Also, for now we also don't print anything if there was a compile-time error.
// In the future, it would be great to run anyway and print useful output here!
if !expr.is_empty() && problems.errors.is_empty() {
const EXPR_TYPE_SEPARATOR: &str = " : "; // e.g. in "5 : Num *"
// Print the expr and its type
{
buf.push('\n');
buf.push_str(&expr);
buf.push_str(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
}

View File

@ -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" }

View File

@ -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 <http://127.0.0.1:8000/repl> (or whatever port your web server mentioned when it started up.)

View File

@ -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([

View File

@ -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<String, String> {
pub async fn entrypoint_from_js(src: String) -> String {
crate::repl::entrypoint_from_js(src).await
}

View File

@ -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
}

View File

@ -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<ReplState> = 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<String, String> {
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<ReplOutput> {
let MonomorphizedModule {
module_id,
procedures,
@ -225,7 +245,7 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
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<String, String> {
DebugPrint::NOTHING,
);
let (_, main_fn_layout) = match procedures.keys().find(|(s, _)| *s == main_fn_symbol) {
Some(layout) => *layout,
None => return Ok(format!("<function> : {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<String, String> {
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!("<span class='color-red'>{js_exception:?}</span>"),
expr_type: String::new(),
})
}
}
let mut app = WasmReplApp { arena };
@ -300,11 +323,8 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
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 })
}

View File

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

71
devtools/signing.md Normal file
View File

@ -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

View File

@ -33,7 +33,6 @@
></textarea>
</section>
</div>
<p style="margin-top: 20px;">⚠️ 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.</p>
<script type="module" src="/repl/repl.js"></script>
</body>
</html>

View File

@ -6,7 +6,7 @@ body {
background-color: #222;
color: #ccc;
font-family: sans-serif;
font-size: 18px;
font-size: 12px;
}
.body-wrapper {
display: flex;

View File

@ -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;