From c46384d7f5c619fefe55d5985f7f3e632b5dca6a Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Tue, 19 Dec 2017 17:23:17 +0100 Subject: [PATCH 1/9] introducing the suspension api --- src/cljs/snapshot/lumo/repl.cljs | 133 +++++++++++++++++++------------ src/js/repl.js | 15 +++- 2 files changed, 96 insertions(+), 52 deletions(-) diff --git a/src/cljs/snapshot/lumo/repl.cljs b/src/cljs/snapshot/lumo/repl.cljs index 6b7e30e8..e4388e82 100644 --- a/src/cljs/snapshot/lumo/repl.cljs +++ b/src/cljs/snapshot/lumo/repl.cljs @@ -646,6 +646,23 @@ "Wrap wfn around all (fn) values in fns hashmap." (into {} (for [[k v] fns] [k (wfn v)]))) +;; -------------------- +;; REPL upgrade + +(deftype ^:private SuspensionRequest [f]) + +(defn suspension-request? [x] (instance? SuspensionRequest x)) + +(defn suspension-request [f] (SuspensionRequest. f)) + +(defprotocol AsyncReader + "Asynchronous stream of strings." + (read-chars [r f] "Calls f with a string or nil (EOF)") + (pushback [r s] "Unread s")) + +(defn yield-control [suspension-request async-reader resume-cb] + ((.-f suspension-request) async-reader resume-cb)) + (declare execute-path) (def ^:private repl-special-fns @@ -917,7 +934,7 @@ (let [{:keys [ex-kind]} (ex-data e)] (keyword-identical? ex-kind :eof))) -(defn- read-chars +(defn- read-all-chars [reader] (let [sb (StringBuffer.)] (loop [c (rt/read-char reader)] @@ -982,7 +999,7 @@ r/*data-readers* (merge tags/*cljs-data-readers* (load-data-readers! env/*compiler*)) r/resolve-symbol ana/resolve-symbol r/*alias-map* (current-alias-map)] - [(r/read {:read-cond :allow :features #{:cljs}} reader) (read-chars reader)]))) + [(r/read {:read-cond :allow :features #{:cljs}} reader) (read-all-chars reader)]))) (defn- ns-for-source [source] (let [[ns-form] (repl-read-string source) @@ -1113,53 +1130,71 @@ (handle-error (ex-info (str "Could not load file " file) {}) true))))) (defn- execute-text - [source {:keys [expression? print-nil-result? filename session-id] :as opts}] - (try - (set-session-state-for-session-id! session-id) - (binding [ana/*cljs-warning-handlers* (if expression? - [warning-handler] - [ana/default-warning-handler]) - cljs/*eval-fn* caching-node-eval - cljs/*load-fn* load - ana/*cljs-ns* @current-ns - *ns* (create-ns @current-ns) - env/*compiler* st - r/resolve-symbol ana/resolve-symbol - tags/*cljs-data-readers* (merge tags/*cljs-data-readers* (load-data-readers! env/*compiler*)) - r/*alias-map* (current-alias-map)] - (let [form (and expression? (first (repl-read-string source))) - eval-opts (merge (make-eval-opts) - (when expression? - {:context :expr - :def-emits-var true}))] - (if (repl-special? form) - ((get repl-special-fns (first form)) form (merge opts eval-opts)) - (cljs/eval-str - st - source - (cond - expression? source - filename (or (ns-for-source source) filename) - :else "source") - eval-opts - (fn [{:keys [ns value error] :as ret}] - (if-not error - (when expression? - (when (or (true? print-nil-result?) - (not (nil? value))) - (js/$$LUMO_GLOBALS.doPrint print-value value)) - (process-1-2-3 form value) - (when (def-form? form) - (let [{:keys [ns name]} (meta value)] - (swap! st assoc-in [::ana/namespaces ns :defs name ::repl-entered-source] source))) - (vreset! current-ns ns)) - (handle-error error true))))))) - (catch :default e - ;; `;;` and `#_` - (when-not (identical? (.-message e) "Unexpected EOF.") - (handle-error e true))) - (finally (capture-session-state-for-session-id session-id))) - nil) + [source {:keys [expression? print-nil-result? filename session-id done-cb] :as opts}] + (let [suspended (volatile! false)] + (try + (set-session-state-for-session-id! session-id) + (binding [ana/*cljs-warning-handlers* (if expression? + [warning-handler] + [ana/default-warning-handler]) + cljs/*eval-fn* caching-node-eval + cljs/*load-fn* load + ana/*cljs-ns* @current-ns + *ns* (create-ns @current-ns) + env/*compiler* st + r/resolve-symbol ana/resolve-symbol + tags/*cljs-data-readers* (merge tags/*cljs-data-readers* (load-data-readers! env/*compiler*)) + r/*alias-map* (current-alias-map)] + (let [form (and expression? (first (repl-read-string source))) + eval-opts (merge (make-eval-opts) + (when expression? + {:context :expr + :def-emits-var true}))] + (if (repl-special? form) + ((get repl-special-fns (first form)) form (merge opts eval-opts)) + (cljs/eval-str + st + source + (cond + expression? source + filename (or (ns-for-source source) filename) + :else "source") + eval-opts + (fn eval-cb [{:keys [ns value error] :as ret}] + (when @suspended + (set-session-state-for-session-id! session-id)) + (if (and expression? (suspension-request? value)) + (if done-cb + (let [async-reader 'TODO] + (vreset! suspended true) + (capture-session-state-for-session-id session-id) + (yield-control value async-reader eval-cb)) + (throw (js/Error. "This REPL can't be upgraded."))) + (try + (if-not error + (when expression? + (when (or (true? print-nil-result?) + (not (nil? value))) + (js/$$LUMO_GLOBALS.doPrint print-value value)) + (process-1-2-3 form value) + (when (def-form? form) + (let [{:keys [ns name]} (meta value)] + (swap! st assoc-in [::ana/namespaces ns :defs name ::repl-entered-source] source))) + (vreset! current-ns ns)) + (handle-error error true)) + (finally + (when @suspended + (capture-session-state-for-session-id session-id) + (done-cb)))))))))) + (catch :default e + ;; `;;` and `#_` + (when-not (identical? (.-message e) "Unexpected EOF.") + (handle-error e true))) + (finally + (when-not @suspended + (capture-session-state-for-session-id session-id) + (when done-cb (done-cb))))) + nil)) (defn- execute-source [source-or-path {:keys [type] :as opts}] diff --git a/src/js/repl.js b/src/js/repl.js index 6827f6cf..d7370dd6 100644 --- a/src/js/repl.js +++ b/src/js/repl.js @@ -86,7 +86,8 @@ export function processLine(replSession: REPLSession, line: string): void { const session = replSession; const { input, rl, isMain } = session; - let extraForms; + let extraForms, done; // done is either a boolean or a function + const donecb = () => { if (done) done(); else done = true; } if (exitCommands.has(line.trim())) { // $FlowIssue - use of rl.output @@ -117,8 +118,16 @@ export function processLine(replSession: REPLSession, line: string): void { cljs.setPrintFns(rl.output); currentREPLInterface = rl; - cljs.execute(session.input, 'text', true, true, session.id); - + done = false; +// cljs.execute(session.input, 'text', true, true, session.id, donecb); + donecb(); // tmp + cljs.execute(session.input, 'text', true, true, session.id); + if (!done) { + // donecb hasn't been called, user code is in control + done = () => processLine(session, extraForms); + break; + } + currentREPLInterface = null; cljs.setPrintFns(); // If *print-newline* is off, we need to emit a newline now, otherwise From b48a94e44812326d62f8db721ab3ca07ea84579d Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Wed, 20 Dec 2017 16:08:24 +0100 Subject: [PATCH 2/9] upgrade works! --- src/cljs/snapshot/lumo/repl.cljs | 79 ++++++++++++++++++++++++-------- src/js/cljs.js | 6 +++ src/js/repl.js | 32 +++++++++---- 3 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/cljs/snapshot/lumo/repl.cljs b/src/cljs/snapshot/lumo/repl.cljs index e4388e82..6e8d4593 100644 --- a/src/cljs/snapshot/lumo/repl.cljs +++ b/src/cljs/snapshot/lumo/repl.cljs @@ -663,6 +663,35 @@ (defn yield-control [suspension-request async-reader resume-cb] ((.-f suspension-request) async-reader resume-cb)) +(defn- create-async-pipe [] + (let [front #js [] + back #js [] + cb (volatile! nil) + spill! #(loop [] + (when-some [s (.pop back)] + (do (.push front s) (recur))))] + #js [(fn + ([] (spill!) (.join front "")) + ([s] + (when (and s (not= "" s)) ; TODO handle EOF + (if-some [f @cb] + (do (vreset! cb nil) (f s)) + (if (pos? (.-length front)) + (.push back s) + (.push front s)))))) + (reify AsyncReader + (read-chars [r f] + (if-some [s (.pop front)] + (f s) + (do + (spill!) + (if-some [s (.pop front)] + (f s) + (vreset! cb f))))) + (pushback [r s] + (when (and s (not= "" s)) + (.push front s))))])) + (declare execute-path) (def ^:private repl-special-fns @@ -1130,7 +1159,7 @@ (handle-error (ex-info (str "Could not load file " file) {}) true))))) (defn- execute-text - [source {:keys [expression? print-nil-result? filename session-id done-cb] :as opts}] + [source {:keys [expression? print-nil-result? filename session-id host-yield-control] :as opts}] (let [suspended (volatile! false)] (try (set-session-state-for-session-id! session-id) @@ -1164,13 +1193,26 @@ (when @suspended (set-session-state-for-session-id! session-id)) (if (and expression? (suspension-request? value)) - (if done-cb - (let [async-reader 'TODO] - (vreset! suspended true) + (if host-yield-control + (if-let [re-yield @suspended] + (re-yield value) + (do (capture-session-state-for-session-id session-id) - (yield-control value async-reader eval-cb)) - (throw (js/Error. "This REPL can't be upgraded."))) + ; host-yield-control is the function for readline yielding control + ; this could be avoided by using .once and .pause but readline seems to have + ; issues with pauses, see https://github.com/nodejs/node-v0.x-archive/issues/8340 + (host-yield-control + (fn [async-reader done-cb] + (let [resume #(try + (eval-cb %) + (finally + ; eval-cb may have resuspended (see re-yield above) + (when-not @suspended (done-cb))))] + (vreset! suspended #(yield-control % async-reader resume)) + (yield-control value async-reader resume)))))) + (throw (js/Error. "This REPL can't be upgraded."))) (try + (vreset! suspended false) (if-not error (when expression? (when (or (true? print-nil-result?) @@ -1184,16 +1226,14 @@ (handle-error error true)) (finally (when @suspended - (capture-session-state-for-session-id session-id) - (done-cb)))))))))) + (capture-session-state-for-session-id session-id)))))))))) (catch :default e ;; `;;` and `#_` (when-not (identical? (.-message e) "Unexpected EOF.") (handle-error e true))) (finally (when-not @suspended - (capture-session-state-for-session-id session-id) - (when done-cb (done-cb))))) + (capture-session-state-for-session-id session-id)))) nil)) (defn- execute-source @@ -1203,14 +1243,17 @@ (execute-text source-or-path opts))) (defn- ^:export execute - [type source-or-path expression? print-nil-result? setNS session-id] - (clear-fns!) - (when setNS - (vreset! current-ns (symbol setNS))) - (execute-source source-or-path {:type type - :expression? expression? - :print-nil-result? print-nil-result? - :session-id session-id})) + ([type source-or-path expression? print-nil-result? setNS session-id] + (execute type source-or-path expression? print-nil-result? setNS session-id nil)) + ([type source-or-path expression? print-nil-result? setNS session-id host-yield-control] + (clear-fns!) + (when setNS + (vreset! current-ns (symbol setNS))) + (execute-source source-or-path {:type type + :expression? expression? + :print-nil-result? print-nil-result? + :session-id session-id + :host-yield-control host-yield-control}))) (defn- ^:export is-readable? [form] diff --git a/src/js/cljs.js b/src/js/cljs.js index 624224fa..289d7996 100644 --- a/src/js/cljs.js +++ b/src/js/cljs.js @@ -270,6 +270,7 @@ export function execute( printNilResult?: boolean = true, sessionID?: number = 0, setNS?: string, + yieldControl: () => void, ): void { // $FlowIssue: context can have globals return ClojureScriptContext.lumo.repl.execute( @@ -279,6 +280,7 @@ export function execute( printNilResult, setNS, sessionID, + yieldControl ); } @@ -315,6 +317,10 @@ export function clearREPLSessionState(sessionID: number): void { return ClojureScriptContext.lumo.repl.clear_state_for_session(sessionID); } +export function createAsyncPipe() { + return ClojureScriptContext.lumo.repl.create_async_pipe(); +} + function executeScript( code: string, type: string, diff --git a/src/js/repl.js b/src/js/repl.js index d7370dd6..aa38467f 100644 --- a/src/js/repl.js +++ b/src/js/repl.js @@ -23,6 +23,7 @@ type KeyType = { export type REPLSession = { id: number, rl: readline$Interface, + linecb?: (s?:string) => void, isMain: boolean, isReverseSearch: boolean, reverseSearchBuffer: string, @@ -85,9 +86,22 @@ function inferPastingBehavior(replSession: REPLSession): void { export function processLine(replSession: REPLSession, line: string): void { const session = replSession; const { input, rl, isMain } = session; + + if (session.linecb) { + session.linecb(line); + return; + } - let extraForms, done; // done is either a boolean or a function - const donecb = () => { if (done) done(); else done = true; } + let extraForms, suspended; // suspended is either a boolean or a function + function yieldControl(f) { + suspended = true; + const [linecb, reader] = cljs.createAsyncPipe(); + session.linecb = linecb; + f(reader, () => { + session.linecb = null; + processLine(session, linecb()); + }); + }; if (exitCommands.has(line.trim())) { // $FlowIssue - use of rl.output @@ -118,13 +132,12 @@ export function processLine(replSession: REPLSession, line: string): void { cljs.setPrintFns(rl.output); currentREPLInterface = rl; - done = false; -// cljs.execute(session.input, 'text', true, true, session.id, donecb); - donecb(); // tmp - cljs.execute(session.input, 'text', true, true, session.id); - if (!done) { - // donecb hasn't been called, user code is in control - done = () => processLine(session, extraForms); + suspended = false; + cljs.execute(session.input, 'text', true, true, session.id, undefined, yieldControl); + if (suspended) { + // yieldControl has been called, user code is in control + session.input = ''; + session.linecb(extraForms); break; } @@ -384,6 +397,7 @@ export function createSession( id: sessionCount, rl, input: '', + linecb: null, isMain, reverseSearchBuffer: '', isReverseSearch: false, From d4ef64e17d21329303e0ef6bb16c389e024466e8 Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Wed, 20 Dec 2017 16:52:29 +0100 Subject: [PATCH 3/9] Tie print-fns to sessions and remove the global cljsSender. --- src/cljs/snapshot/lumo/repl.cljs | 13 +++++++++--- src/js/cljs.js | 34 ++++++++++++-------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/cljs/snapshot/lumo/repl.cljs b/src/cljs/snapshot/lumo/repl.cljs index 6e8d4593..cd13f08d 100644 --- a/src/cljs/snapshot/lumo/repl.cljs +++ b/src/cljs/snapshot/lumo/repl.cljs @@ -1055,7 +1055,9 @@ :*2 *2 :*3 *3 :*e *e - :ns @current-ns}) + :ns @current-ns + :*print-fn* *print-fn* + :*print-err-fn* *print-err-fn*}) (defn- set-session-state! "Sets the session state given a sesssion state map." @@ -1070,7 +1072,9 @@ (set! *2 (:*2 session-state)) (set! *3 (:*3 session-state)) (set! *e (:*e session-state)) - (vreset! current-ns (:ns session-state))) + (vreset! current-ns (:ns session-state)) + (set! *print-fn* (:*print-fn* session-state)) + (set! *print-err-fn* (:*print-err-fn* session-state))) (def ^{:private true :doc "The default state used to initialize a new REPL session."} @@ -1090,7 +1094,10 @@ (defn- set-session-state-for-session-id! "Sets the session state for a given session." [session-id] - (set-session-state! (get @session-states session-id @default-session-state))) + (set-session-state! (or (get @session-states session-id) + (assoc @default-session-state + :*print-fn* *print-fn* + :*print-err-fn* *print-err-fn*)))) (defn- capture-session-state-for-session-id "Captures the session state for a given session." diff --git a/src/js/cljs.js b/src/js/cljs.js index 289d7996..44cc60a5 100644 --- a/src/js/cljs.js +++ b/src/js/cljs.js @@ -207,35 +207,27 @@ function setRuntimeOpts(opts: CLIOptsType): void { ); } -let cljsSender: stream$Writable; - -function printFn(...args: string[]): void { - if (utilBinding.watchdogHasPendingSigint()) { - throw interruptSentinel; - } - cljsSender.write(args.join(' ')); +function mkPrintFn(cljsSender: stream$Writable) { + return (...args: string[]): void => { + if (utilBinding.watchdogHasPendingSigint()) { + throw interruptSentinel; + } + cljsSender.write(args.join(' ')); + }; } -function printErrFn(...args: string[]): void { - if (utilBinding.watchdogHasPendingSigint()) { - throw interruptSentinel; - } - - process.stderr.write(args.join(' ')); -} +const printErrFn = mkPrintFn(process.stderr); +const printOutFn = mkPrintFn(process.stdout); export function setPrintFns(stream?: stream$Writable): void { if (stream == null || stream === process.stdout) { - cljsSender = process.stdout; - // $FlowIssue: context can have globals + ClojureScriptContext.cljs.core.set_print_fn_BANG_(printOutFn); ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printErrFn); } else { - cljsSender = stream; - // $FlowIssue: context can have globals - ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printFn); + const printFn = mkPrintFn(stream) + ClojureScriptContext.cljs.core.set_print_fn_BANG_(printFn); + ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printFn); } - // $FlowIssue: context can have globals - ClojureScriptContext.cljs.core.set_print_fn_BANG_(printFn); } function initClojureScriptEngine(opts: CLIOptsType): void { From 6b10196cbbf8440fdee442b0c07a1b10c1edf727 Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Thu, 21 Dec 2017 16:35:28 +0100 Subject: [PATCH 4/9] disable prompt while suspended --- src/js/repl.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/js/repl.js b/src/js/repl.js index aa38467f..b17a1eb4 100644 --- a/src/js/repl.js +++ b/src/js/repl.js @@ -92,13 +92,16 @@ export function processLine(replSession: REPLSession, line: string): void { return; } - let extraForms, suspended; // suspended is either a boolean or a function + let extraForms, suspended; function yieldControl(f) { suspended = true; const [linecb, reader] = cljs.createAsyncPipe(); session.linecb = linecb; + const prompt = rl._prompt; + rl.setPrompt(''); f(reader, () => { session.linecb = null; + rl.setPrompt(prompt); processLine(session, linecb()); }); }; @@ -287,6 +290,8 @@ function handleKeyPress( const { rl, isReverseSearch } = session; const isReverseSearchKey = ctrl && name === 'r'; + if (session.linecb) return; + // TODO: factor this out into own function if (isReverseSearch || isReverseSearchKey) { let failedSearch = false; From 7d5cce39056909678b9da4140816e862917ebafb Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Thu, 21 Dec 2017 17:17:09 +0100 Subject: [PATCH 5/9] Handle newlines between repl and upgrade. Add newlines to line events before appending to the async reader, remove trailing readline when flushing the pushback buffer on resume. --- src/cljs/snapshot/lumo/repl.cljs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/cljs/snapshot/lumo/repl.cljs b/src/cljs/snapshot/lumo/repl.cljs index cd13f08d..888b87fa 100644 --- a/src/cljs/snapshot/lumo/repl.cljs +++ b/src/cljs/snapshot/lumo/repl.cljs @@ -671,14 +671,21 @@ (when-some [s (.pop back)] (do (.push front s) (recur))))] #js [(fn - ([] (spill!) (.join front "")) + ([] + (spill!) + (when-some [last (.pop front)] + ; remove last newline when going back to repl/readline + (let [n (dec (.-length last))] + (.push front (if (= \newline (.charAt last n)) (subs last 0 n) last)))) + (.join front "")) ([s] (when (and s (not= "" s)) ; TODO handle EOF - (if-some [f @cb] - (do (vreset! cb nil) (f s)) - (if (pos? (.-length front)) - (.push back s) - (.push front s)))))) + (let [s (str s "\n")] + (if-some [f @cb] + (do (vreset! cb nil) (f s)) + (if (pos? (.-length front)) + (.push back s) + (.push front s))))))) (reify AsyncReader (read-chars [r f] (if-some [s (.pop front)] From 4eb9aa9ac3050acfffeacc3f4263627900cb6a40 Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Thu, 21 Dec 2017 18:11:40 +0100 Subject: [PATCH 6/9] pleasing yarn lint --- src/js/cljs.js | 26 +++++++++++++------------- src/js/repl.js | 47 ++++++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/js/cljs.js b/src/js/cljs.js index 44cc60a5..06c55bea 100644 --- a/src/js/cljs.js +++ b/src/js/cljs.js @@ -207,13 +207,13 @@ function setRuntimeOpts(opts: CLIOptsType): void { ); } -function mkPrintFn(cljsSender: stream$Writable) { - return (...args: string[]): void => { - if (utilBinding.watchdogHasPendingSigint()) { - throw interruptSentinel; - } - cljsSender.write(args.join(' ')); - }; +function mkPrintFn(cljsSender: stream$Writable): () => void { + return (...args: string[]): void => { + if (utilBinding.watchdogHasPendingSigint()) { + throw interruptSentinel; + } + cljsSender.write(args.join(' ')); + }; } const printErrFn = mkPrintFn(process.stderr); @@ -224,9 +224,9 @@ export function setPrintFns(stream?: stream$Writable): void { ClojureScriptContext.cljs.core.set_print_fn_BANG_(printOutFn); ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printErrFn); } else { - const printFn = mkPrintFn(stream) - ClojureScriptContext.cljs.core.set_print_fn_BANG_(printFn); - ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printFn); + const printFn = mkPrintFn(stream); + ClojureScriptContext.cljs.core.set_print_fn_BANG_(printFn); + ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printFn); } } @@ -272,7 +272,7 @@ export function execute( printNilResult, setNS, sessionID, - yieldControl + yieldControl, ); } @@ -309,8 +309,8 @@ export function clearREPLSessionState(sessionID: number): void { return ClojureScriptContext.lumo.repl.clear_state_for_session(sessionID); } -export function createAsyncPipe() { - return ClojureScriptContext.lumo.repl.create_async_pipe(); +export function createAsyncPipe(): array { + return ClojureScriptContext.lumo.repl.create_async_pipe(); } function executeScript( diff --git a/src/js/repl.js b/src/js/repl.js index b17a1eb4..a62c8160 100644 --- a/src/js/repl.js +++ b/src/js/repl.js @@ -23,7 +23,7 @@ type KeyType = { export type REPLSession = { id: number, rl: readline$Interface, - linecb?: (s?:string) => void, + linecb?: (s?: string) => void, isMain: boolean, isReverseSearch: boolean, reverseSearchBuffer: string, @@ -86,25 +86,25 @@ function inferPastingBehavior(replSession: REPLSession): void { export function processLine(replSession: REPLSession, line: string): void { const session = replSession; const { input, rl, isMain } = session; - + if (session.linecb) { - session.linecb(line); - return; + session.linecb(line); + return undefined; } - let extraForms, suspended; - function yieldControl(f) { - suspended = true; - const [linecb, reader] = cljs.createAsyncPipe(); - session.linecb = linecb; - const prompt = rl._prompt; - rl.setPrompt(''); - f(reader, () => { - session.linecb = null; - rl.setPrompt(prompt); - processLine(session, linecb()); - }); - }; + let suspended; + function yieldControl(f: (async_reader: object, resume_cb: ()=>void)=>void): void { + suspended = true; + const [linecb, reader] = cljs.createAsyncPipe(); + session.linecb = linecb; + const bakprompt = rl._prompt; + rl.setPrompt(''); + f(reader, () => { + session.linecb = null; + rl.setPrompt(bakprompt); + processLine(session, linecb()); + }); + } if (exitCommands.has(line.trim())) { // $FlowIssue - use of rl.output @@ -117,6 +117,7 @@ export function processLine(replSession: REPLSession, line: string): void { session.input = `${input}\n${line}`; } + let extraForms; for (;;) { const currentInput = session.input; extraForms = cljs.isReadable(currentInput); @@ -138,12 +139,12 @@ export function processLine(replSession: REPLSession, line: string): void { suspended = false; cljs.execute(session.input, 'text', true, true, session.id, undefined, yieldControl); if (suspended) { - // yieldControl has been called, user code is in control - session.input = ''; - session.linecb(extraForms); - break; + // yieldControl has been called, user code is in control + session.input = ''; + session.linecb(extraForms); + break; } - + currentREPLInterface = null; cljs.setPrintFns(); // If *print-newline* is off, we need to emit a newline now, otherwise @@ -291,7 +292,7 @@ function handleKeyPress( const isReverseSearchKey = ctrl && name === 'r'; if (session.linecb) return; - + // TODO: factor this out into own function if (isReverseSearch || isReverseSearchKey) { let failedSearch = false; From 5a294015a020d3ecea2e96ffe956717a07d5acaf Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Thu, 21 Dec 2017 18:22:11 +0100 Subject: [PATCH 7/9] tighter spill! --- src/cljs/snapshot/lumo/repl.cljs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cljs/snapshot/lumo/repl.cljs b/src/cljs/snapshot/lumo/repl.cljs index 888b87fa..9e6a69f7 100644 --- a/src/cljs/snapshot/lumo/repl.cljs +++ b/src/cljs/snapshot/lumo/repl.cljs @@ -667,9 +667,7 @@ (let [front #js [] back #js [] cb (volatile! nil) - spill! #(loop [] - (when-some [s (.pop back)] - (do (.push front s) (recur))))] + spill! #(when-some [s (.pop back)] (.push front s) (recur))] #js [(fn ([] (spill!) From 6732fad5e8ec80c2c9a4e40750904cdc7ecaa487 Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Thu, 21 Dec 2017 19:03:46 +0100 Subject: [PATCH 8/9] Passing type checks --- src/js/cljs.js | 13 ++++++++++--- src/js/repl.js | 6 ++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/js/cljs.js b/src/js/cljs.js index 06c55bea..5d68b18d 100644 --- a/src/js/cljs.js +++ b/src/js/cljs.js @@ -221,11 +221,15 @@ const printOutFn = mkPrintFn(process.stdout); export function setPrintFns(stream?: stream$Writable): void { if (stream == null || stream === process.stdout) { + // $FlowIssue: context can have globals ClojureScriptContext.cljs.core.set_print_fn_BANG_(printOutFn); + // $FlowIssue: context can have globals ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printErrFn); } else { const printFn = mkPrintFn(stream); + // $FlowIssue: context can have globals ClojureScriptContext.cljs.core.set_print_fn_BANG_(printFn); + // $FlowIssue: context can have globals ClojureScriptContext.cljs.core.set_print_err_fn_BANG_(printFn); } } @@ -255,6 +259,8 @@ function initClojureScriptEngine(opts: CLIOptsType): void { setRuntimeOpts(opts); } +export type AsyncReader = {}; + export function execute( code: string, type?: string = 'text', @@ -262,7 +268,7 @@ export function execute( printNilResult?: boolean = true, sessionID?: number = 0, setNS?: string, - yieldControl: () => void, + yieldControl?: (f: (async_reader: AsyncReader, resume_cb: ()=>void) => void) => void, ): void { // $FlowIssue: context can have globals return ClojureScriptContext.lumo.repl.execute( @@ -309,8 +315,9 @@ export function clearREPLSessionState(sessionID: number): void { return ClojureScriptContext.lumo.repl.clear_state_for_session(sessionID); } -export function createAsyncPipe(): array { - return ClojureScriptContext.lumo.repl.create_async_pipe(); +export function createAsyncPipe(): [(s?: string)=>string, AsyncReader] { + // $FlowIssue: context can have globals + return ClojureScriptContext.lumo.repl.create_async_pipe(); } function executeScript( diff --git a/src/js/repl.js b/src/js/repl.js index a62c8160..af43c53a 100644 --- a/src/js/repl.js +++ b/src/js/repl.js @@ -11,6 +11,7 @@ import { currentTimeMicros, isWhitespace, indentationSpaces } from './util'; import { close as socketServerClose } from './socketRepl'; import type { CLIOptsType } from './cli'; +import type { AsyncReader } from './cljs'; type KeyType = { name: string, @@ -23,7 +24,7 @@ type KeyType = { export type REPLSession = { id: number, rl: readline$Interface, - linecb?: (s?: string) => void, + linecb: ?(s?: string) => string, isMain: boolean, isReverseSearch: boolean, reverseSearchBuffer: string, @@ -93,7 +94,7 @@ export function processLine(replSession: REPLSession, line: string): void { } let suspended; - function yieldControl(f: (async_reader: object, resume_cb: ()=>void)=>void): void { + function yieldControl(f: (async_reader: AsyncReader, resume_cb: ()=>void)=>void): void { suspended = true; const [linecb, reader] = cljs.createAsyncPipe(); session.linecb = linecb; @@ -141,6 +142,7 @@ export function processLine(replSession: REPLSession, line: string): void { if (suspended) { // yieldControl has been called, user code is in control session.input = ''; + // $FlowIssue: linecb is guaranteed to be defined when suspended session.linecb(extraForms); break; } From 8f8eec4e469de4a5a6727cfb1ed3e260bf51e66b Mon Sep 17 00:00:00 2001 From: Christophe Grand Date: Thu, 21 Dec 2017 19:08:48 +0100 Subject: [PATCH 9/9] comment the purpose of each arity --- src/cljs/snapshot/lumo/repl.cljs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cljs/snapshot/lumo/repl.cljs b/src/cljs/snapshot/lumo/repl.cljs index 9e6a69f7..08211a71 100644 --- a/src/cljs/snapshot/lumo/repl.cljs +++ b/src/cljs/snapshot/lumo/repl.cljs @@ -668,15 +668,15 @@ back #js [] cb (volatile! nil) spill! #(when-some [s (.pop back)] (.push front s) (recur))] - #js [(fn - ([] + #js [(fn line-cb + ([] ; 0-arg is called on resumption by repl.js to retrieve the content of the buffer (spill!) (when-some [last (.pop front)] - ; remove last newline when going back to repl/readline + ; remove last newline since repl/readline assumes no trailing newline (let [n (dec (.-length last))] (.push front (if (= \newline (.charAt last n)) (subs last 0 n) last)))) (.join front "")) - ([s] + ([s] ; 1-arg is called by repl.js when new input is available (when (and s (not= "" s)) ; TODO handle EOF (let [s (str s "\n")] (if-some [f @cb]