568 lines
18 KiB
Clojure
568 lines
18 KiB
Clojure
(ns ludus.interpreter
|
|
(:require
|
|
[ludus.parser :as parser]
|
|
[ludus.scanner :as scanner]
|
|
[ludus.ast :as ast]
|
|
[ludus.prelude :as prelude]
|
|
[ludus.data :as data]
|
|
[ludus.show :as show]
|
|
[clojure.pprint :as pp]
|
|
[clojure.set]))
|
|
|
|
;; right now this is not very efficient:
|
|
;; it's got runtime checking
|
|
;; we should be able to do these checks statically
|
|
;; that's for later, tho
|
|
(defn- resolve-word [word ctx-vol]
|
|
(let [ctx @ctx-vol]
|
|
(if (contains? ctx word)
|
|
(get ctx word)
|
|
(if (contains? ctx ::parent)
|
|
(recur word (::parent ctx))
|
|
(throw (ex-info (str "Unbound name: " word) {}))))))
|
|
|
|
(declare interpret-ast match interpret)
|
|
|
|
(defn- match-tuple [pattern value ctx-vol]
|
|
(cond
|
|
(not (vector? value)) {:success false :reason "Could not match non-tuple value to tuple"}
|
|
|
|
(not (= ::data/tuple (first value))) {:success false :reason "Could not match list to tuple"}
|
|
|
|
(not (= (:length pattern) (dec (count value))))
|
|
{:success false :reason "Cannot match tuples of different lengths"}
|
|
|
|
(= 0 (:length pattern) (dec (count value))) {:success true :ctx {}}
|
|
|
|
:else (let [members (:members pattern)]
|
|
(loop [i (:length pattern)
|
|
ctx {}]
|
|
(if (= 0 i)
|
|
{:success true :ctx ctx}
|
|
(let [match? (match (nth members (dec i)) (nth value i) ctx-vol)]
|
|
(if (:success match?)
|
|
(recur (dec i) (merge ctx (:ctx match?)))
|
|
{:success false :reason (str "Could not match " pattern " with " value)})))))))
|
|
|
|
(defn- match-list [pattern value ctx-vol]
|
|
(cond
|
|
(not (vector? value)) {:success false :reason "Could not match non-list value to list"}
|
|
|
|
(= ::data/tuple (first value)) {:success false :reason "Could not match tuple value to list pattern"}
|
|
|
|
;; TODO: fix this with splats
|
|
(not (= (count (:members pattern)) (count value)))
|
|
{:success false :reason "Cannot match lists of different lengths"}
|
|
|
|
(= 0 (count (:members pattern)) (count value)) {:success true :ctx {}}
|
|
|
|
:else (let [members (:members pattern)]
|
|
(loop [i (dec (count members))
|
|
ctx {}]
|
|
(if (> 0 i)
|
|
{:success true :ctx ctx}
|
|
(let [match? (match (nth members i) (nth value i) ctx-vol)]
|
|
(if (:success match?)
|
|
(recur (dec i) (merge ctx (:ctx match?)))
|
|
{:success false :reason (str "Could not match " pattern " with " value)})))))))
|
|
|
|
(defn- match-hashmap [pattern value ctx-vol]
|
|
(cond
|
|
(not (map? value))
|
|
{:success false :reason "Could not match non-hashmap value to hashmap pattern"}
|
|
|
|
(not (::data/hashmap value))
|
|
{:success false :reason "Cannot match non-hashmap data types a hashmap pattern"}
|
|
|
|
:else
|
|
(let [members (:members pattern)
|
|
kws (keys members)]
|
|
(loop [i (dec (count kws)) ctx {}]
|
|
(if (> 0 i)
|
|
{:success true :ctx ctx}
|
|
(let [kw (nth kws i)]
|
|
(if (contains? value kw)
|
|
(let [match? (match (kw members) (kw value) ctx-vol)]
|
|
(if (:success match?)
|
|
(recur (dec i) (merge ctx (:ctx match?)))
|
|
{:success false :reason (str "Could not match " pattern " with " value " at key " kw)}
|
|
))
|
|
{:success false :reason (str "Could not match " pattern " with " value " at key " kw)}
|
|
)))))))
|
|
|
|
(defn- match-struct [pattern value ctx-vol]
|
|
(cond
|
|
(not (map? value))
|
|
{:success false :reason "Could not match non-struct value to struct pattern"}
|
|
|
|
(not (::data/struct value))
|
|
{:success false :reason "Cannot match non-struct data types a struct pattern"}
|
|
|
|
:else
|
|
(let [members (:members pattern)
|
|
kws (keys members)]
|
|
(loop [i (dec (count kws)) ctx {}]
|
|
(if (> 0 i)
|
|
{:success true :ctx ctx}
|
|
(let [kw (nth kws i)]
|
|
(if (contains? value kw)
|
|
(let [match? (match (kw members) (kw value) ctx-vol)]
|
|
(if (:success match?)
|
|
(recur (dec i) (merge ctx (:ctx match?)))
|
|
{:success false :reason (str "Could not match " pattern " with " value " at key " kw)}
|
|
))
|
|
{:success false :reason (str "Could not match " pattern " with " value " at key " kw)}
|
|
)))))))
|
|
|
|
(defn- match [pattern value ctx-vol]
|
|
(let [ctx @ctx-vol]
|
|
(case (::ast/type pattern)
|
|
::ast/placeholder {:success true :ctx {}}
|
|
|
|
::ast/atom
|
|
(let [match-value (:value pattern)]
|
|
(if (= match-value value)
|
|
{:success true :ctx {}}
|
|
{:success false
|
|
:reason (str "No match: Could not match " match-value " with " value)}))
|
|
|
|
::ast/word
|
|
(let [word (:word pattern)]
|
|
(if (contains? ctx word)
|
|
{:success false :reason (str "Name " word " is already bound")}
|
|
{:success true :ctx {word value}}))
|
|
|
|
::ast/tuple (match-tuple pattern value ctx-vol)
|
|
|
|
::ast/list (match-list pattern value ctx-vol)
|
|
|
|
::ast/hash (match-hashmap pattern value ctx-vol)
|
|
|
|
::ast/struct (match-struct pattern value ctx-vol)
|
|
|
|
(throw (ex-info "Unknown pattern on line " {:pattern pattern})))))
|
|
|
|
(defn- update-ctx [ctx new-ctx]
|
|
(merge ctx new-ctx))
|
|
|
|
;; TODO: get "if let" pattern working
|
|
;; TODO: get typed exceptions to distinguish panics
|
|
(defn- interpret-let [ast ctx]
|
|
(let [pattern (:pattern ast)
|
|
expr (:expr ast)
|
|
value (interpret-ast expr ctx)
|
|
match (match pattern value ctx)
|
|
success (:success match)]
|
|
(if success
|
|
(vswap! ctx update-ctx (:ctx match))
|
|
(throw (ex-info (:reason match) {:ast ast})))
|
|
value))
|
|
|
|
(defn- interpret-if [ast ctx]
|
|
(let [if-expr (:if ast)
|
|
then-expr (:then ast)
|
|
else-expr (:else ast)
|
|
if-value (interpret-ast if-expr ctx)]
|
|
(if if-value
|
|
(interpret-ast then-expr ctx)
|
|
(interpret-ast else-expr ctx))))
|
|
|
|
(defn- interpret-match [ast ctx]
|
|
(let [match-expr (:expr ast)
|
|
expr (interpret-ast match-expr ctx)
|
|
clauses (:clauses ast)]
|
|
(loop [clause (first clauses)
|
|
clauses (rest clauses)]
|
|
(if clause
|
|
(let [pattern (:pattern clause)
|
|
body (:body clause)
|
|
new-ctx (volatile! {::parent ctx})
|
|
match? (match pattern expr new-ctx)
|
|
success (:success match?)
|
|
clause-ctx (:ctx match?)]
|
|
(if success
|
|
(do
|
|
(vswap! new-ctx #(merge % clause-ctx))
|
|
(interpret-ast body new-ctx))
|
|
(recur (first clauses) (rest clauses))))
|
|
(throw (ex-info "Match Error: No match found" {:ast ast}))))))
|
|
|
|
(defn- interpret-cond [ast ctx]
|
|
(let [clauses (:clauses ast)]
|
|
(loop [clause (first clauses)
|
|
clauses (rest clauses)]
|
|
(if (not clause)
|
|
(throw (ex-info "Cond Error: No match found" {:ast ast}))
|
|
(let [test-expr (:test clause)
|
|
body (:body clause)
|
|
truthy? (boolean (interpret-ast test-expr ctx))]
|
|
(if truthy?
|
|
(interpret-ast body ctx)
|
|
(recur (first clauses) (rest clauses))
|
|
)
|
|
)
|
|
)
|
|
)))
|
|
|
|
(defn- interpret-called-kw [kw tuple ctx]
|
|
;; TODO: check this statically
|
|
(if (not (= 1 (:length tuple)))
|
|
(throw (ex-info "Called keywords must be unary" {:ast kw}))
|
|
(let [kw (interpret-ast kw ctx)
|
|
map (second (interpret-ast tuple ctx))]
|
|
(if (::data/struct map)
|
|
(if (contains? map kw)
|
|
(kw map)
|
|
(if (= (::data/type map) ::data/ns)
|
|
(throw (ex-info (str "Namespace error: no member " kw " in ns " (::data/name map)) {:ast kw}))
|
|
(throw (ex-info (str "Struct error: no member at " kw) {:ast kw}))))
|
|
(get map kw))
|
|
)))
|
|
|
|
(defn- call-fn [lfn tuple ctx]
|
|
(cond
|
|
(= ::data/partial (first tuple))
|
|
{::data/type ::data/clj
|
|
:name (str (:name lfn) "{partial}")
|
|
:body (fn [arg]
|
|
(call-fn
|
|
lfn
|
|
(concat [::data/tuple] (replace {::data/placeholder arg} (rest tuple)))
|
|
ctx))}
|
|
|
|
(= (::data/type lfn) ::data/clj) (apply (:body lfn) (next tuple))
|
|
|
|
(= (::data/type lfn) ::data/fn)
|
|
(let [clauses (:clauses lfn)
|
|
closed-over (:ctx lfn)]
|
|
(loop [clause (first clauses)
|
|
clauses (rest clauses)]
|
|
(if clause
|
|
(let [pattern (:pattern clause)
|
|
body (:body clause)
|
|
fn-ctx (volatile! {::parent closed-over})
|
|
match? (match pattern tuple fn-ctx)
|
|
success (:success match?)
|
|
clause-ctx (:ctx match?)]
|
|
(if success
|
|
(do
|
|
(vswap! fn-ctx #(merge % clause-ctx))
|
|
(interpret-ast body fn-ctx))
|
|
(recur (first clauses) (rest clauses))))
|
|
|
|
(throw (ex-info "Match Error: No match found" {:ast (:ast lfn)})))))
|
|
|
|
(keyword? lfn)
|
|
(if (= 2 (count tuple))
|
|
(let [target (second tuple) kw lfn]
|
|
(if (::data/struct target)
|
|
(if (contains? target kw)
|
|
(kw target)
|
|
(if (= (::data/type target) ::data/ns)
|
|
(throw (ex-info (str "Namespace error: no member " kw " in ns" (::data/name target)) {:ast kw}))
|
|
(throw (ex-info (str "Struct error: no member at " kw) {:ast kw}))
|
|
)
|
|
)
|
|
(kw target)))
|
|
(throw (ex-info "Called keywords take a single argument" {:ast lfn})))
|
|
|
|
:else (throw (ex-info "I don't know how to call that" {:ast lfn}))))
|
|
|
|
(defn- interpret-synthetic-term [prev-value curr ctx]
|
|
(let [type (::ast/type curr)]
|
|
(if (= type ::ast/atom)
|
|
(if (::data/struct prev-value)
|
|
(if (contains? prev-value (:value curr))
|
|
(get prev-value (:value curr))
|
|
(if (= (::data/type prev-value) ::data/ns)
|
|
(throw (ex-info (str "Namespace error: no member " (:value curr) " in ns " (::data/name prev-value)) {:ast curr}))
|
|
(throw (ex-info (str "Struct error: no member " (:value curr)) {:ast curr}))))
|
|
(get prev-value (:value curr)))
|
|
(call-fn prev-value (interpret-ast curr ctx) ctx))))
|
|
|
|
(defn- interpret-synthetic [ast ctx]
|
|
(let [terms (:terms ast)
|
|
first (first terms)
|
|
second (second terms)
|
|
rest (rest (rest terms))
|
|
first-term-type (::ast/type first)
|
|
first-val (if (= first-term-type ::ast/atom)
|
|
(interpret-called-kw first second ctx)
|
|
(interpret-synthetic-term (interpret-ast first ctx) second ctx))]
|
|
(reduce #(interpret-synthetic-term %1 %2 ctx) first-val rest)))
|
|
|
|
(defn- interpret-fn [ast ctx] ;; TODO: fix context/closure (no cycles)?
|
|
(let [name (:name ast)
|
|
clauses (:clauses ast)]
|
|
(if (= name ::ast/anon)
|
|
{::data/type ::data/fn
|
|
:name name
|
|
:ast ast
|
|
:clauses clauses
|
|
:ctx ctx}
|
|
(let [fn {::data/type ::data/fn
|
|
:name name
|
|
:clauses clauses
|
|
:ctx ctx}]
|
|
(if (contains? @ctx name)
|
|
(throw (ex-info (str "Name " name " is already bound") {:ast ast}))
|
|
(do
|
|
(vswap! ctx update-ctx {name fn})
|
|
fn))))))
|
|
|
|
(defn- interpret-do [ast ctx]
|
|
(let [exprs (:exprs ast)
|
|
origin (interpret-ast (first exprs) ctx)
|
|
fns (rest exprs)]
|
|
(reduce #(call-fn (interpret-ast %2 ctx) [::data/tuple %1] ctx) origin fns)))
|
|
|
|
(defn- map-values [f]
|
|
(map (fn [kv]
|
|
(let [[k v] kv]
|
|
[k (f v)]))))
|
|
|
|
(defn- interpret-ns [ast ctx]
|
|
(let [members (:members ast)
|
|
name (:name ast)]
|
|
(if (contains? @ctx name)
|
|
(throw (ex-info (str "ns name " name " is already bound") {:ast ast}))
|
|
(let [ns (into
|
|
{::data/struct true ::data/type ::data/ns ::data/name name}
|
|
(map-values #(interpret-ast % ctx))
|
|
members)]
|
|
(vswap! ctx update-ctx {name ns})
|
|
ns))))
|
|
|
|
(defn- interpret-import [ast ctx]
|
|
(let [path (:path ast)
|
|
name (:name ast)]
|
|
(if (contains? @ctx name)
|
|
(throw (ex-info (str "Name " name " is alrady bound") {:ast ast}))
|
|
(let [result ;; TODO: add any error handling at all
|
|
(-> path
|
|
(slurp)
|
|
(scanner/scan)
|
|
(parser/parse)
|
|
(interpret))]
|
|
(vswap! ctx update-ctx {name result})
|
|
result ;; TODO: test this!
|
|
))))
|
|
|
|
(defn- interpret-ref [ast ctx]
|
|
(let [name (:name ast) expr (:expr ast)]
|
|
(if (contains? @ctx name)
|
|
(throw (ex-info (str "Name " name " is already bound") {:ast ast})))
|
|
(let [value (interpret-ast expr ctx)
|
|
box (atom value)
|
|
ref {::data/ref true ::data/value box ::data/name name}]
|
|
(vswap! ctx update-ctx {name ref})
|
|
ref)))
|
|
|
|
(defn- interpret-loop [ast ctx]
|
|
(let [tuple (interpret-ast (:expr ast) ctx)
|
|
clauses (:clauses ast)]
|
|
(loop [input tuple]
|
|
(let [output (loop [clause (first clauses)
|
|
clauses (rest clauses)]
|
|
(if clause
|
|
(let [pattern (:pattern clause)
|
|
body (:body clause)
|
|
new-ctx (volatile! {::parent ctx})
|
|
match? (match pattern input new-ctx)
|
|
success (:success match?)
|
|
clause-ctx (:ctx match?)]
|
|
(if success
|
|
(do
|
|
(vswap! new-ctx #(merge % clause-ctx))
|
|
(interpret-ast body new-ctx))
|
|
(recur (first clauses) (rest clauses))))
|
|
|
|
(throw (ex-info (str "Match Error: No match found in loop for " input) {:ast ast}))))]
|
|
(if (::data/recur output)
|
|
(recur (:tuple output))
|
|
output
|
|
))
|
|
))
|
|
)
|
|
|
|
(defn- panic [ast ctx]
|
|
(throw (ex-info (show/show (interpret-ast (:expr ast) ctx)) {:ast ast})))
|
|
|
|
(defn- list-term [ctx]
|
|
(fn [list member]
|
|
(if (= (::ast/type member) ::ast/splat)
|
|
(let [splatted (interpret-ast (:expr member) ctx)
|
|
splat-list? (and
|
|
(vector? splatted)
|
|
(not (= (first splatted) ::data/tuple)))]
|
|
(if splat-list?
|
|
(concat list splatted)
|
|
(throw (ex-info "Cannot splat non-list into list" {:ast member}))))
|
|
(concat list [(interpret-ast member ctx)]))))
|
|
|
|
(defn- interpret-list [ast ctx]
|
|
(let [members (:members ast)]
|
|
(into [] (reduce (list-term ctx) [] members))))
|
|
|
|
(defn- set-term [ctx]
|
|
(fn [set member]
|
|
(if (= (::ast/type member) ::ast/splat)
|
|
(let [splatted (interpret-ast (:expr member) ctx)
|
|
splat-set? (set? splatted)]
|
|
(if splat-set?
|
|
(clojure.set/union set splatted)
|
|
(throw (ex-info "Cannot splat non-set into set" {:ast member}))))
|
|
(conj set (interpret-ast member ctx)))))
|
|
|
|
(defn- interpret-set [ast ctx]
|
|
(let [members (:members ast)]
|
|
(reduce (set-term ctx) #{} members)))
|
|
|
|
(defn- hash-term [ctx]
|
|
(fn [hash member]
|
|
(if (= (::ast/type member) ::ast/splat)
|
|
(let [splatted (interpret-ast (:expr member) ctx)
|
|
splat-map? (and
|
|
(map? splatted)
|
|
(::data/hashmap splatted))]
|
|
(if splat-map?
|
|
(merge hash splatted)
|
|
(throw (ex-info "Cannot splat non-hashmap into hashmap" {:ast member}))))
|
|
(let [k (first member) v (second member)]
|
|
(assoc hash k (interpret-ast v ctx))))))
|
|
|
|
(defn- interpret-hash [ast ctx]
|
|
(let [members (:members ast)]
|
|
(assoc (reduce (hash-term ctx) {} members) ::data/hashmap true)
|
|
)
|
|
)
|
|
|
|
(defn interpret-ast [ast ctx]
|
|
(case (::ast/type ast)
|
|
|
|
::ast/atom (:value ast)
|
|
|
|
::ast/word (resolve-word (:word ast) ctx)
|
|
|
|
::ast/let (interpret-let ast ctx)
|
|
|
|
::ast/if (interpret-if ast ctx)
|
|
|
|
::ast/match (interpret-match ast ctx)
|
|
|
|
::ast/cond (interpret-cond ast ctx)
|
|
|
|
::ast/synthetic (interpret-synthetic ast ctx)
|
|
|
|
::ast/fn (interpret-fn ast ctx)
|
|
|
|
::ast/pipeline (interpret-do ast ctx)
|
|
|
|
::ast/placeholder ::data/placeholder
|
|
|
|
::ast/ns (interpret-ns ast ctx)
|
|
|
|
::ast/import (interpret-import ast ctx)
|
|
|
|
::ast/ref (interpret-ref ast ctx)
|
|
|
|
::ast/panic (panic ast ctx)
|
|
|
|
::ast/recur
|
|
{::data/recur true :tuple (interpret-ast (:tuple ast) ctx)}
|
|
|
|
::ast/loop (interpret-loop ast ctx)
|
|
|
|
::ast/block
|
|
(let [exprs (:exprs ast)
|
|
inner (pop exprs)
|
|
last (peek exprs)
|
|
ctx (volatile! {::parent ctx})]
|
|
(run! #(interpret-ast % ctx) inner)
|
|
(interpret-ast last ctx))
|
|
|
|
::ast/script
|
|
(let [exprs (:exprs ast)
|
|
inner (pop exprs)
|
|
last (peek exprs)
|
|
ctx (volatile! prelude/prelude)]
|
|
(run! #(interpret-ast % ctx) inner)
|
|
(interpret-ast last ctx))
|
|
|
|
;; note that, excepting tuples and structs,
|
|
;; runtime representations are bare
|
|
;; tuples are vectors with a special first member
|
|
::ast/tuple
|
|
(let [members (:members ast)]
|
|
(into
|
|
[(if (:partial ast) ::data/partial ::data/tuple)]
|
|
(map #(interpret-ast % ctx)) members))
|
|
|
|
::ast/list (interpret-list ast ctx)
|
|
|
|
::ast/set
|
|
(interpret-set ast ctx)
|
|
|
|
::ast/hash (interpret-hash ast ctx)
|
|
|
|
::ast/struct
|
|
(let [members (:members ast)]
|
|
(into {::data/struct true} (map-values #(interpret-ast % ctx)) members))
|
|
|
|
(throw (ex-info "Unknown AST node type" {:ast ast}))))
|
|
|
|
(defn interpret [parsed]
|
|
(try
|
|
(interpret-ast (::parser/ast parsed) {})
|
|
(catch clojure.lang.ExceptionInfo e
|
|
(println "Ludus panicked!")
|
|
(println (ex-message e))
|
|
(pp/pprint (ex-data e))
|
|
(System/exit 67))))
|
|
|
|
(defn interpret-safe [parsed]
|
|
(try
|
|
(interpret-ast (::parser/ast parsed) {})
|
|
(catch clojure.lang.ExceptionInfo e
|
|
(println "Ludus panicked!")
|
|
(println (ex-message e))
|
|
(pp/pprint (ex-data e)))))
|
|
|
|
(comment
|
|
|
|
(def source "
|
|
|
|
fn foo () -> ${1, 2, 3}
|
|
let bar = ${1, ...foo (), 3}
|
|
|
|
")
|
|
|
|
(println "")
|
|
(println "****************************************")
|
|
(println "*** *** NEW INTERPRETATION *** ***")
|
|
(println "")
|
|
|
|
(-> source
|
|
(scanner/scan)
|
|
(parser/parse)
|
|
(interpret-safe)
|
|
(show/show)
|
|
;;(println)
|
|
))
|
|
|
|
(comment "
|
|
|
|
Left to do:
|
|
* if-let pattern
|
|
* improve panics
|
|
* add location info for panics
|
|
* refactor calling keywords
|
|
* refactor accessing structs vs. hashes
|
|
|
|
")
|
|
|
|
|
|
|
|
|
|
|