Skip to content
This repository has been archived by the owner on Jun 4, 2022. It is now read-only.

Suspension #334

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 152 additions & 60 deletions src/cljs/snapshot/lumo/repl.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,59 @@
"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))

(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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably worth giving a name to fn just to make the pattern clearer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this loop [] is useless it should be

+        spill! #(when-some [s (.pop back)]
+                  (do (.push front s) (recur)))]

where would you like a name?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the fn inside the Javascript array

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, it's just #(when-some [s (.pop back)] (.push front s) (recur)) ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean this line:

(fn 
        ([]
         (spill!)

The two-arity we, minor anyways

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added comments to each arity in 8f8eec4

([]
(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
(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)]
(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
Expand Down Expand Up @@ -917,7 +970,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)]
Expand Down Expand Up @@ -982,7 +1035,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)
Expand All @@ -1009,7 +1062,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."
Expand All @@ -1024,7 +1079,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."}
Expand All @@ -1044,7 +1101,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."
Expand Down Expand Up @@ -1113,53 +1173,82 @@
(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 host-yield-control] :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 host-yield-control
(if-let [re-yield @suspended]
(re-yield value)
(do
(capture-session-state-for-session-id session-id)
; 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?)
(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))))))))))
(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))))
nil))

(defn- execute-source
[source-or-path {:keys [type] :as opts}]
Expand All @@ -1168,14 +1257,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]
Expand Down
40 changes: 19 additions & 21 deletions src/js/cljs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -270,6 +262,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(
Expand All @@ -279,6 +272,7 @@ export function execute(
printNilResult,
setNS,
sessionID,
yieldControl
);
}

Expand Down Expand Up @@ -315,6 +309,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,
Expand Down
Loading