Refactor into modules, fix if typing

This commit is contained in:
Jeremy Penner 2024-06-22 19:54:59 -04:00
parent 65675bcb00
commit fca1219d2e
6 changed files with 363 additions and 249 deletions

53
notes.md Normal file
View file

@ -0,0 +1,53 @@
# type system
data types:
i64, f64 - numbers
bool - logical boolean values
{fn [type1 type2 ... -> rettype]}
{array [type length]}
(struct name
^type1 member1
^type2 member2
^type3 member3)
{tuple [^{name member1} type1 ^{name member2} type2 ^{name member3} type3]}
(enum name
(clause1 type1 type2 ...)
(clause2 type1 type2 ...)
clause3)
{variant [^{tuple [type1 type2 ...]} clause1
^{tuple [type1 type2 ...]} clause2
^void clause3]}
(protocol name
(method name [self ^type1 arg1 ^type2 arg2 -> rettype]))
(impl protocol type
(method name [self ^type1 arg1 ^type2 arg2 -> rettype]
body...))
Variables have both a datatype and an isolation modifier. There are three possible isolation types:
* `const` - this is the default, if no isolation modifier is given. No in-place mutations are possible with `const` values.
* `val` - a `val` variable can be mutated in-place, but changes _only_ affect that variable. If it is assigned to any other
variable or passed as a parameter, it is copied if necessary and can be treated as a new, totally distinct value.
* `ref` - a `ref` is analogous to a full pointer or object reference. Copies of the same `ref` can exist in multiple places,
and refer to the same object in memory. Changes to data mutated via a `ref` are immediately visible to any other
code that has the same `ref`.
When defining data structures, individual members can be annotated as `ref`, but not `const` or `val` - only the structure
as a whole can be `const` or `val`.
Q: should `const` structures be able to modify `ref` members directly? leaning towards yes. `ref` seems to inherently imply
interior mutability.
# memory management
* "hot reload" implies "given the previous source code and a memory, I can reason about the types of everything in the memory"
* compacting garbage collection is simply the degenerate case of rearranging memory to be legible to new code!
* we have typed roots (globals), and we can follow typed references from there
* stack is not accessible from wasm, and GC / reload would only happen when wasm code returns to JS - no suspension

View file

@ -0,0 +1,24 @@
(ns tock.compiler
(:require [tock.compiler.util :refer [form-dispatch compile-error lookup bind!] :as u]
[tock.compiler.type :refer [typecheck-expr]]
[tock.compiler.wasm :refer [ctx-to-wasm]]
[meander.epsilon :as m]))
(defmulti compile-toplevel (fn [form _ctx] (form-dispatch form)))
(defmethod compile-toplevel :default [form _ctx]
(throw (compile-error form "Unrecognized form")))
(defmethod compile-toplevel 'defn [form ctx]
(m/match form
(_ (m/pred simple-symbol? ?name) . !fndef ...)
(let [funcref (typecheck-expr (apply list 'fn !fndef) ctx)
exported-funcs (lookup ctx ::u/exported-funcs)]
(bind! ctx ?name {:funcref funcref})
(swap! exported-funcs assoc (name ?name) (last funcref)))))
(defn new-ctx [] [(atom {::u/module-funcs (atom []) ::u/exported-funcs (atom {})})])
(defn compile [forms]
(let [ctx (new-ctx)]
(doseq [form forms] (compile-toplevel form ctx))
(ctx-to-wasm ctx)))

View file

@ -0,0 +1,158 @@
(ns tock.compiler.type
(:require [meander.epsilon :as m]
[tock.compiler.util :refer [bind! compile-error form-dispatch lookup map-subexpressions new-scope] :as u]))
; there are two type syntaxes:
; 1. internal syntax - always stored in a form's metadata under the :type key. fetched with `expr-type`, added by
; `decorate-type`. generally is either a bare symbol, or for parameterized types, a dynamic
; variant stored as a vector, like ['fn type type type].
; 2. input syntax - stored in metadata with ^{} prefix syntax. converted to tock's internal syntax with `syntax-type`.
; generally, type names are stored as {:tag 'name}, and parameterized types like ['enum ...] are
; written in the input as ^{enum [...]}. (if there is only one parameter, the vector is skipped).
(defn syntax-type [metadata _ctx error]
(m/match metadata
(m/pred symbol? ?symbol) ?symbol
{:tag ?symbol} ?symbol
{:fn [!type ... '-> ?return-type]} (apply vector (concat '[fn] !type [?return-type]))
{:fn [!type ...]} (apply vector (concat '[fn] !type '[void]))
_ (when error (throw error))))
(defn expr-type [expr] (:type (meta expr)))
(defn assoc-type [expr symbol] (with-meta expr (assoc (meta expr) :type symbol)))
;; built-in types
(defn make-type [symbol] (fn [expr] (assoc-type expr symbol)))
(def void (make-type 'void))
(def f64 (make-type 'f64))
(def bool (make-type 'bool))
;; typechecking helpers
(defn coerce [expr typesym error]
(let [ltype (expr-type expr)]
(cond (= ltype typesym) expr
(= typesym 'void) (void `(u/cast ~expr))
:else (throw error))))
(defn unify [lexpr rexpr error]
(let [ltype (expr-type lexpr)
rtype (expr-type rexpr)]
(cond (= ltype rtype) [lexpr rexpr]
(nil? error) [(coerce lexpr 'void nil) (coerce rexpr 'void nil)]
:else (throw error))))
;; The typechecker itself!
;; This is currently doing several jobs at once:
;; 1. Convert from input syntax to internal syntax (wrapping literal values in (`lit) so they can have
;; metadata attached)
;; 2. Attach type metadata for each subexpression
;; 3. Validate that the calculated type actually makes sense
;; 4. Create bindings for local variables and function parameters when defined
;; 5. Replace naked references to locals with wrapper syntax specifying exactly where the value comes from
;; (such as (`local index))
;; 6. Mark expressions where returned values need to be explicitly discarded (`cast 'void)
;; todo:
;; - implicitly coerce numeric values when it can be done losslessly
;; External syntax uses simple namespaceless 'symbols, internal syntax uses `symbols
(defmulti decorate-type (fn [form _ctx] (form-dispatch form)))
(defmethod decorate-type 'if [form _ctx]
(m/match form
(_ ?test ?when-true ?when-false)
(let [test-expr (coerce ?test 'bool (compile-error ?test "Condition must be a boolean expression"))
[true-expr false-expr] (unify ?when-true ?when-false nil)
type (expr-type true-expr)]
(assoc-type (list 'if test-expr true-expr false-expr) type))))
(defn equatable-type? [typesym] (contains? #{'f64 'i32 'bool} typesym))
(defn ordered-type? [typesym] (= typesym 'f64))
(defn logical-type? [typesym] (= typesym 'bool))
(defn numerical-type? [typesym] (= typesym 'f64))
(defn decorate-binop [form compute-resulttype verb]
(m/match form
(?op ?left ?right)
(let [[left-expr right-expr] (unify ?left ?right (compile-error ?right (str "Cannot " verb " incompatible types")))]
(if-let [type (compute-resulttype (expr-type left-expr))]
(assoc-type (list ?op left-expr right-expr) type)
(throw (compile-error ?right (str "Cannot " verb " values of type " type " using " ?op)))))))
(defn decorate-comparison [form typecheck]
(decorate-binop form (fn [type] (if (typecheck type) 'bool nil)) "compare"))
(defn decorate-combinator [form typecheck verb]
(decorate-binop form (fn [type] (if (typecheck type) type nil)) verb))
(defmethod decorate-type '= [form _ctx] (decorate-comparison form equatable-type?))
(defmethod decorate-type 'not= [form _ctx] (decorate-comparison form equatable-type?))
(defmethod decorate-type '< [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type '<= [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type '> [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type '>= [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type 'and [form _ctx] (decorate-combinator form logical-type? "accept"))
(defmethod decorate-type 'or [form _ctx] (decorate-combinator form logical-type? "accept"))
(defmethod decorate-type '+ [form _ctx] (decorate-combinator form numerical-type? "add"))
(defmethod decorate-type '- [form _ctx] (decorate-combinator form numerical-type? "subtract"))
(defmethod decorate-type '* [form _ctx] (decorate-combinator form numerical-type? "multiply"))
(defmethod decorate-type '/ [form _ctx] (decorate-combinator form numerical-type? "divide"))
(defmethod decorate-type `u/atom [form ctx]
(cond (integer? form) (f64 `(u/lit ~form))
(boolean? form) (bool `(u/lit ~form))
(simple-symbol? form) (m/match (lookup ctx form)
{:local ?local-id :type ?type} (assoc-type `(u/local ~?local-id) ?type)
{:funcref ?funcref} ?funcref
_ (throw (compile-error form "Undefined symbol")))
:else (throw (compile-error form "Not a valid literal"))))
(defmulti typecheck-expr (fn [form _ctx] (form-dispatch form)))
(defmethod typecheck-expr :default [form ctx]
(decorate-type (map-subexpressions form #(typecheck-expr % ctx)) ctx))
; todo: actually validate types
(defn validate-type [type] type)
; (fn [^type name ^type name -> return-type] body)
(defn alloc-local [ctx]
(swap! (lookup ctx ::u/lastlocal) inc))
; todo: must store function metadata globally
(defn alloc-func [ctx func-type func-body]
(let [funcs (swap! (lookup ctx ::u/module-funcs)
(fn [funcs]
(conj funcs {:type func-type :body func-body})))]
(- (count funcs) 1)))
(defn bind-params! [params ctx]
(let [bind-param! (fn [name type]
(bind! ctx name {:local (alloc-local ctx)
:type (validate-type type)}))
bind-all! (fn [names return-type]
(doseq [name names] (bind-param! name (syntax-type (meta name) ctx (compile-error name "Expected explicit type"))))
(apply vector (concat ['fn]
(map #(syntax-type (meta %) ctx nil) names)
[(syntax-type return-type ctx (compile-error return-type "Invalid return type"))])))]
(m/match params
[!name ... '-> ?return-type] (bind-all! !name ?return-type)
[!name ...] (bind-all! !name 'void))))
(defmethod typecheck-expr 'fn [form ctx]
(let [fn-ctx (new-scope ctx {::u/lastlocal (atom -1)})]
(m/match form
(_ ?params . !body ..1)
(let [fn-type (bind-params! ?params fn-ctx)
return-type (last fn-type)
body-expr (coerce (typecheck-expr (apply list 'do !body) fn-ctx) return-type
(compile-error form ["Expected return type" return-type]))]
(assoc-type (list 'funcref (alloc-func ctx fn-type body-expr)) fn-type)))))
(defmethod typecheck-expr 'do [form ctx]
(m/match form
(_ . !statement ... ?return-expr)
(let [statements (apply list (map (fn [expr] (coerce (typecheck-expr expr ctx) 'void nil)) !statement))
return-expr (typecheck-expr ?return-expr ctx)
return-type (expr-type return-expr)]
(assoc-type (apply list (concat ['do] statements [return-expr])) return-type))))

View file

@ -0,0 +1,30 @@
(ns tock.compiler.util)
(defn compile-error [term message]
; todo: extract location of error term
(js/Error. [term message]))
(defn form-dispatch [form]
(if (list? form) (first form) 'tock.compiler.util/atom))
; not sure about this... in non-special-form cases, all first values are subexpressions
; as well, it's simply not needed for decorate-type, as in order to typecheck any special
; form, you need to explicitly recurse anyway
(defmulti map-subexpressions (fn [form _f] (form-dispatch form)))
(defmethod map-subexpressions :default [form f]
(apply list (first form) (map f (rest form))))
(defmethod map-subexpressions 'tock.compiler.util/atom [form _f] form)
; ctx is a vector of atoms of maps
(defn new-scope
([ctx base] (conj ctx (atom base)))
([ctx] (new-scope ctx {})))
(defn lookup [ctx key]
(loop [scopes (reverse ctx)]
(when (seq scopes)
(let [scope (deref (first scopes))
value (get scope key)]
(if (nil? value) (recur (rest scopes)) value)))))
(defn bind! [ctx key data]
(swap! (last ctx) assoc key data))

View file

@ -0,0 +1,95 @@
(ns tock.compiler.wasm
(:require [helins.wasm :as wasm]
[helins.wasm.ir :as ir]
[helins.wasm.bin :as op]
[helins.binf.string :as binf.string]
[tock.compiler.util :refer [compile-error form-dispatch lookup] :as u]
[tock.compiler.type :refer [expr-type]]
[meander.epsilon :as m]))
(def operator-assembly
{['= 'f64] [[op/f64-eq]]
['= 'i32] [[op/i32-eq]]
['= 'bool] [[op/i32-eq]]
['not= 'f64] [[op/f64-eq] [op/i32-eqz]]
['not= 'i32] [[op/i32-ne]]
['not= 'bool] [[op/i32-ne]]
['< 'f64] [[op/f64-lt]]
['<= 'f64] [[op/f64-le]]
['> 'f64] [[op/f64-gt]]
['>= 'f64] [[op/f64-ge]]
['< 'i32] [[op/i32-lt_s]]
['<= 'i32] [[op/i32-le_s]]
['> 'i32] [[op/i32-gt_s]]
['>= 'i32] [[op/i32-ge_s]]
['and 'bool] [[op/i32-and]]
['or 'bool] [[op/i32-or]]
['+ 'f64] [[op/f64-add]]
['+ 'i32] [[op/i32-add]]
['- 'f64] [[op/f64-sub]]
['- 'i32] [[op/i32-sub]]
['* 'f64] [[op/f64-mul]]
['* 'i32] [[op/i32-mul]]
['/ 'f64] [[op/f64-div]]
['/ 'i32] [[op/i32-div_s]]})
(defn type-to-wasmtype [type]
(m/match type
'f64 op/numtype-f64
'bool op/numtype-i32
'void op/blocktype-nil
['fn . _ ...] op/numtype-i32))
(defn wasm-function-signature [type]
(m/match type
['fn . !types ... ?return-type]
[(apply vector (map type-to-wasmtype !types))
(if (= ?return-type 'void) [] [(type-to-wasmtype ?return-type)])]))
(defmulti emit-code (fn [form] (form-dispatch form)))
(defmethod emit-code :default [form]
(m/match form
((m/and (funcref ?func-id) (m/app meta {:type ?type})) . !params ...)
(concat (mapcat emit-code !params) [[op/call ?func-id]])
(?op . !params ...)
(if-let [ops (get operator-assembly [?op (expr-type form)])]
(concat (mapcat emit-code !params) ops)
(throw (compile-error form ["Don't know how to compile" ?op])))))
(defmethod emit-code `u/local [form]
(m/match form (_ ?local-id) [[op/local-get ?local-id]]))
(defmethod emit-code `u/lit [form]
(m/match form (m/and (_ ?lit) (m/app meta {:type ?type}))
(cond
(= ?type 'i32) [[op/i32-const ?lit]]
(= ?type 'i64) [[op/i64-const ?lit]]
(= ?type 'f64) [[op/f64-const ?lit]]
(= ?type 'bool) [[op/i32-const (if ?lit 1 0)]]
:else (throw (compile-error form "Invalid literal")))))
(defmethod emit-code 'do [form]
(mapcat #(emit-code %) (rest form)))
(defmethod emit-code 'if [form]
(m/match form
(_ ?cond ?when-true ?when-false)
(concat (emit-code ?cond) [[op/if- [:wasm/valtype (type-to-wasmtype (expr-type form))] (emit-code ?when-true) (emit-code ?when-false)]])))
(defn ctx-to-wasm [ctx]
(let [funcs (deref (lookup ctx ::u/module-funcs))
exported-funcs (deref (lookup ctx ::u/exported-funcs))]
(pr "generating" funcs exported-funcs)
(as-> (wasm/ctx) wasm
(reduce (fn [wasm i]
(let [{:keys [body type]} (get funcs i)]
(-> wasm
(ir/assoc-type (ir/type-signature {} (wasm-function-signature type)))
(ir/assoc-func (ir/func {} i))
(assoc-in [:wasm/codesec i] (ir/func' {} [] (apply vector (emit-code body)))))))
wasm
(range (count funcs)))
(reduce (fn [wasm [name funcid]]
(assoc-in wasm [:wasm/exportsec :wasm.export/func funcid] [(ir/export' {} (binf.string/encode name))]))
wasm
exported-funcs))))

View file

@ -1,260 +1,14 @@
(ns tock.experiment
(:require [helins.wasm :as wasm]
[helins.wasm.ir :as ir]
[helins.wasm.bin :as op]
[helins.binf :as binf]
[helins.binf.string :as binf.string]
[meander.epsilon :as m]))
[tock.compiler :refer [compile]]))
(defn compile-error [term message]
; todo: extract location of error term
(js/Error. [term message]))
; typechecking pass: rewrite expressions with type decorations, depth-first.
; I can't think of any reason this shouldn't work fine, as long as we require
; functions to have explicit type annotations
; first task: decorate types for (if (= 1 2) 3 4)
; ^f64 (if ^bool (= ^f64 (lit 1) ^f64 (lit 2)) ^f64 (lit 3) ^f64 (lit 4))
(defn expr-type [expr] (:type (meta expr)))
(defn assoc-type [expr symbol] (with-meta expr (assoc (meta expr) :type symbol)))
(defn make-type [symbol] (fn [expr] (assoc-type expr symbol)))
(def void (make-type 'void))
(def f64 (make-type 'f64))
(def bool (make-type 'bool))
(defn coerce [expr typesym error]
(let [ltype (expr-type expr)]
(cond (= ltype typesym) expr
(= typesym 'void) (void `(cast ~expr))
:else (throw error))))
(defn unify [lexpr rexpr error]
(let [ltype (expr-type lexpr)
rtype (expr-type rexpr)]
(cond (= ltype rtype) [lexpr rexpr]
(nil? error) [(coerce lexpr 'void nil) (coerce rexpr 'void nil)]
:else (throw error))))
(defn form-dispatch [form]
(if (list? form) (first form) `atom))
; not sure about this... in non-special-form cases, all first values are subexpressions
; as well, it's simply not needed for decorate-type, as in order to typecheck any special
; form, you need to explicitly recurse anyway
(defmulti map-subexpressions (fn [form _f] (form-dispatch form)))
(defmethod map-subexpressions :default [form f]
(apply list (first form) (map f (rest form))))
(defmethod map-subexpressions `atom [form _f] form)
; ctx is a vector of atoms of maps
(defn new-scope
([ctx base] (conj ctx (atom base)))
([ctx] (new-scope ctx {})))
(defn lookup [ctx key]
(loop [scopes (reverse ctx)]
(when (seq scopes)
(let [scope (deref (first scopes))
value (get scope key)]
(if (nil? value) (recur (rest scopes)) value)))))
(defn bind! [ctx key data]
(swap! (last ctx) assoc key data))
(defmulti decorate-type (fn [form _ctx] (form-dispatch form)))
(defmethod decorate-type 'if [form _ctx]
(m/match form
(_ ?test ?when-true ?when-false)
(let [test-expr (coerce ?test 'bool (compile-error ?test "Condition must be a boolean expression"))
[true-expr false-expr] (unify ?when-true ?when-false nil)
type (expr-type true-expr)]
(assoc-type (list 'if test-expr true-expr false-expr) type))))
(defn equatable-type? [typesym] (contains? #{'f64 'bool} typesym))
(defn ordered-type? [typesym] (= typesym 'f64))
(defn logical-type? [typesym] (= typesym 'bool))
(defn numerical-type? [typesym] (= typesym 'f64))
(defn decorate-binop [form compute-resulttype verb]
(m/match form
(?op ?left ?right)
(let [[left-expr right-expr] (unify ?left ?right (compile-error ?right (str "Cannot " verb " incompatible types")))]
(if-let [type (compute-resulttype (expr-type left-expr))]
(assoc-type (list ?op left-expr right-expr) type)
(throw (compile-error ?right (str "Cannot " verb " values of type " type " using " ?op)))))))
(defn decorate-comparison [form typecheck]
(decorate-binop form (fn [type] (if (typecheck type) 'bool nil)) "compare"))
(defn decorate-combinator [form typecheck verb]
(decorate-binop form (fn [type] (if (typecheck type) type nil)) verb))
(defmethod decorate-type '= [form _ctx] (decorate-comparison form equatable-type?))
(defmethod decorate-type 'not= [form _ctx] (decorate-comparison form equatable-type?))
(defmethod decorate-type '< [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type '<= [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type '> [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type '>= [form _ctx] (decorate-comparison form ordered-type?))
(defmethod decorate-type 'and [form _ctx] (decorate-combinator form logical-type? "accept"))
(defmethod decorate-type 'or [form _ctx] (decorate-combinator form logical-type? "accept"))
(defmethod decorate-type '+ [form _ctx] (decorate-combinator form numerical-type? "add"))
(defmethod decorate-type `atom [form ctx]
(cond (integer? form) (f64 `(lit ~form))
(boolean? form) (bool `(lit ~form))
(simple-symbol? form) (m/match (lookup ctx form)
{:local ?local-id :type ?type} (assoc-type `(local ~?local-id) ?type)
{:funcref ?funcref} ?funcref
_ (throw (compile-error form "Undefined symbol")))
:else (throw (compile-error form "Not a valid literal"))))
(defmulti typecheck-expr (fn [form _ctx] (form-dispatch form)))
(defmethod typecheck-expr :default [form ctx]
(decorate-type (map-subexpressions form #(typecheck-expr % ctx)) ctx))
; todo: actually validate types
(defn validate-type [type] type)
; (fn [name type name type return-type] body)
(defn alloc-local [ctx]
(swap! (lookup ctx ::lastlocal) inc))
; todo: must store function metadata globally
(defn alloc-func [ctx func-type func-body]
(let [funcs (swap! (lookup ctx ::module-funcs)
(fn [funcs]
(conj funcs {:type func-type :body func-body})))]
(- (count funcs) 1)))
(defn bind-params! [params ctx]
(let [bind-param! (fn [name type]
(bind! ctx name {:local (alloc-local ctx)
:type (validate-type type)}))
bind-all! (fn [names types return-type]
(doseq [[name type] (map vector names types)] (bind-param! name type))
(apply vector (concat ['fn] types [return-type])))]
(m/match params
[!name !type ... ?return-type] (bind-all! !name !type ?return-type)
[!name !type ...] (bind-all! !name !type 'void))))
(defmethod typecheck-expr 'fn [form ctx]
(let [fn-ctx (new-scope ctx {::lastlocal (atom -1)})]
(m/match form
(_ ?params . !body ..1)
(let [fn-type (bind-params! ?params fn-ctx)
return-type (last fn-type)
body-expr (coerce (typecheck-expr (apply list 'do !body) fn-ctx) return-type
(compile-error form ["Expected return type" return-type]))]
(assoc-type (list 'funcref (alloc-func ctx fn-type body-expr)) fn-type)))))
(defmethod typecheck-expr 'do [form ctx]
(m/match form
(_ . !statement ... ?return-expr)
(let [statements (apply list (map (fn [expr] (coerce (typecheck-expr expr ctx) 'void nil)) !statement))
return-expr (typecheck-expr ?return-expr ctx)
return-type (expr-type return-expr)]
(assoc-type (apply list (concat ['do] statements [return-expr])) return-type))))
(defmulti compile-toplevel (fn [form _ctx] (form-dispatch form)))
(defmethod compile-toplevel :default [form _ctx]
(throw (compile-error form "Unrecognized form")))
(defmethod compile-toplevel 'defn [form ctx]
(m/match form
(_ (m/pred simple-symbol? ?name) . !fndef ...)
(let [funcref (typecheck-expr (apply list 'fn !fndef) ctx)
exported-funcs (lookup ctx ::exported-funcs)]
(bind! ctx ?name {:funcref funcref})
(swap! exported-funcs assoc (name ?name) (last funcref)))))
(defn new-ctx [] [(atom {::module-funcs (atom []) ::exported-funcs (atom {})})])
(def operator-assembly
{['= 'f64] [[op/f64-eq]]
['= 'bool] [[op/i32-eq]]
['not= 'f64] [[op/f64-eq] [op/i32-eqz]]
['not= 'bool] [[op/i32-ne]]
['< 'f64] [[op/f64-lt]]
['<= 'f64] [[op/f64-le]]
['> 'f64] [[op/f64-gt]]
['>= 'f64] [[op/f64-ge]]
['and 'bool] [[op/i32-and]]
['or 'bool] [[op/i32-or]]
['+ 'f64] [[op/f64-add]]})
(defmulti emit-code (fn [form] (form-dispatch form)))
(defmethod emit-code :default [form]
(m/match form
((m/and (funcref ?func-id) (m/app meta {:type ?type})) . !params ...)
(concat (mapcat emit-code !params) [[op/call ?func-id]])
(?op . !params ...)
(if-let [ops (get operator-assembly [?op (expr-type form)])]
(concat (mapcat emit-code !params) ops)
(throw (compile-error form ["Don't know how to compile" ?op])))))
(defmethod emit-code `local [form]
(m/match form (_ ?local-id) [[op/local-get ?local-id]]))
(defmethod emit-code `lit [form]
(m/match form (m/and (_ ?lit) (m/app meta {:type ?type}))
(cond
(= ?type 'f64) [[op/f64-const ?lit]]
(= ?type 'bool) [[op/i32-const (if ?lit 0xffff 0)]]
:else (throw (compile-error form "Invalid literal")))))
(defmethod emit-code 'do [form]
(mapcat #(emit-code %) (rest form)))
(defn type-to-wasmtype [type]
(m/match type
'f64 op/numtype-f64
'bool op/numtype-i32
['fn . !types ... ?return-type]
[(apply vector (map type-to-wasmtype !types))
(if (= ?return-type 'void) [] [(type-to-wasmtype ?return-type)])]))
(defn ctx-to-wasm [ctx]
(let [funcs (deref (lookup ctx ::module-funcs))
exported-funcs (deref (lookup ctx ::exported-funcs))]
(pr "generating" funcs exported-funcs)
(as-> (wasm/ctx) wasm
(reduce (fn [wasm i]
(let [{:keys [body type]} (get funcs i)]
(-> wasm
(ir/assoc-type (ir/type-signature {} (type-to-wasmtype type)))
(ir/assoc-func (ir/func {} i))
(assoc-in [:wasm/codesec i] (ir/func' {} [] (emit-code body))))))
wasm
(range (count funcs)))
(reduce (fn [wasm [name funcid]]
(assoc-in wasm [:wasm/exportsec :wasm.export/func funcid] [(ir/export' {} (binf.string/encode name))]))
wasm
exported-funcs))))
(defn compile [forms]
(let [ctx (new-ctx)]
(doseq [form forms] (compile-toplevel form ctx)) ; todo: top-level compiler
(ctx-to-wasm ctx)))
(binding [*print-meta* true]
(let [ctx (new-ctx)
form '(fn [x f64 y f64 bool] (< x y))]
(pr (typecheck-expr form ctx))
(pr (deref (first ctx)))))
;; ; https://github.com/kalai-transpiler/kalai
;; (def test-wasm
;; (-> (wasm/ctx)
;; (ir/assoc-type (ir/type-signature {} [[op/numtype-i32 op/numtype-i32] [op/numtype-i32]]))
;; (ir/assoc-func (ir/func {} 0))
;; (assoc-in [:wasm/codesec 0] (ir/func' {} [] [[op/local-get 0] [op/local-get 1] [op/i32-add]]))
;; (assoc-in [:wasm/exportsec :wasm.export/func 0] [(ir/export' {} (binf.string/encode "add"))])))
(def test-wasm
(compile
'[(defn add [left f64 right f64 f64] (+ left right))]))
'[(defn thing [^bool op ^f64 val -> f64] (if op (+ val 1) (* val 2)))]))
(defn decompile-url [url]
(-> (js/fetch url)
@ -272,6 +26,6 @@
(defn main []
(js/console.log test-wasm)
(-> (instantiate-wasm test-wasm #js {})
(.then #(js/console.log (-> % (.-instance) (.-exports) (.add 1 2)))))
(.then #(js/console.log (-> % (.-instance) (.-exports) (.thing false 2)))))
(-> (decompile-url "release.wasm")
(.then #(js/console.log (-> % :wasm/exportsec :wasm.export/func (get 0))))))