diff options
author | Kevin (Kun) "Kassimo" Qian <kevinkassimo@gmail.com> | 2019-02-11 11:01:28 -0800 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2019-02-11 14:01:28 -0500 |
commit | 489c69f8e1c15c75461d24d91aa3e9c2a756e807 (patch) | |
tree | 7d1d68516742c75e7ac576662774e7718c55cecc | |
parent | d26655371b796cf5dad762d1b7154c25251cb41d (diff) |
REPL multiline support with recoverable errors (#1731)
-rw-r--r-- | js/repl.ts | 122 | ||||
-rw-r--r-- | src/repl.rs | 12 | ||||
-rw-r--r-- | tools/repl_test.py | 7 |
3 files changed, 82 insertions, 59 deletions
diff --git a/js/repl.ts b/js/repl.ts index 162c6f42f..e452eb191 100644 --- a/js/repl.ts +++ b/js/repl.ts @@ -79,31 +79,74 @@ export async function replLoop(): Promise<void> { const historyFile = "deno_history.txt"; const rid = startRepl(historyFile); - let code = ""; + const quitRepl = (exitCode: number) => { + // Special handling in case user calls deno.close(3). + try { + close(rid); // close signals Drop on REPL and saves history. + } catch {} + exit(exitCode); + }; + while (true) { + let code = ""; + // Top level read try { - code = await readBlock(rid, "> ", " "); + code = await readline(rid, "> "); + if (code.trim() === "") { + continue; + } } catch (err) { if (err.message === "EOF") { - break; + quitRepl(0); + } else { + // If interrupted, don't print error. + if (err.message !== "Interrupted") { + // e.g. this happens when we have deno.close(3). + // We want to display the problem. + const formattedError = formatError( + libdeno.errorToJSON(err)); + console.error(formattedError); + } + // Quit REPL anyways. + quitRepl(1); + } + } + // Start continued read + while (!evaluate(code)) { + code += "\n"; + try { + code += await readline(rid, " "); + } catch (err) { + // If interrupted on continued read, + // abort this read instead of quitting. + if (err.message === "Interrupted") { + break; + } else if (err.message === "EOF") { + quitRepl(0); + } else { + // e.g. this happens when we have deno.close(3). + // We want to display the problem. + const formattedError = formatError( + libdeno.errorToJSON(err)); + console.error(formattedError); + quitRepl(1); + } } - console.error(err); - exit(1); } - - evaluate(code); } - - close(rid); } -function evaluate(code: string): void { - if (code.trim() === "") { - return; - } +// Evaluate code. +// Returns true if code is consumed (no error/irrecoverable error). +// Returns false if error is recoverable +function evaluate(code: string): boolean { const [result, errInfo] = libdeno.evalContext(code); if (!errInfo) { console.log(result); + } else if (errInfo.isCompileError && + isRecoverableError(errInfo.thrown)) { + // Recoverable compiler error + return false; // don't consume code. } else { if (errInfo.isNativeError) { const formattedError = formatError( @@ -114,42 +157,23 @@ function evaluate(code: string): void { console.error("Thrown:", errInfo.thrown); } } + return true; } -async function readBlock( - rid: number, - prompt: string, - continuedPrompt: string -): Promise<string> { - let code = ""; - do { - code += await readline(rid, prompt); - prompt = continuedPrompt; - } while (parenthesesAreOpen(code)); - return code; -} - -// modified from -// https://codereview.stackexchange.com/a/46039/148556 -function parenthesesAreOpen(code: string): boolean { - const parentheses = "[]{}()"; - const stack = []; - - for (const ch of code) { - const bracePosition = parentheses.indexOf(ch); - - if (bracePosition === -1) { - // not a paren - continue; - } - - if (bracePosition % 2 === 0) { - stack.push(bracePosition + 1); // push next expected brace position - } else { - if (stack.pop() !== bracePosition) { - return false; - } - } - } - return stack.length > 0; +// Error messages that allow users to continue input +// instead of throwing an error to REPL +// ref: https://github.com/v8/v8/blob/master/src/message-template.h +// TODO(kevinkassimo): this list might not be comprehensive +const recoverableErrorMessages = [ + "Unexpected end of input", // { or [ or ( + "Missing initializer in const declaration", // const a + "Missing catch or finally after try", // try {} + "missing ) after argument list", // console.log(1 + "Unterminated template literal" // `template + // TODO(kevinkassimo): need a parser to handling errors such as: + // "Missing } in template expression" // `${ or `${ a 123 }` +]; + +function isRecoverableError(e: Error): boolean { + return recoverableErrorMessages.includes(e.message); } diff --git a/src/repl.rs b/src/repl.rs index 107d8a187..55bf4a114 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,8 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. use rustyline; -use rustyline::error::ReadlineError::Interrupted; - use crate::msg::ErrorKind; use std::error::Error; @@ -10,7 +8,6 @@ use crate::deno_dir::DenoDir; use crate::errors::new as deno_error; use crate::errors::DenoResult; use std::path::PathBuf; -use std::process::exit; #[cfg(not(windows))] use rustyline::Editor; @@ -99,13 +96,8 @@ impl Repl { .map(|line| { self.editor.add_history_entry(line.as_ref()); line - }).map_err(|e| match e { - Interrupted => { - self.save_history().unwrap(); - exit(1) - } - e => deno_error(ErrorKind::Other, e.description().to_string()), - }) + }).map_err(|e| deno_error(ErrorKind::Other, e.description().to_string())) + // Forward error to TS side for processing } } diff --git a/tools/repl_test.py b/tools/repl_test.py index 2ce692b99..0958c18d0 100644 --- a/tools/repl_test.py +++ b/tools/repl_test.py @@ -84,6 +84,13 @@ class Repl(object): assertEqual(err, '') assertEqual(code, 0) + # This should print error instead of wait for input + def test_eval_unterminated(self): + out, err, code = self.input("eval('{')") + assertEqual(out, '') + assert "Unexpected end of input" in err + assertEqual(code, 0) + def test_reference_error(self): out, err, code = self.input("not_a_variable") assertEqual(out, '') |