First stab at 'actors': using threads.

This commit is contained in:
Scott Richmond 2022-05-31 14:21:19 -04:00
parent 09696c212a
commit 1e28556baa
3 changed files with 238 additions and 52 deletions

View File

@ -1,15 +1,17 @@
(ns ludus.interpreter (ns ludus.interpreter
(:require (:require
[ludus.parser :as parser] [ludus.parser :as parser]
[ludus.scanner :as scanner] [ludus.scanner :as scanner]
[ludus.ast :as ast] [ludus.ast :as ast]
[ludus.prelude :as prelude] [ludus.prelude :as prelude]
[ludus.data :as data] [ludus.data :as data]
[ludus.show :as show] [ludus.show :as show]
[ludus.loader :as loader] [ludus.loader :as loader]
[ludus.token :as token] [ludus.token :as token]
[clojure.pprint :as pp] [clojure.pprint :as pp]
[clojure.set])) [clojure.set]))
(def ^:dynamic self 1001)
;; right now this is not very efficient: ;; right now this is not very efficient:
;; it's got runtime checking ;; it's got runtime checking
@ -25,6 +27,62 @@
(declare interpret-ast match interpret) (declare interpret-ast match interpret)
(def processes (atom {}))
(def current-pid (atom 1001))
(defn new-process []
(let [pid @current-pid
process (atom {:pid pid
:queue clojure.lang.PersistentQueue/EMPTY
:in nil
:status :occupied
})]
(swap! processes #(assoc % pid process))
(swap! current-pid inc)
process))
(def vm-state (atom :stopped))
(defn- values [m] (into [] (map (fn [[_ v]] v)) m))
(defn- process-msg [process]
;;(println "processing message" self)
(let [q (:queue process)
in (:in process)]
(when (not (realized? in))
;;(println "delivering message in" self)
(deliver in (peek q))
(assoc process :queue (pop q) :in nil))))
(defn- run-process [process-atom]
(let [process @process-atom
status (:status process)
q (:queue process)
in (:in process)]
;;(println "running process" self ":" (into [] q))
(when (and (= status :idle) (not-empty q) in)
(swap! process-atom process-msg))))
(defn- start-vm []
;; (println "Starting Ludus VM")
(when (= @vm-state :stopped)
(future
(reset! vm-state :running)
(loop []
(if (= @vm-state :running)
(do
(run! run-process (values @processes))
(recur))
;; (println "Ludus VM shutting down")
)))))
(defn- stop-vm []
(reset! vm-state :stopped)
(reset! processes {})
(reset! current-pid 1001)
nil)
(defn- match-tuple [pattern value ctx-vol] (defn- match-tuple [pattern value ctx-vol]
(cond (cond
(not (vector? value)) {:success false :reason "Could not match non-tuple value to tuple"} (not (vector? value)) {:success false :reason "Could not match non-tuple value to tuple"}
@ -219,9 +277,9 @@
:name (str (:name lfn) "{partial}") :name (str (:name lfn) "{partial}")
:body (fn [arg] :body (fn [arg]
(call-fn (call-fn
lfn lfn
(concat [::data/tuple] (replace {::data/placeholder arg} (rest tuple))) (concat [::data/tuple] (replace {::data/placeholder arg} (rest tuple)))
ctx))} ctx))}
(= (::data/type lfn) ::data/clj) (apply (:body lfn) (next tuple)) (= (::data/type lfn) ::data/clj) (apply (:body lfn) (next tuple))
@ -319,9 +377,9 @@
(if (contains? @ctx name) (if (contains? @ctx name)
(throw (ex-info (str "ns name " name " is already bound") {:ast ast})) (throw (ex-info (str "ns name " name " is already bound") {:ast ast}))
(let [ns (into (let [ns (into
{::data/struct true ::data/type ::data/ns ::data/name name} {::data/struct true ::data/type ::data/ns ::data/name name}
(map-values #(interpret-ast % ctx)) (map-values #(interpret-ast % ctx))
members)] members)]
(vswap! ctx update-ctx {name ns}) (vswap! ctx update-ctx {name ns})
ns)))) ns))))
@ -383,8 +441,8 @@
(if (= (::ast/type member) ::ast/splat) (if (= (::ast/type member) ::ast/splat)
(let [splatted (interpret-ast (:expr member) ctx) (let [splatted (interpret-ast (:expr member) ctx)
splat-list? (and splat-list? (and
(vector? splatted) (vector? splatted)
(not (= (first splatted) ::data/tuple)))] (not (= (first splatted) ::data/tuple)))]
(if splat-list? (if splat-list?
(concat list splatted) (concat list splatted)
(throw (ex-info "Cannot splat non-list into list" {:ast member})))) (throw (ex-info "Cannot splat non-list into list" {:ast member}))))
@ -413,8 +471,8 @@
(if (= (::ast/type member) ::ast/splat) (if (= (::ast/type member) ::ast/splat)
(let [splatted (interpret-ast (:expr member) ctx) (let [splatted (interpret-ast (:expr member) ctx)
splat-map? (and splat-map? (and
(map? splatted) (map? splatted)
(::data/dict splatted))] (::data/dict splatted))]
(if splat-map? (if splat-map?
(merge dict splatted) (merge dict splatted)
(throw (ex-info "Cannot splat non-dict into dict" {:ast member})))) (throw (ex-info "Cannot splat non-dict into dict" {:ast member}))))
@ -425,8 +483,61 @@
(let [members (:members ast)] (let [members (:members ast)]
(assoc (reduce (dict-term ctx) {} members) ::data/dict true))) (assoc (reduce (dict-term ctx) {} members) ::data/dict true)))
(defn- interpret-receive [ast ctx]
(let [process-atom (get @processes self)
in (promise)
clauses (:clauses ast)]
;; (println "receiving in" self)
(swap! process-atom #(assoc % :in in :status :idle))
(let [msg @in]
(swap! process-atom #(assoc % :status :occupied))
;; (println "message received by" self ":" msg)
(loop [clause (first clauses)
clauses (rest clauses)]
(if clause
(let [pattern (:pattern clause)
body (:body clause)
new-ctx (volatile! {::parent ctx})
match? (match pattern msg new-ctx)
success (:success match?)
clause-ctx (:ctx match?)]
(if success
(do
(vswap! new-ctx #(merge % clause-ctx))
(let [result (interpret-ast body new-ctx)]
(swap! process-atom #(assoc % :status :idle))
result))
(recur (first clauses) (rest clauses))))
(throw (ex-info "Match Error: No match found" {:ast ast})))))))
(defn- interpret-send [ast ctx]
(let [msg (interpret-ast (:msg ast) ctx)
pid (interpret-ast (:pid ast) ctx)
process-atom (get @processes pid)
process @process-atom
q (:queue process)
status (:status process)]
(when (not (= :dead status))
(swap! process-atom #(assoc % :queue (conj q msg)))
;;(println "sent" msg "to" (:pid process))
)
msg))
(defn- interpret-spawn [ast ctx]
(let [expr (:expr ast)
process (new-process)
pid (:pid @process)]
(with-bindings {#'self pid}
(future
(try (interpret-ast expr ctx)
(catch Exception e
(println "Panic in Ludus process" (str self ":") (ex-message e))))
(swap! @process #(assoc % :status :dead))))
pid))
(defn interpret-ast [ast ctx] (defn interpret-ast [ast ctx]
(case (::ast/type ast) (case (::ast/type ast)
::ast/self self
::ast/atom (:value ast) ::ast/atom (:value ast)
@ -456,6 +567,12 @@
::ast/panic (panic ast ctx) ::ast/panic (panic ast ctx)
::ast/spawn (interpret-spawn ast ctx)
::ast/send (interpret-send ast ctx)
::ast/receive (interpret-receive ast ctx)
::ast/recur ::ast/recur
{::data/recur true :tuple (interpret-ast (:tuple ast) ctx)} {::data/recur true :tuple (interpret-ast (:tuple ast) ctx)}
@ -482,8 +599,8 @@
::ast/tuple ::ast/tuple
(let [members (:members ast)] (let [members (:members ast)]
(into (into
[(if (:partial ast) ::data/partial ::data/tuple)] [(if (:partial ast) ::data/partial ::data/tuple)]
(map #(interpret-ast % ctx)) members)) (map #(interpret-ast % ctx)) members))
::ast/list (interpret-list ast ctx) ::ast/list (interpret-list ast ctx)
@ -500,20 +617,32 @@
(defn interpret [parsed file] (defn interpret [parsed file]
(try (try
(let [base-ctx (volatile! (merge {:file file} prelude/prelude))] (let [base-ctx (volatile! (merge {:file file} prelude/prelude))
(interpret-ast (::parser/ast parsed) base-ctx)) process (new-process)]
(start-vm)
(with-bindings {#'self (:pid @process)}
(let [result (interpret-ast (::parser/ast parsed) base-ctx)]
(swap! process #(assoc % :status :dead))
(stop-vm)
result)))
(catch clojure.lang.ExceptionInfo e (catch clojure.lang.ExceptionInfo e
(println "Ludus panicked in" file) (println "Ludus panicked in" file)
(println "On line" (get-in (ex-data e) [:ast :token ::token/line])) (println "On line" (get-in (ex-data e) [:ast :token ::token/line]))
(println (ex-message e)) (println (ex-message e))
;;(pp/pprint (ex-data e))
(System/exit 67)))) (System/exit 67))))
(defn interpret-safe [parsed] (defn interpret-safe [parsed]
(try (try
(let [base-ctx (volatile! (merge {} prelude/prelude))] (let [base-ctx (volatile! (merge {} prelude/prelude))
(interpret-ast (::parser/ast parsed) base-ctx)) process (new-process)]
(start-vm)
(with-bindings {#'self (:pid @process)}
(let [result (interpret-ast (::parser/ast parsed) base-ctx)]
(swap! process #(assoc % :status :dead))
result))
(stop-vm))
(catch clojure.lang.ExceptionInfo e (catch clojure.lang.ExceptionInfo e
(stop-vm)
(println "Ludus panicked!") (println "Ludus panicked!")
(println "On line" (get-in (ex-data e) [:ast :token ::token/line])) (println "On line" (get-in (ex-data e) [:ast :token ::token/line]))
(println (ex-message e)) (println (ex-message e))
@ -539,9 +668,31 @@
(println (ex-message e)) (println (ex-message e))
{:result ::error :ctx (volatile! orig-ctx)}))))) {:result ::error :ctx (volatile! orig-ctx)})))))
(comment
(def source "panic! :oops (comment
(start-vm)
(def source "
fn echo () -> {
& print (self, :echoing)
loop () with () -> {
& print (self, :looping)
receive {
msg -> {
print (\"from \", self, \": \", msg)
recur ()
}
}
}
}
let echoer = spawn echo ()
send :hello to echoer
send :foo to echoer
send :bar to echoer
send :baz to echoer
sleep (1000)
") ")
@ -550,13 +701,13 @@
(println "*** *** NEW INTERPRETATION *** ***") (println "*** *** NEW INTERPRETATION *** ***")
(println "") (println "")
(-> source (let [result (-> source
(scanner/scan) (scanner/scan)
(parser/parse) (parser/parse)
(interpret-safe) (interpret-safe)
(show/show) (show/show)
;;(println) )]
)) result))
(comment " (comment "

View File

@ -953,11 +953,7 @@
(defn- parse-spawn [parser] (defn- parse-spawn [parser]
(let [expr (parse-expr (advance parser))] (let [expr (parse-expr (advance parser))]
(case (node-type expr) (assoc expr ::ast {::ast/type ::ast/spawn :expr (::ast expr) :token (current parser)})))
(::ast/word ::ast/fn ::ast/synthetic)
(assoc expr ::ast {::ast/type ::ast/spawn :token (current parser) :expr (::ast expr)})
(panic parser "Expected function literal, word, or synthetic expression after spawn."))))
(defn- parse-send [parser] (defn- parse-send [parser]
(let [msg (parse-expr (advance parser)) (let [msg (parse-expr (advance parser))
@ -1000,9 +996,12 @@
(::token/lparen ::token/keyword) (parse-synthetic parser) (::token/lparen ::token/keyword) (parse-synthetic parser)
(parse-word parser))) (parse-word parser)))
(::token/nil ::token/true ::token/false ::token/self) (::token/nil ::token/true ::token/false)
(parse-atomic-word parser) (parse-atomic-word parser)
(::token/self)
(assoc (advance parser) ::ast {::ast/type ::ast/self :token token})
::token/lparen (parse-tuple parser) ::token/lparen (parse-tuple parser)
::token/lbracket (parse-list parser) ::token/lbracket (parse-list parser)
@ -1067,24 +1066,54 @@
(comment (comment
(def pp pp/pprint) (def pp pp/pprint)
(def source " (def source1 "
cond { x -> x fn echo () -> {
y -> y } loop () with () -> {
receive {
msg -> {
print (msg)
recur ()
}
}
}
}
& let my = spawn echo ()
") ")
(def lexed (scanner/scan source)) (def source2 "
(def tokens (:tokens lexed)) fn echo () -> {
(def p (parser tokens)) loop () with () -> {
receive {
msg -> {
print (msg)
recur ()
}
}
}
}
")
(time (do (def lexed2 (scanner/scan source2))
(def tokens2 (:tokens lexed2))
(def p2 (parser tokens2))
(def ast2 (::ast (parse-script p2)))))
(time (do (def lexed1 (scanner/scan source1))
(def tokens1 (:tokens lexed1))
(def p1 (parser tokens1))
(def ast1 (::ast (parse-script p1)))))
(println "") (println "")
(println "") (println "")
(println "******************************************************") (println "******************************************************")
(println "") (println "")
(println "*** *** NEW PARSE *** ***") (println "*** *** TEST PARSE *** ***")
(-> p (println "asts are the same?" (= ast1 ast2))
(parse-script) (pp ast1)
(::ast) (pp ast2))
(pp)))
(comment " (comment "
Further thoughts/still to do: Further thoughts/still to do:

View File

@ -1,7 +1,7 @@
(ns ludus.prelude (ns ludus.prelude
(:require (:require
[ludus.data :as data] [ludus.data :as data]
[ludus.show])) [ludus.show]))
;; TODO: make eq, and, or special forms that short-circuit ;; TODO: make eq, and, or special forms that short-circuit
;; Right now, they evaluate all their args ;; Right now, they evaluate all their args
@ -75,6 +75,10 @@
::data/type ::data/clj ::data/type ::data/clj
:body ludus.show/show}) :body ludus.show/show})
(def sleep- {:name "sleep"
::data/type ::data/clj
:body (fn [ms] (Thread/sleep ms))})
(def prelude {"eq" eq (def prelude {"eq" eq
"add" add "add" add
;;"panic!" panic! ;;"panic!" panic!
@ -89,4 +93,6 @@
"deref" deref- "deref" deref-
"set!" set!- "set!" set!-
"and" and- "and" and-
"or" or-}) "or" or-
"sleep" sleep-
})