diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cljlib.fnl | 3341 |
1 files changed, 3341 insertions, 0 deletions
diff --git a/src/cljlib.fnl b/src/cljlib.fnl new file mode 100644 index 0000000..8ce0733 --- /dev/null +++ b/src/cljlib.fnl @@ -0,0 +1,3341 @@ +;;; reduced + +(set package.preload.reduced + (or package.preload.reduced + ;; https://gitlab.com/andreyorst/reduced.lua + #(let [Reduced + {:__fennelview + (fn [[x] view options indent] + (.. "#<reduced: " (view x options (+ 11 indent)) ">")) + :__index {:unbox (fn [[x]] x)} + :__name :reduced + :__tostring (fn [[x]] (.. "reduced: " (tostring x)))}] + (fn reduced [value] + "Wrap `value` as an instance of the Reduced object. +Reduced will terminate the `reduce` function, if it supports this kind +of termination." + (setmetatable [value] Reduced)) + (fn reduced? [value] + "Check if `value` is an instance of Reduced." + (rawequal (getmetatable value) Reduced)) + {:is_reduced reduced? : reduced :reduced? reduced?}))) + +;;; itable + +(set package.preload.itable + (or package.preload.itable + (fn [] +;;;###include itable/src/itable.fnl + ))) + +;;; lazy-seq + +(set package.preload.lazy-seq + (or package.preload.lazy-seq + (fn [] +;;;###include lazy-seq/lazy-seq.fnl + ))) + +;;; cljlib + +(eval-compiler + (local lib-name (or ... :cljlib)) + + (fn string? [x] + (= :string (type x))) + + (fn has? [tbl sym] + ;; searches for the given symbol in a table. + (var has false) + (each [_ elt (ipairs tbl) :until has] + (set has (= sym elt))) + has) + + ;; ns + + (local cljlib-namespaces + {} + ;; A map of files and their respective namespaces. Each entry is a + ;; filename followed by a table with two keys: `:current` and + ;; `:known`. The second one holds all namespaces that were defined + ;; for the file via the `ns` macro, and thus are available to switch + ;; with the `in-ns` macro. The `:current` key represents currently + ;; active namespace that is used for binding via the `def` macro and + ;; its derivatives. + ) + + (fn current-file [ast] + (. (ast-source ast) :filename)) + + (fn create-ns [name] + (let [file (current-file name)] + (when (not (. cljlib-namespaces file)) + (tset cljlib-namespaces file {:known {}})) + (tset cljlib-namespaces file :current name) + (tset cljlib-namespaces file :known (tostring name) true)) + `(setmetatable + {} + {:__name "namespace" + :__fennelview #(do ,(: "#<namespace: %s>" :format (tostring name)))})) + + (fn known-ns? [name] + (let [file (current-file name)] + (?. cljlib-namespaces file :known (tostring name)))) + + (fn current-ns [ast] + (?. cljlib-namespaces (current-file ast) :current)) + + (fn in-ns [name] + "Sets the compile-time variable `cljlib-namespaces` to the given `name`. +Affects such macros as `def`, `defn`, which will bind names to the +specified namespace. + +# Examples +Creating several namespaces in the file, and defining functions in each: + +``` fennel +(ns a) +(defn f [] \"f from a\") +(ns b) +(defn f [] \"f from b\") +(in-ns a) +(defn g [] \"g from a\") +(in-ns b) +(defn g [] \"g from b\") + +(assert-eq (a.f) \"f from a\") +(assert-eq (b.f) \"f from b\") +(assert-eq (a.g) \"g from a\") +(assert-eq (b.g) \"g from b\") +``` + +Note, switching namespaces in the REPL doesn't affect non-namespaced +local bindings. In other words, when defining a local with `def`, a +bot a local binding and a namespaced binding are created, and +switching current namespace won't change the local binding: + +``` fennel :skip-test +>> (ns foo) +nil +>> (def x 42) +nil +>> (ns bar) +nil +>> (def x 1337) +nil +>> (in-ns foo) +#<namespace: foo> +>> x ; user might have expected to see 42 here +1337 +>> foo.x +42 +>> bar.x +1337 +``` + +Sadly, Fennel itself has no support for namespace switching in REPL, +so this feature can be only partially emulated by the cljlib library. +" + (assert-compile (known-ns? name) + (: "no such namespace: %s" :format (tostring name)) + name) + (tset cljlib-namespaces (current-file name) :current name) + name) + + (fn ns [name commentary requirements] + "Namespace declaration macro. +Accepts the `name` of the generated namespace, and creates a local +variable with this name holding a table. Optionally accepts +`commentary` describing what namespace is about and a `requirements` +spec, specifying what libraries should be required. + +The `requirements` spec is a list that consists of vectors, specifying +library name and a possible alias or a vector of names to refer to +without a prefix: + +``` fennel :skip-test +(ns some-namespace + \"Description of the some-namespace.\" + (:require [some.lib] + [some.other.lib :as lib2] + [another.lib :refer [foo bar baz]])) + +(defn inc [x] (+ x 1)) +``` + +Which is equivalent to: + +``` fennel :skip-test +(local some-namespace {}) +(local lib (require :some.lib)) +(local lib2 (require :some.other.lib)) +(local {:bar bar :baz baz :foo foo} (require :another.lib)) +(comment \"Description of the some-namespace.\") +``` + +Note that when no `:as` alias is given, the library will be named +after the innermost part of the require path, i.e. `some.lib` is +transformed to `lib`. + +See `in-ns` on how to switch namespaces." + (let [bind-table [name] + require-table [(create-ns name)] + requirements (if (string? commentary) + requirements + commentary)] + (match requirements + [:require & requires] + (each [_ spec (ipairs requires)] + (match spec + (where (or [module :as alias :refer names] + [module :refer names :as alias])) + (do (table.insert bind-table (collect [_ name (ipairs names) :into {'&as alias}] + (values (tostring name) name))) + (table.insert require-table `(require ,(tostring module)))) + [module :as alias] + (do (table.insert bind-table alias) + (table.insert require-table `(require ,(tostring module)))) + [module :refer names] + (do (table.insert bind-table (collect [_ name (ipairs names)] + (values (tostring name) name))) + (table.insert require-table `(require ,(tostring module)))) + [module] + (do (->> (string.gsub (tostring module) ".+%.(.-)$" "%1") + (pick-values 1) + sym + (table.insert bind-table)) + (table.insert require-table `(require ,(tostring module)))) + _ (assert-compile false "wrong require syntax" name))) + nil nil + _ (assert-compile false "wrong require syntax" name)) + (if (string? commentary) + `(local ,bind-table + (values ,require-table (comment ,commentary))) + `(local ,bind-table ,require-table)))) + + ;; def + + (fn def [...] + "Name binding macro similar to `local` but acts in terms of current +namespace set with the `ns` macro, unless `:private` was passed before +the binding name. Accepts the `name` to be bound and the `initializer` +expression. `meta` can be either an associative table where keys are +strings, or a string representing a key from the table. If a sole +string is given, its value is set to `true` in the meta table." + {:fnl/arglist [([name initializer]) ([meta name initializer])]} + (match [...] + (where (or [:private name val] + [{:private true} name val])) + `(local ,name ,val) + [name val] + (let [namespace (current-ns name)] + (if (in-scope? namespace) + `(local ,name + (let [v# ,val] + (tset ,namespace ,(tostring name) v#) + v#)) + `(local ,name ,val))))) + + ;; defn + + (local errors + {:vararg "... is't allowed in the arglist, use & destructuring" + :same-arity "Can't have 2 overloads with same arity" + :arity-order "Overloads must be sorted by arities" + :amp-arity "Variadic overload must be the last overload" + :extra-rest-args "Only one argument allowed after &" + :wrong-arg-amount "Wrong number of args (%s) passed to %s" + :extra-amp "Can't have more than 1 variadic overload"}) + + (fn first [[x]] x) + (fn rest [[_ & xs]] xs) + (fn vfirst [x] x) + (fn vrest [_ ...] ...) + + (fn length* [arglist] + ;; Gets "length" of variadic arglist, stopping at first & plus 1 arg. + ;; Additionally checks whether there are more than one arg after &. + (var (l amp? n) (values 0 false nil)) + (each [i arg (ipairs arglist) :until amp?] + (if (= arg '&) + (set (amp? n) (values true i)) + (set l (+ l 1)))) + (when n + (assert-compile (= (length arglist) (+ n 1)) + errors.extra-rest-args + (. arglist (length arglist)))) + (if amp? (+ l 1) l)) + + (fn check-arglists [arglists] + ;; performs a check that arglists are ordered correctly, and that + ;; only one of multiarity arglists has the & symbol, additionally + ;; checking for a presence of the multiple-values symbol. + (var (size amp?) (values -1 false)) + (each [_ [arglist] (ipairs arglists)] + (assert-compile (not (has? arglist '...)) errors.vararg arglist) + (let [len (length* arglist)] + (assert-compile (not= size len) errors.same-arity arglist) + (assert-compile (< size len) errors.arity-order arglist) + (assert-compile (not amp?) (if (has? arglist '&) + errors.extra-amp + errors.amp-arity) arglist) + (set size len) + (set amp? (has? arglist '&))))) + + (fn with-immutable-rest [arglist body] + `(let [core# (require ,lib-name) + ,arglist (core#.list ...)] + ,(unpack body))) + + (fn add-missing-arities! [arglists name] + "Adds missing arity overloads for given `arglists`. +For example, given the [[[a] body] [[a b c] body]], will generate +[[[] error] + [[a] body] + [[arg_1_ arg_2_] error] + [[a b c] body]] + +Because inital arglist didn't specify arities of 0 and 2." + (for [i (- (length* arglists) 1) 1 -1] + (let [current-args (first (. arglists i)) + current-len (length* current-args) + next-args (first (. arglists (+ i 1))) + next-len (length* next-args) + next-len (if (has? next-args '&) (- next-len 1) next-len)] + (when (not= (+ current-len 1) next-len) + (for [len (- next-len 1) (+ current-len 1) -1] + (table.insert arglists (+ i 1) [(fcollect [i 1 len :into {:fake true}] (gensym :arg)) + `(error (: ,errors.wrong-arg-amount :format ,len ,(tostring name)))]))))) + (while (not= 0 (length* (first (first arglists)))) + (let [len (- (length* (first (first arglists))) 1)] + (table.insert arglists 1 [(fcollect [i 1 len :into {:fake true}] (gensym :arg)) + `(error (: ,errors.wrong-arg-amount :format ,len ,(tostring name)))])))) + + ;; TODO: implement pre-post conditions + (fn gen-match-fn [name doc arglists] + ;; automated multi-arity dispatch generator + (check-arglists arglists) + (add-missing-arities! arglists name) + (let [match-body `(match (select :# ...))] + (var variadic? false) + (each [_ [arglist & body] (ipairs arglists)] + (table.insert match-body (if (has? arglist '&) + (do (set variadic? true) (sym :_)) + (length arglist))) + (table.insert match-body (if variadic? + (with-immutable-rest arglist body) + (if (and (> (length arglist) 0) (not arglist.fake)) + `(let [(,(unpack arglist)) (values ...)] + ,(if (> (length body) 0) + (unpack body) + 'nil)) + `(do ,(unpack body)))))) + (when (not variadic?) + (table.insert match-body (sym :_)) + (table.insert match-body + `(error (: ,errors.wrong-arg-amount :format ,(sym :_) ,(tostring name))))) + `(fn ,name [...] + {:fnl/docstring ,doc + :fnl/arglist ,(icollect [_ [arglist] (ipairs arglists)] + (when (not arglist.fake) + (list (sequence (unpack arglist)))))} + ,match-body))) + + ;; TODO: implement pre-post conditions + (fn gen-fn [name doc arglist _pre-post body] + (check-arglists [[arglist]]) + `(fn ,name [...] + {:fnl/docstring ,doc + :fnl/arglist ,(sequence arglist)} + ,(if (has? arglist '&) + (with-immutable-rest arglist [body]) + `(let ,(if (> (length arglist) 0) + `[(,(unpack arglist)) (values ...)] + `[]) + (let [cnt# (select "#" ...)] + (when (not= ,(length arglist) cnt#) + (error (: ,errors.wrong-arg-amount :format cnt# ,(tostring name))))) + ,body)))) + + (fn fn* [...] + "Clojure-inspired `fn' macro for defining functions. +Accepts an optional `name` and `docstring?`, followed by the binding +list containing function's `params*`. The `body` is wrapped in an +implicit `do`. The `doc-string?` argument specifies an optional +documentation for the function. Supports multi-arity dispatching via +the following syntax: + +(fn* optional-name + optional-docstring + ([arity1] body1) + ([other arity2] body2)) + +Accepts `pre-post?` conditions in a form of a table after argument +list: + +(fn* optional-name + optional-docstring + [arg1 arg2] + {:pre [(check1 arg1 arg2) (check2 arg1)] + :post [(check1 $) ... (checkN $)]} + body) + +The same syntax applies to multi-arity version. + +(pre- and post-checks are not yet implemented)" + {:fnl/arglist [([name doc-string? [params*] pre-post? body]) + ([name doc-string? ([params*] pre-post? body)+])]} + (let [{: name? : doc? : args : pre-post? : body : multi-arity?} + ;; descent into maddness + (match (values ...) + (where (name docstring [[] &as arity]) + (and (sym? name) + (string? docstring) + (list? arity))) + {:pat '(fn* foo "bar" ([baz]) ...) + :name? name + :doc? docstring + :args [arity (select 4 ...)] + :multi-arity? true} + (where (name [[] &as arity]) + (and (sym? name) + (list? arity))) + {:pat '(fn* foo ([baz]) ...) + :name? name + :args [arity (select 3 ...)] + :multi-arity? true} + (where (docstring [[] &as arity]) + (and (string? docstring) + (list? arity))) + {:pat '(fn* "bar" ([baz]) ...) + :name? (gensym :fn) + :doc? docstring + :args [arity (select 3 ...)] + :multi-arity? true} + (where ([[] &as arity]) + (list? arity)) + {:pat '(fn* ([baz]) ...) + :name? (gensym :fn) + :args [arity (select 2 ...)] + :multi-arity? true} + (where (name docstring args {&as pre-post}) + (and (sym? name) + (string? docstring) + (sequence? args) + (or (not= nil pre-post.pre) + (not= nil pre-post.post)))) + {:pat '(fn* foo "foo" [baz] {:pre qux :post quux} ...) + :name? name + :doc? docstring + :args args + :pre-post? pre-post + :body [(select 5 ...)]} + (where (name docstring args) + (and (sym? name) + (string? docstring) + (sequence? args))) + {:pat '(fn* foo "foo" [baz] ...) + :name? name + :doc? docstring + :args args + :body [(select 4 ...)]} + (where (name args {&as pre-post}) + (and (sym? name) + (sequence? args) + (or (not= nil pre-post.pre) + (not= nil pre-post.post)))) + {:pat '(fn* foo [baz] {:pre qux :post quux} ...) + :name? name + :args args + :pre-post? pre-post + :body [(select 4 ...)]} + (where (name args) + (and (sym? name) (sequence? args))) + {:pat '(fn* foo [baz] ...) + :name? name + :args args + :body [(select 3 ...)]} + (where (docstring args {&as pre-post}) + (and (string? docstring) + (sequence? args) + (or (not= nil pre-post.pre) + (not= nil pre-post.post)))) + {:pat '(fn* "bar" [baz] {:pre qux :post quux} ...) + :name? (gensym :fn) + :doc? docstring + :args args + :pre-post? pre-post + :body [(select 4 ...)]} + (where (docstring args) + (and (string? docstring) + (sequence? args))) + {:pat '(fn* "bar" [baz] ...) + :name? (gensym :fn) + :doc? docstring + :args args + :body [(select 3 ...)]} + (where (args {&as pre-post}) + (and (sequence? args) + (or (not= nil pre-post.pre) + (not= nil pre-post.post)))) + {:pat '(fn* [baz] {:pre qux :post quux} ...) + :name? (gensym :fn) + :args args + :pre-post? pre-post + :body [(select 3 ...)]} + (where (args) + (sequence? args)) + {:pat '(fn* [baz] ...) + :name? (gensym :fn) + :args args + :body [(select 2 ...)]} + _ (assert-compile (string.format + "Expression %s didn't match any pattern." + (view `(fn* ,...)))))] + (if multi-arity? + (gen-match-fn name? doc? args) + (gen-fn name? doc? args pre-post? `(do ,(unpack body)))))) + + (fn defn [name ...] + "Same as `(def name (fn* name docstring? [params*] pre-post? exprs*))` +or `(def name (fn* name docstring? ([params*] pre-post? exprs*)+))` +with any doc-string or attrs added to the function metadata. Accepts +`name` which will be used to refer to a function in the current +namespace, and optional `doc-string?`, a vector of function's +`params*`, `pre-post?` conditions, and the `body` of the function. +The body is wrapped in an implicit do. See `fn*` for more info." + {:fnl/arglist [([name doc-string? [params*] pre-post? body]) + ([name doc-string? ([params*] pre-post? body)+])]} + (assert-compile (sym? name) "expected a function name, use `fn*` for anonymous functions" name) + (def name (fn* name ...))) + + (fn defn- [name ...] + "Same as `(def :private name (fn* name docstring? [params*] pre-post? +exprs*))` or `(def :private name (fn* name docstring? ([params*] +pre-post? exprs*)+))` with any doc-string or attrs added to the +function metadata. Accepts `name` which will be used to refer to a +function, and optional `doc-string?`, a vector of function's +`params*`, `pre-post?` conditions, and the `body` of the function. +The body is wrapped in an implicit do. See `fn*` for more info." + {:fnl/arglist [([name doc-string? [params*] pre-post? body]) + ([name doc-string? ([params*] pre-post? body)+])]} + (assert-compile (sym? name) "expected a function name, use `fn*` for anonymous functions" name) + (def :private name (fn* name ...))) + + ;; Time + + (fn time [expr] + "Measure the CPU time spent executing `expr`." + `(let [c# os.clock + pack# #(doto [$...] (tset :n (select "#" $...))) + s# (c#) + res# (pack# ,expr) + e# (c#)] + (print (.. "Elapsed time: " (* (- e# s#) 1000) " msecs")) + ((or table.unpack _G.unpack) res# 1 res#.n))) + + ;; let variants + + (fn when-let [[name test] ...] + "When `test` is logical `true`, evaluates the `body` with `name` bound +to the value of `test`." + {:fnl/arglist [[name test] & body]} + `(let [val# ,test] + (if val# + (let [,name val#] + ,...)))) + + (fn if-let [[name test] if-branch else-branch ...] + "When `test` is logical `true`, evaluates the `if-branch` with `name` +bound to the value of `test`. Otherwise, evaluates the `else-branch`" + {:fnl/arglist [[name test] if-branch else-branch]} + (assert-compile (= 0 (select "#" ...)) "too many arguments to if-let" ...) + `(let [val# ,test] + (if val# + (let [,name val#] + ,if-branch) + ,else-branch))) + + (fn when-some [[name test] ...] + "When `test` is not `nil`, evaluates the `body` with `name` bound to +the value of `test`." + {:fnl/arglist [[name test] & body]} + `(let [val# ,test] + (if (not= nil val#) + (let [,name val#] + ,...)))) + + (fn if-some [[name test] if-branch else-branch ...] + "When `test` is not `nil`, evaluates the `if-branch` with `name` +bound to the value of `test`. Otherwise, evaluates the `else-branch`" + {:fnl/arglist [[name test] if-branch else-branch]} + (assert-compile (= 0 (select "#" ...)) "too many arguments to if-some" ...) + `(let [val# ,test] + (if (not= nil val#) + (let [,name val#] + ,if-branch) + ,else-branch))) + + ;; Multimethods + + (fn defmulti [...] + "Create multifunction `name' with runtime dispatching based on results +from `dispatch-fn'. Returns a proxy table with `__call` metamethod, +that calls `dispatch-fn' on its arguments. Amount of arguments +passed, should be the same as accepted by `dispatch-fn'. Looks for +multimethod based on result from `dispatch-fn'. + +Accepts optional `docstring?', and `options*' arguments, where +`options*' is a sequence of key value pairs representing additional +attributes. Supported options: + +`:default` - the default dispatch value, defaults to `:default`. + +By default, multifunction has no multimethods, see +`defmethod' on how to add one." + {:fnl/arglist [name docstring? dispatch-fn options*]} + (let [[name & options] (if (> (select :# ...) 0) [...] + (error "wrong argument amount for defmulti")) + docstring (if (string? (first options)) (first options)) + options (if docstring (rest options) options) + dispatch-fn (first options) + options* (rest options)] + (assert (= (% (length options*) 2) 0) "wrong argument amount for defmulti") + (let [options {}] + (for [i 1 (length options*) 2] + (tset options (. options* i) (. options* (+ i 1)))) + (def name + `(let [pairs# (fn [t#] + (match (getmetatable t#) + {:__pairs p#} (p# t#) + ,(sym :_) (pairs t#))) + {:eq eq#} (require ,lib-name)] + (setmetatable + {} + {:__index (fn [t# key#] + (accumulate [res# nil + k# v# (pairs# t#) + :until res#] + (when (eq# k# key#) + v#))) + :__call + (fn [t# ...] + ,docstring + (let [dispatch-value# (,dispatch-fn ...) + view# (match (pcall require :fennel) + (true fennel#) #(fennel#.view $ {:one-line true}) + ,(sym :_) tostring)] + ((or (. t# dispatch-value#) + (. t# (or (. ,options :default) :default)) + (error (.. "No method in multimethod '" + ,(tostring name) + "' for dispatch value: " + (view# dispatch-value#)) + 2)) ...))) + :__name (.. "multifn " ,(tostring name)) + :__fennelview tostring + :cljlib/type :multifn})))))) + + (fn defmethod [multifn dispatch-val ...] + "Attach new method to multi-function dispatch value. Accepts the +`multi-fn' as its first argument, the `dispatch-value' as second, and +`fnspec' - a function tail starting from argument list, followed by +function body as in `fn*'. + +# Examples +Here are some examples how multimethods can be used. + +## Factorial example +Key idea here is that multimethods can call itself with different +values, and will dispatch correctly. Here, `fac' recursively calls +itself with less and less number until it reaches `0` and dispatches +to another multimethod: + +``` fennel +(ns test) + +(defmulti fac (fn [x] x)) + +(defmethod fac 0 [_] 1) +(defmethod fac :default [x] (* x (fac (- x 1)))) + +(assert-eq (fac 4) 24) +``` + +`:default` is a special method which gets called when no other methods +were found for given dispatch value. + +## Multi-arity dispatching +Multi-arity function tails are also supported: + +``` fennel +(ns test) + +(defmulti foo (fn* ([x] [x]) ([x y] [x y]))) + +(defmethod foo [10] [_] (print \"I knew I'll get 10\")) +(defmethod foo [10 20] [_ _] (print \"I knew I'll get both 10 and 20\")) +(defmethod foo :default ([x] (print (.. \"Umm, got\" x))) + ([x y] (print (.. \"Umm, got both \" x \" and \" y)))) +``` + +Calling `(foo 10)` will print `\"I knew I'll get 10\"`, and calling +`(foo 10 20)` will print `\"I knew I'll get both 10 and 20\"`. +However, calling `foo' with any other numbers will default either to +`\"Umm, got x\"` message, when called with single value, and `\"Umm, got +both x and y\"` when calling with two values. + +## Dispatching on object's type +We can dispatch based on types the same way we dispatch on values. +For example, here's a naive conversion from Fennel's notation for +tables to Lua's one: + +``` fennel +(ns test) + +(defmulti to-lua-str (fn [x] (type x))) + +(defmethod to-lua-str :number [x] (tostring x)) +(defmethod to-lua-str :table [x] + (let [res []] + (each [k v (pairs x)] + (table.insert res (.. \"[\" (to-lua-str k) \"] = \" (to-lua-str v)))) + (.. \"{\" (table.concat res \", \") \"}\"))) +(defmethod to-lua-str :string [x] (.. \"\\\"\" x \"\\\"\")) +(defmethod to-lua-str :default [x] (tostring x)) + +(assert-eq (to-lua-str {:a {:b 10}}) \"{[\\\"a\\\"] = {[\\\"b\\\"] = 10}}\") + +(assert-eq (to-lua-str [:a :b :c [:d {:e :f}]]) + \"{[1] = \\\"a\\\", [2] = \\\"b\\\", [3] = \\\"c\\\", [4] = {[1] = \\\"d\\\", [2] = {[\\\"e\\\"] = \\\"f\\\"}}}\") +``` + +And if we call it on some table, we'll get a valid Lua table, which we +can then reformat as we want and use in Lua. + +All of this can be done with functions, and single entry point +function, that uses if statement and branches on the type, however one +of the additional features of multimethods, is that separate libraries +can extend such multimethod by adding additional claues to it without +needing to patch the source of the function. For example later on +support for userdata or coroutines can be added to `to-lua-str' +function as a separate multimethods for respective types." + {:fnl/arglist [multi-fn dispatch-value fnspec]} + (when (= (select :# ...) 0) (error "wrong argument amount for defmethod")) + `(let [dispatch# ,dispatch-val + multifn# ,multifn] + (and (not (. multifn# dispatch#)) + (doto multifn# + (tset dispatch# ,(fn* ...)))))) + + ;; loop + + (fn assert-tail [tail-sym body] + "Asserts that the passed in tail-sym function is a tail-call position of the +passed-in body. + +Throws an error if it is in a position to be returned or if the function is +situated to be called from a position other than the tail of the passed-in +body." + (fn last-arg? [form i] + (= (- (length form) 1) i)) + + ;; Tail in special forms are (After macroexpanding): + ;; + ;; - Every second form in an if, or the last form + ;; (if ... (sym ...) (sym ...)) + ;; + ;; - Last form in a let + ;; (let [] (sym ...)) + ;; + ;; - Last form in a do + ;; (do ... (sym ...)) + ;; + ;; Anything else fails the assert + (fn path-tail? [op i form] + (if (= op 'if) (and (not= 1 i) (or (last-arg? form i) (= 0 (% i 2)))) + (= op 'let) (last-arg? form i) + (= op 'do) (last-arg? form i) + false)) + + ;; Check the current form for the tail-sym, and if it's in a bad + ;; place, error out. If we run into other forms, we recurse with the + ;; comprehension if this is the tail form or not + (fn walk [body ok] + (let [[op & operands] body] + (if (list? op) (walk op true) + (assert-compile (not (and (= tail-sym op) (not ok))) + (.. (tostring tail-sym) " must be in tail position") + op) + (each [i v (ipairs operands)] + (if (list? v) (walk v (and ok (path-tail? op i body))) + (assert-compile (not= tail-sym v) + (.. (tostring tail-sym) " must not be passed") + v)))))) + + (walk `(do ,(macroexpand body)) true)) + + + (fn loop [binding-vec ...] + "Recursive loop macro. + +Similar to `let`, but binds a special `recur` call that will reassign +the values of the `binding-vec` and restart the loop `body*`. Unlike +`let`, doesn't support multiple-value destructuring. + +The first argument is a binding table with alternating symbols (or destructure +forms), and the values to bind to them. + +For example: + +``` fennel +(loop [[first & rest] [1 2 3 4 5] + i 0] + (if (= nil first) + i + (recur rest (+ 1 i)))) +``` + +This would destructure the first table argument, with the first value inside it +being assigned to `first` and the remainder of the table being assigned to +`rest`. `i` simply gets bound to 0. + +The body of the form executes for every item in the table, calling `recur` each +time with the table lacking its head element (thus consuming one element per +iteration), and with `i` being called with one value greater than the previous. + +When the loop terminates (When the user doesn't call `recur`) it will return the +number of elements in the passed in table. (In this case, 5) + +# Limitations + +In order to only evaluate expressions once and support sequential +bindings, the binding table has to be transformed like this: + +``` fennel :skip-test +(loop [[x & xs] (foo) + y (+ x 1)] + ...) + +(let [_1_ (foo) + [x & xs] _1_ + _2_ (+ x 1) + y _2_] + ((fn recur [[x & xs] y] ...) _1_ _2_) +``` + +This ensures that `foo` is called only once, its result is cached in a +`sym1#` binding, and that `y` can use the destructured value, obtained +from that binding. The value of this binding is later passed to the +function to begin the first iteration. + +This has two unfortunate consequences. One is that the initial +destructuring happens twice - first, to make sure that later bindings +can be properly initialized, and second, when the first looping +function call happens. Another one is that as a result, `loop` macro +can't work with multiple-value destructuring, because these can't be +cached as described above. E.g. this will not work: + +``` fennel :skip-test +(loop [(x y) (foo)] ...) +``` + +Because it would be transformed to: + +``` fennel :skip-test +(let [_1_ (foo) + (x y) _1_] + ((fn recur [(x y)] ...) _1_) +``` + +`x` is correctly set, but `y` is completely lost. Therefore, this +macro checks for lists in bindings." + {:fnl/arglist [binding-vec body*]} + (let [recur (sym :recur) + keys [] + gensyms [] + bindings []] + (assert-tail recur ...) + (each [i v (ipairs binding-vec)] + (when (= 0 (% i 2)) + (let [key (. binding-vec (- i 1)) + gs (gensym (tostring i))] + (assert-compile (not (list? key)) "loop macro doesn't support multiple-value destructuring" key) + ;; [sym1# sym2# etc...], for the function application below + (table.insert gensyms gs) + + ;; let bindings + (table.insert bindings gs) ;; sym1# + (table.insert bindings v) ;; (expression) + (table.insert bindings key) ;; [first & rest] + (table.insert bindings gs) ;; sym1# + + ;; The gensyms we use for function application + (table.insert keys key)))) + `(let ,bindings + ((fn ,recur ,keys + ,...) + ,(table.unpack gensyms))))) + + ;; Try catch finally + + (fn catch? [[fun]] + "Test if expression is a catch clause." + (= (tostring fun) :catch)) + + (fn finally? [[fun]] + "Test if expression is a finally clause." + (= (tostring fun) :finally)) + + (fn add-finally [finally form] + "Stores `form' as body of `finally', which will be injected into +`match' branches at places appropriate for it to run. + +Checks if there already was `finally' clause met, which can be only +one." + (assert-compile (= (length finally) 0) + "Only one finally clause can exist in try expression" + []) + (table.insert finally (list 'do ((or table.unpack _G.unpack) form 2)))) + + (fn add-catch [finally catches form] + "Appends `catch' body to a sequence of catch bodies that will later +be used in `make-catch-clauses' to produce AST. + +Checks if there already was `finally' clause met." + (assert-compile (= (length finally) 0) + "finally clause must be last in try expression" + []) + (table.insert catches (list 'do ((or table.unpack _G.unpack) form 2)))) + + (fn make-catch-clauses [catches finally] + "Generates AST of error branches for `match' macro." + (let [clauses []] + (var add-catchall? true) + (each [_ [_ binding-or-val & body] (ipairs catches)] + (when (sym? binding-or-val) + (set add-catchall? false)) + (table.insert clauses `(false ,binding-or-val)) + (table.insert clauses `(let [res# ((or table.pack #(doto [$...] (tset :n (select :# $...)))) + (do ,((or table.unpack _G.unpack) body)))] + ,(. finally 1) + (table.unpack res# 1 res#.n)))) + (when add-catchall? + ;; implicit catchall which retrows error further is added only + ;; if there were no catch clause that used symbol as catch value + (table.insert clauses `(false _#)) + (table.insert clauses `(do ,(. finally 1) (error _#)))) + ((or table.unpack _G.unpack) clauses))) + + (fn add-to-try [finally catches try form] + "Append form to the try body. There must be no `catch' of `finally' +clauses when we push body epression." + (assert-compile (and (= (length finally) 0) + (= (length catches) 0)) + "Only catch or finally clause can follow catch in try expression" + []) + (table.insert try form)) + + (fn try [...] + "General purpose try/catch/finally macro. +Wraps its body in `pcall' and checks the return value with `match' +macro. + +Catch clause is written either as `(catch symbol body*)`, thus acting +as catch-all, or `(catch value body*)` for catching specific errors. +It is possible to have several `catch' clauses. If no `catch' clauses +specified, an implicit catch-all clause is created. `body*', and +inner expressions of `catch-clause*', and `finally-clause?' are +wrapped in implicit `do'. + +The `finally` clause is optional, and written as (finally body*). If +present, it must be the last clause in the `try' form, and the only +`finally' clause. Note that `finally' clause is for side effects +only, and runs either after succesful run of `try' body, or after any +`catch' clause body, before returning the result. If no `catch' +clause is provided `finally' runs in implicit catch-all clause, and +trows error to upper scope using `error' function. + +To throw error from `try' to catch it with `catch' clause use `error' +or `assert' functions. + +# Examples +Catch all errors, ignore those and return fallback value: + +``` fennel +(fn add [x y] + (try + (+ x y) + (catch _ 0))) + +(assert-eq (add nil 1) 0) +``` + +Catch error and do cleanup: + +``` fennel +(local tbl []) + +(try + (table.insert tbl \"a\") + (table.insert tbl \"b\" \"c\") + (catch _ + (each [k _ (pairs tbl)] + (tset tbl k nil)))) + +(assert-eq (length tbl) 0) + +``` + +Always run some side effect action: + +``` fennel +(local t []) +(local res (try 10 (finally (table.insert t :finally)))) +(assert-eq (. t 1) :finally) +(assert-eq res 10) + +(local res (try (error 10) (catch 10 nil) (finally (table.insert t :again)))) +(assert-eq (. t 2) :again) +(assert-eq res nil) +```" + {:fnl/arglist [body* catch-clause* finally-clause?]} + (let [try '(do) + catches [] + finally []] + (each [_ form (ipairs [...])] + (if (list? form) + (if (catch? form) (add-catch finally catches form) + (finally? form) (add-finally finally form) + (add-to-try finally catches try form)) + (add-to-try finally catches try form))) + `(match (pcall (fn [] ((or table.pack #(doto [$...] (tset :n (select :# $...)))) ,try))) + (true _#) (do ,(. finally 1) ((or table.unpack _G.unpack) _# 1 _#.n)) + ,(make-catch-clauses catches finally)))) + + ;; Misc + + (fn cond [...] + "Takes a set of test expression pairs. It evaluates each test one at a +time. If a test returns logical true, `cond` evaluates and returns +the value of the corresponding expression and doesn't evaluate any of +the other tests or exprs. `(cond)` returns nil." + (assert-compile (= 0 (% (select "#" ...) 2)) + "cond requires an even number of forms" + ...) + (if (= 0 (select "#" ...)) + `nil + `(if ,...))) + + ;; Lazy seq + + (fn lazy-seq [...] + "Takes a `body` of expressions that returns a sequence, table or nil, +and yields a lazy sequence that will invoke the body only the first +time `seq` is called, and will cache the result and return it on all +subsequent `seq` calls. See also - `realized?`" + {:fnl/arglist [& body]} + `(do + (import-macros + {:lazy-seq lazy-seq#} + (doto :lazy-seq require)) + (let [core# (require ,lib-name) + res# (lazy-seq# ,...)] + (match (getmetatable res#) + mt# (doto mt# + (tset :cljlib/type :seq) + (tset :cljlib/conj + (fn [s# v#] (core#.cons v# s#))) + (tset :cljlib/empty #(core#.list)))) + res#))) + + (fn lazy-cat [...] + "Expands to code which yields a lazy sequence of the concatenation of +`colls` - expressions returning collections. Each expression is not +evaluated until it is needed." + {:fnl/arglist [& colls]} + `(do + (import-macros + {:lazy-cat lazy-cat#} + (doto :lazy-seq require)) + (let [core# (require ,lib-name) + res# (lazy-cat# ,...)] + (match (getmetatable res#) + mt# (doto mt# + (tset :cljlib/type :seq) + (tset :cljlib/conj + (fn [s# v#] (core#.cons v# s#))) + (tset :cljlib/empty #(core#.list)))) + res#))) + + (tset macro-loaded lib-name + {: fn* + : defn + : defn- + : in-ns + : ns + : def + : time + : when-let + : when-some + : if-let + : if-some + : defmulti + : defmethod + : cond + : loop + : try + : lazy-seq + : lazy-cat})) + +(import-macros + {: defn + : defn- + : ns + : def + : fn* + : if-let + : if-some + : cond} + (or ... :cljlib)) + +(ns core + "MIT License + +Copyright (c) 2022 Andrey Listopadov + +Permission is hereby granted‚ free of charge‚ to any person obtaining a copy +of this software and associated documentation files (the “Software”)‚ to deal +in the Software without restriction‚ including without limitation the rights +to use‚ copy‚ modify‚ merge‚ publish‚ distribute‚ sublicense‚ and/or sell +copies of the Software‚ and to permit persons to whom the Software is +furnished to do so‚ subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”‚ WITHOUT WARRANTY OF ANY KIND‚ EXPRESS OR +IMPLIED‚ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY‚ +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM‚ DAMAGES OR OTHER +LIABILITY‚ WHETHER IN AN ACTION OF CONTRACT‚ TORT OR OTHERWISE‚ ARISING FROM‚ +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE." + (:require [lazy-seq :as lazy] + [itable :as itable])) + +;;; Utility functions + +(fn unpack* [x ...] + (if (core.seq? x) + (lazy.unpack x) + (itable.unpack x ...))) + +(fn pack* [...] + (doto [...] (tset :n (select "#" ...)))) + +(fn pairs* [t] + (match (getmetatable t) + {:__pairs p} (p t) + _ (pairs t))) + +(fn ipairs* [t] + (match (getmetatable t) + {:__ipairs i} (i t) + _ (ipairs t))) + +(fn length* [t] + (match (getmetatable t) + {:__len l} (l t) + _ (length t))) + +(defn apply + "Apply `f` to the argument list formed by prepending intervening +arguments to `args`, and `f` must support variadic amount of +arguments. + +# Examples +Applying `add` to different amount of arguments: + +``` fennel +(assert-eq (apply add [1 2 3 4]) 10) +(assert-eq (apply add 1 [2 3 4]) 10) +(assert-eq (apply add 1 2 3 4 5 6 [7 8 9]) 45) +```" + ([f args] (f (unpack* args))) + ([f a args] (f a (unpack* args))) + ([f a b args] (f a b (unpack* args))) + ([f a b c args] (f a b c (unpack* args))) + ([f a b c d & args] + (let [flat-args [] + len (- (length* args) 1)] + (for [i 1 len] + (tset flat-args i (. args i))) + (each [i a (pairs* (. args (+ len 1)))] + (tset flat-args (+ i len) a)) + (f a b c d (unpack* flat-args))))) + +(defn add + "Sum arbitrary amount of numbers." + ([] 0) + ([a] a) + ([a b] (+ a b)) + ([a b c] (+ a b c)) + ([a b c d] (+ a b c d)) + ([a b c d & rest] (apply add (+ a b c d) rest))) + +(defn sub + "Subtract arbitrary amount of numbers." + ([] 0) + ([a] (- a)) + ([a b] (- a b)) + ([a b c] (- a b c)) + ([a b c d] (- a b c d)) + ([a b c d & rest] (apply sub (- a b c d) rest))) + +(defn mul + "Multiply arbitrary amount of numbers." + ([] 1) + ([a] a) + ([a b] (* a b)) + ([a b c] (* a b c)) + ([a b c d] (* a b c d)) + ([a b c d & rest] (apply mul (* a b c d) rest))) + +(defn div + "Divide arbitrary amount of numbers." + ([a] (/ 1 a)) + ([a b] (/ a b)) + ([a b c] (/ a b c)) + ([a b c d] (/ a b c d)) + ([a b c d & rest] (apply div (/ a b c d) rest))) + +(defn le + "Returns true if nums are in monotonically non-decreasing order" + ([a] true) + ([a b] (<= a b)) + ([a b & [c d & more]] + (if (<= a b) + (if d (apply le b c d more) + (<= b c)) + false))) + +(defn lt + "Returns true if nums are in monotonically decreasing order" + ([a] true) + ([a b] (< a b)) + ([a b & [c d & more]] + (if (< a b) + (if d (apply lt b c d more) + (< b c)) + false))) + +(defn ge + "Returns true if nums are in monotonically non-increasing order" + ([a] true) + ([a b] (>= a b)) + ([a b & [c d & more]] + (if (>= a b) + (if d (apply ge b c d more) + (>= b c)) + false))) + +(defn gt + "Returns true if nums are in monotonically increasing order" + ([a] true) + ([a b] (> a b)) + ([a b & [c d & more]] + (if (> a b) + (if d (apply gt b c d more) + (> b c)) + false))) + +(defn inc + "Increase number `x` by one" + [x] + (+ x 1)) + +(defn dec + "Decrease number `x` by one" + [x] + (- x 1)) + +(defn class + "Return cljlib type of the `x`, or lua type." + [x] + (match (type x) + :table (match (getmetatable x) + {:cljlib/type t} t + _ :table) + t t)) + +(defn constantly + "Returns a function that takes any number of arguments and returns `x`." + [x] + (fn [] x)) + +(defn complement + "Takes a function `f` and returns the function that takes the same +amount of arguments as `f`, has the same effect, and returns the +opposite truth value." + [f] + (fn* + ([] (not (f))) + ([a] (not (f a))) + ([a b] (not (f a b))) + ([a b & cs] (not (apply f a b cs))))) + +(defn identity + "Returns its argument." + [x] + x) + +(defn comp + "Compose functions." + ([] identity) + ([f] f) + ([f g] + (fn* + ([] (f (g))) + ([x] (f (g x))) + ([x y] (f (g x y))) + ([x y z] (f (g x y z))) + ([x y z & args] (f (apply g x y z args))))) + ([f g & fs] + (core.reduce comp (core.cons f (core.cons g fs))))) + +(defn eq + "Comparison function. + +Accepts arbitrary amount of values, and does the deep comparison. If +values implement `__eq` metamethod, tries to use it, by checking if +first value is equal to second value, and the second value is equal to +the first value. If values are not equal and are tables does the deep +comparison. Tables as keys are supported." + ([] true) + ([_] true) + ([a b] + (if (and (= a b) (= b a)) + true + (= :table (type a) (type b)) + (do (var (res count-a) (values true 0)) + (each [k v (pairs* a) :until (not res)] + (set res (eq v (do (var (res done) (values nil nil)) + (each [k* v (pairs* b) :until done] + (when (eq k* k) + (set (res done) (values v true)))) + res))) + (set count-a (+ count-a 1))) + (when res + (let [count-b (accumulate [res 0 _ _ (pairs* b)] + (+ res 1))] + (set res (= count-a count-b)))) + res) + false)) + ([a b & cs] + (and (eq a b) (apply eq b cs)))) + +(fn deep-index [tbl key] + "This function uses the `eq` function to compare keys of the given +table `tbl` and the given `key`. Several other functions also reuse +this indexing method, such as sets." + (accumulate [res nil + k v (pairs* tbl) + :until res] + (when (eq k key) + v))) + +(fn deep-newindex [tbl key val] + "This function uses the `eq` function to compare keys of the given +table `tbl` and the given `key`. If the key is found it's being +set, if not a new key is set." + (var done false) + (when (= :table (type key)) + (each [k _ (pairs* tbl) :until done] + (when (eq k key) + (rawset tbl k val) + (set done true)))) + (when (not done) + (rawset tbl key val))) + +(defn memoize + "Returns a memoized version of a referentially transparent function. +The memoized version of the function keeps a cache of the mapping from +arguments to results and, when calls with the same arguments are +repeated often, has higher performance at the expense of higher memory +use." + [f] + (let [memo (setmetatable {} {:__index deep-index})] + (fn* [& args] + (match (. memo args) + res (unpack* res 1 res.n) + _ (let [res (pack* (f ...))] + (tset memo args res) + (unpack* res 1 res.n)))))) + +(defn deref + "Dereference an object." + [x] + (match (getmetatable x) + {:cljlib/deref f} (f x) + _ (error "object doesn't implement cljlib/deref metamethod" 2))) + +(defn empty + "Get an empty variant of a given collection." + [x] + (match (getmetatable x) + {:cljlib/empty f} (f) + _ (match (type x) + :table [] + :string "" + _ (error (.. "don't know how to create empty variant of type " _))))) + +;;;Tests and predicates + +(defn nil? + "Test if `x` is nil." + ([] true) + ([x] (= x nil))) + +(defn zero? + "Test if `x` is equal to zero." + [x] + (= x 0)) + +(defn pos? + "Test if `x` is greater than zero." + [x] + (> x 0)) + +(defn neg? + "Test if `x` is less than zero." + [x] + (< x 0)) + +(defn even? + "Test if `x` is even." + [x] + (= (% x 2) 0)) + +(defn odd? + "Test if `x` is odd." + [x] + (not (even? x))) + +(defn string? + "Test if `x` is a string." + [x] + (= (type x) :string)) + +(defn boolean? + "Test if `x` is a Boolean" + [x] + (= (type x) :boolean)) + +(defn true? + "Test if `x` is `true`" + [x] + (= x true)) + +(defn false? + "Test if `x` is `false`" + [x] + (= x false)) + +(defn int? + "Test if `x` is a number without floating point data. + +Number is rounded with `math.floor` and compared with original number." + [x] + (and (= (type x) :number) + (= x (math.floor x)))) + +(defn pos-int? + "Test if `x` is a positive integer." + [x] + (and (int? x) + (pos? x))) + +(defn neg-int? + "Test if `x` is a negative integer." + [x] + (and (int? x) + (neg? x))) + +(defn double? + "Test if `x` is a number with floating point data." + [x] + (and (= (type x) :number) + (not= x (math.floor x)))) + +(defn empty? + "Check if collection is empty." + [x] + (match (type x) + :table + (match (getmetatable x) + {:cljlib/type :seq} + (nil? (core.seq x)) + (where (or nil {:cljlib/type nil})) + (let [(next*) (pairs* x)] + (= (next* x) nil))) + :string (= x "") + :nil true + _ (error "empty?: unsupported collection"))) + +(defn not-empty + "If `x` is empty, returns `nil`, otherwise `x`." + [x] + (if (not (empty? x)) + x)) + +(defn map? + "Check whether `x` is an associative table. + +Non-empty tables are tested by calling `next`. If the length of the +table is greater than zero, the last integer key is passed to the +`next`, and if `next` returns a key, the table is considered +associative. If the length is zero, `next` is called with what `paris` +returns for the table, and if the key is returned, table is considered +associative. + +Empty tables can't be analyzed with this method, and `map?` will +always return `false`. If you need this test pass for empty table, +see `hash-map` for creating tables that have additional metadata +attached for this test to work. + +# Examples +Non-empty map: + +``` fennel +(assert-is (map? {:a 1 :b 2})) +``` + +Empty tables don't pass the test: + +``` fennel +(assert-not (map? {})) +``` + +Empty tables created with `hash-map` will pass the test: + +``` fennel +(assert-is (map? (hash-map))) +```" + [x] + (if (= :table (type x)) + (match (getmetatable x) + {:cljlib/type :hash-map} true + {:cljlib/type :sorted-map} true + (where (or nil {:cljlib/type nil})) + (let [len (length* x) + (nxt t k) (pairs* x)] + (not= nil (nxt t (if (= len 0) k len)))) + _ false) + false)) + +(defn vector? + "Check whether `tbl` is a sequential table. + +Non-empty sequential tables are tested for two things: +- `next` returns the key-value pair, +- key, that is returned by the `next` is equal to `1`. + +Empty tables can't be analyzed with this method, and `vector?` will +always return `false`. If you need this test pass for empty table, +see `vector` for creating tables that have additional +metadata attached for this test to work. + +# Examples +Non-empty vector: + +``` fennel +(assert-is (vector? [1 2 3 4])) +``` + +Empty tables don't pass the test: + +``` fennel +(assert-not (vector? [])) +``` + +Empty tables created with `vector` will pass the test: + +``` fennel +(assert-is (vector? (vector))) +```" + [x] + (if (= :table (type x)) + (match (getmetatable x) + {:cljlib/type :vector} true + (where (or nil {:cljlib/type nil})) + (let [len (length* x) + (nxt t k) (pairs* x)] + (if (not= nil (nxt t (if (= len 0) k len))) false + (> len 0) true + false)) + _ false) + false)) + +(defn set? + "Check if object is a set." + [x] + (match (getmetatable x) + {:cljlib/type :hash-set} true + _ false)) + +(defn seq? + "Check if object is a sequence." + [x] + (lazy.seq? x)) + +(defn some? + "Returns true if x is not nil, false otherwise." + [x] + (not= x nil)) + +;;; Vector + +(fn vec->transient [immutable] + (fn [vec] + (var len (length vec)) + (->> {:__index (fn [_ i] + (if (<= i len) + (. vec i))) + :__len #len + :cljlib/type :transient + :cljlib/conj #(error "can't `conj` onto transient vector, use `conj!`") + :cljlib/assoc #(error "can't `assoc` onto transient vector, use `assoc!`") + :cljlib/dissoc #(error "can't `dissoc` onto transient vector, use `dissoc!`") + :cljlib/conj! (fn [tvec v] + (set len (+ len 1)) + (doto tvec (tset len v))) + :cljlib/assoc! (fn [tvec ...] + (let [len (length tvec)] + (for [i 1 (select "#" ...) 2] + (let [(k v) (select i ...)] + (if (<= 1 i len) + (tset tvec i v) + (error (.. "index " i " is out of bounds")))))) + tvec) + :cljlib/pop! (fn [tvec] + (if (= len 0) + (error "transient vector is empty" 2) + (let [val (table.remove tvec)] + (set len (- len 1)) + tvec))) + :cljlib/dissoc! #(error "can't `dissoc!` with a transient vector") + :cljlib/persistent! (fn [tvec] + (let [v (fcollect [i 1 len] (. tvec i))] + (while (> len 0) + (table.remove tvec) + (set len (- len 1))) + (setmetatable tvec + {:__index #(error "attempt to use transient after it was persistet") + :__newindex #(error "attempt to use transient after it was persistet")}) + (immutable (itable v))))} + (setmetatable {})))) + +(fn vec* [v len] + (match (getmetatable v) + mt (doto mt + (tset :__len (constantly (or len (length* v)))) + (tset :cljlib/type :vector) + (tset :cljlib/editable true) + (tset :cljlib/conj + (fn [t v] + (let [len (length* t)] + (vec* (itable.assoc t (+ len 1) v) (+ len 1))))) + (tset :cljlib/pop + (fn [t] + (let [len (- (length* t) 1) + coll []] + (when (< len 0) + (error "can't pop empty vector" 2)) + (for [i 1 len] + (tset coll i (. t i))) + (vec* (itable coll) len)))) + (tset :cljlib/empty + (fn [] (vec* (itable [])))) + (tset :cljlib/transient (vec->transient vec*)) + (tset :__fennelview (fn [coll view inspector indent] + (if (empty? coll) + "[]" + (let [lines (fcollect [i 1 (length* coll)] + (.. " " (view (. coll i) inspector indent)))] + (tset lines 1 (.. "[" (string.gsub (or (. lines 1) "") "^%s+" ""))) + (tset lines (length lines) (.. (. lines (length lines)) "]")) + lines))))) + nil (vec* (setmetatable v {}))) + v) + +(defn vec + "Coerce collection `coll` to a vector." + [coll] + (cond (empty? coll) (vec* (itable []) 0) + (vector? coll) (vec* (itable coll) (length* coll)) + :else (let [packed (-> coll core.seq lazy.pack) + len packed.n] + (-> packed + (doto (tset :n nil)) + (itable {:fast-index? true}) + (vec* len))))) + +(defn vector + "Constructs sequential table out of its arguments. + +Sets additional metadata for function `vector?` to work. + +# Examples + +``` fennel +(def :private v (vector 1 2 3 4)) +(assert-eq v [1 2 3 4]) +```" + [& args] + (vec args)) + +(defn nth + "Returns the value at the `index`. `get` returns `nil` if `index` out +of bounds, `nth` raises an error unless `not-found` is supplied. +`nth` also works for strings and sequences." + ([coll i] + (if (vector? coll) + (if (or (< i 1) (< (length* coll) i)) + (error (string.format "index %d is out of bounds" i)) + (. coll i)) + (string? coll) + (nth (vec coll) i) + (seq? coll) + (nth (vec coll) i) + :else + (error "expected an indexed collection"))) + ([coll i not-found] + (assert (int? i) "expected an integer key") + (if (vector? coll) + (or (. coll i) not-found) + (string? coll) + (nth (vec coll) i not-found) + (seq? coll) + (nth (vec coll) i not-found) + :else + (error "expected an indexed collection")))) + +;;; Sequences + +(defn- seq* + "Add cljlib sequence meta-info." + [x] + (match (getmetatable x) + mt (doto mt + (tset :cljlib/type :seq) + (tset :cljlib/conj + (fn [s v] (core.cons v s))) + (tset :cljlib/empty #(core.list)))) + x) + +(defn seq + "Construct a sequence from the given collection `coll`. If `coll` is +an associative table, returns sequence of vectors with key and value. +If `col` is sequential table, returns its shallow copy. If `col` is +string, return sequential table of its codepoints. + +# Examples +Sequential tables are transformed to sequences: + +``` fennel +(seq [1 2 3 4]) ;; @seq(1 2 3 4) +``` + +Associative tables are transformed to format like this `[[key1 value1] +... [keyN valueN]]` and order is non-deterministic: + +``` fennel +(seq {:a 1 :b 2 :c 3}) ;; @seq([:b 2] [:a 1] [:c 3]) +```" + [coll] + (seq* (match (getmetatable coll) + {:cljlib/seq f} (f coll) + _ (cond (lazy.seq? coll) (lazy.seq coll) + (map? coll) (lazy.map vec coll) + :else (lazy.seq coll))))) + +(defn rseq + "Returns, in possibly-constant time, a seq of the items in `rev` in reverse order. +Input must be traversable with `ipairs`. Doesn't work in constant +time if `rev` implements a linear-time `__len` metamethod, or invoking +Lua `#` operator on `rev` takes linar time. If `t` is empty returns +`nil`. + +# Examples + +``` fennel +(def :private v [1 2 3]) +(def :private r (rseq v)) + +(assert-eq (reverse v) r) +```" + [rev] + (seq* (lazy.rseq rev))) + +(defn lazy-seq + "Create lazy sequence from the result of calling a function `f`. +Delays execution of `f` until sequence is consumed. `f` must return a +sequence or a vector." + [f] + (seq* (lazy.lazy-seq f))) + +(defn first + "Return first element of a `coll`. Calls `seq` on its argument." + [coll] + (lazy.first (seq coll))) + +(defn rest + "Returns a sequence of all elements of a `coll` but the first one. +Calls `seq` on its argument." + [coll] + (seq* (lazy.rest (seq coll)))) + +(defn- next* + "Return the tail of a sequence. + +If the sequence is empty, returns nil." + [s] + (seq* (lazy.next s))) + +(doto core (tset :next next*)) ; luajit doesn't like next redefinition + +(defn count + "Count amount of elements in the sequence." + [s] + (match (getmetatable s) + {:cljlib/type :vector} (length* s) + _ (lazy.count s))) + +(defn cons + "Construct a cons cell. +Prepends new `head` to a `tail`, which must be either a table, +sequence, or nil. + +# Examples + +``` fennel +(assert-eq [0 1] (cons 0 [1])) +(assert-eq (list 0 1 2 3) (cons 0 (cons 1 (list 2 3)))) +```" + [head tail] + (seq* (lazy.cons head tail))) + +(fn list + [...] + "Create eager sequence of provided values. + +# Examples + +``` fennel +(local l (list 1 2 3 4 5)) +(assert-eq [1 2 3 4 5] l) +```" + (seq* (lazy.list ...))) + +(set core.list list) + +(defn list* + "Creates a new sequence containing the items prepended to the rest, +the last of which will be treated as a sequence. + +# Examples + +``` fennel +(local l (list* 1 2 3 [4 5])) +(assert-eq [1 2 3 4 5] l) +```" + [& args] + (seq* (apply lazy.list* args))) + +(defn last + "Returns the last element of a `coll`. Calls `seq` on its argument." + [coll] + (match (next* coll) + coll* (last coll*) + _ (first coll))) + +(defn butlast + "Returns everything but the last element of the `coll` as a new + sequence. Calls `seq` on its argument." + [coll] + (seq (lazy.drop-last coll))) + +(defn map + "Returns a lazy sequence consisting of the result of applying `f` to +the set of first items of each `coll`, followed by applying `f` to the +set of second items in each `coll`, until any one of the `colls` is +exhausted. Any remaining items in other `colls` are ignored. Function +`f` should accept number-of-colls arguments. Returns a transducer when +no collection is provided. + +# Examples + +``` fennel +(map #(+ $ 1) [1 2 3]) ;; => @seq(2 3 4) +(map #(+ $1 $2) [1 2 3] [4 5 6]) ;; => @seq(5 7 9) +(def :private res (map #(+ $ 1) [:a :b :c])) ;; will raise an error only when realized +```" + ([f] + (fn* [rf] + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (rf result (f input))) + ([result input & inputs] + (rf result (apply f input inputs)))))) + ([f coll] + (seq* (lazy.map f coll))) + ([f coll & colls] + (seq* (apply lazy.map f coll colls)))) + +(defn mapv + "Returns a vector consisting of the result of applying `f` to the +set of first items of each `coll`, followed by applying `f` to the set +of second items in each coll, until any one of the `colls` is +exhausted. Any remaining items in other collections are ignored. +Function `f` should accept number-of-colls arguments." + ([f coll] + (->> coll + (core.transduce (map f) + core.conj! + (core.transient (vector))) + core.persistent!)) + ([f coll & colls] (vec (apply map f coll colls)))) + +(defn map-indexed + "Returns a lazy sequence consisting of the result of applying `f` to 1 +and the first item of `coll`, followed by applying `f` to 2 and the +second item in `coll`, etc., until `coll` is exhausted. Returns a +transducer when no collection is provided." + ([f] + (fn* [rf] + (var i -1) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (set i (+ i 1)) + (rf result (f i input)))))) + ([f coll] + (seq* (lazy.map-indexed f coll)))) + +(defn mapcat + "Apply `concat` to the result of calling `map` with `f` and +collections `colls`. Returns a transducer when no collection is +provided." + ([f] + (comp (map f) core.cat)) + ([f & colls] + (seq* (apply lazy.mapcat f colls)))) + +(defn filter + "Returns a lazy sequence of the items in `coll` for which +`pred` returns logical true. Returns a transducer when no collection +is provided." + ([pred] + (fn* [rf] + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (if (pred input) + (rf result input) + result))))) + ([pred coll] + (seq* (lazy.filter pred coll)))) + +(defn filterv + "Returns a vector of the items in `coll` for which +`pred` returns logical true." + [pred coll] + (vec (filter pred coll))) + +(defn every? + "Test if every item in `coll` satisfies the `pred`." + [pred coll] + (lazy.every? pred coll)) + +(defn some + "Test if any item in `coll` satisfies the `pred`." + [pred coll] + (lazy.some? pred coll)) + +(defn not-any? + "Test if no item in `coll` satisfy the `pred`." + [pred coll] + (some #(not (pred $)) coll)) + +(defn range + "Returns lazy sequence of numbers from `lower` to `upper` with optional +`step`." + ([] (seq* (lazy.range))) + ([upper] (seq* (lazy.range upper))) + ([lower upper] (seq* (lazy.range lower upper))) + ([lower upper step] (seq* (lazy.range lower upper step)))) + +(defn concat + "Return a lazy sequence of concatenated `colls`." + [& colls] + (seq* (apply lazy.concat colls))) + +(defn reverse + "Returns a lazy sequence with same items as in `coll` but in reverse order." + [coll] + (seq* (lazy.reverse coll))) + +(defn take + "Returns a lazy sequence of the first `n` items in `coll`, or all items if +there are fewer than `n`." + ([n] + (fn* [rf] + (var n n) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [result (if (< 0 n) + (rf result input) + result)] + (set n (- n 1)) + (if (not (< 0 n)) + (core.ensure-reduced result) + result)))))) + ([n coll] + (seq* (lazy.take n coll)))) + +(defn take-while + "Take the elements from the collection `coll` until `pred` returns logical +false for any of the elemnts. Returns a lazy sequence. Returns a +transducer when no collection is provided." + ([pred] + (fn* [rf] + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (if (pred input) + (rf result input) + (core.reduced result)))))) + ([pred coll] + (seq* (lazy.take-while pred coll)))) + +(defn drop + "Drop `n` elements from collection `coll`, returning a lazy sequence +of remaining elements. Returns a transducer when no collection is +provided." + ([n] + (fn* [rf] + (var nv n) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [n nv] + (set nv (- nv 1)) + (if (pos? n) + result + (rf result input))))))) + ([n coll] + (seq* (lazy.drop n coll)))) + +(defn drop-while + "Drop the elements from the collection `coll` until `pred` returns logical +false for any of the elemnts. Returns a lazy sequence. Returns a +transducer when no collection is provided." + ([pred] + (fn* [rf] + (var dv true) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [drop? dv] + (if (and drop? (pred input)) + result + (do + (set dv nil) + (rf result input)))))))) + ([pred coll] + (seq* (lazy.drop-while pred coll)))) + +(defn drop-last + "Return a lazy sequence from `coll` without last `n` elements." + ([] (seq* (lazy.drop-last))) + ([coll] (seq* (lazy.drop-last coll))) + ([n coll] (seq* (lazy.drop-last n coll)))) + +(defn take-last + "Return a sequence of last `n` elements of the `coll`." + [n coll] + (seq* (lazy.take-last n coll))) + +(defn take-nth + "Return a lazy sequence of every `n` item in `coll`. Returns a +transducer when no collection is provided." + ([n] + (fn* [rf] + (var iv -1) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (set iv (+ iv 1)) + (if (= 0 (% iv n)) + (rf result input) + result))))) + ([n coll] + (seq* (lazy.take-nth n coll)))) + +(defn split-at + "Return a table with sequence `coll` being split at `n`" + [n coll] + (vec (lazy.split-at n coll))) + +(defn split-with + "Return a table with sequence `coll` being split with `pred`" + [pred coll] + (vec (lazy.split-with pred coll))) + +(defn nthrest + "Returns the nth rest of `coll`, `coll` when `n` is 0. + +# Examples + +``` fennel +(assert-eq (nthrest [1 2 3 4] 3) [4]) +(assert-eq (nthrest [1 2 3 4] 2) [3 4]) +(assert-eq (nthrest [1 2 3 4] 1) [2 3 4]) +(assert-eq (nthrest [1 2 3 4] 0) [1 2 3 4]) +``` +" + [coll n] + (seq* (lazy.nthrest coll n))) + +(defn nthnext + "Returns the nth next of `coll`, (seq coll) when `n` is 0." + [coll n] + (lazy.nthnext coll n)) + +(defn keep + "Returns a lazy sequence of the non-nil results of calling `f` on the +items of the `coll`. Returns a transducer when no collection is +provided." + ([f] + (fn* [rf] + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [v (f input)] + (if (nil? v) + result + (rf result v))))))) + ([f coll] + (seq* (lazy.keep f coll)))) + +(defn keep-indexed + "Returns a lazy sequence of the non-nil results of (f index item) in +the `coll`. Note, this means false return values will be included. +`f` must be free of side effects. Returns a transducer when no +collection is provided." + ([f] + (fn* [rf] + (var iv -1) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (set iv (+ iv 1)) + (let [v (f iv input)] + (if (nil? v) + result + (rf result v))))))) + ([f coll] + (seq* (lazy.keep-indexed f coll)))) + +(defn partition + "Given a collection `coll`, returns a lazy sequence of lists of `n` +items each, at offsets `step` apart. If `step` is not supplied, +defaults to `n`, i.e. the partitions do not overlap. If a `pad` +collection is supplied, use its elements as necessary to complete last +partition up to `n` items. In case there are not enough padding +elements, return a partition with less than `n` items." + ([n coll] (map seq* (lazy.partition n coll))) + ([n step coll] (map seq* (lazy.partition n step coll))) + ([n step pad coll] (map seq* (lazy.partition n step pad coll)))) + +(fn array [] + (var len 0) + (->> {:__len #len + :__index {:clear (fn [self] + (while (not= 0 len) + (tset self len nil) + (set len (- len 1)) + self)) + :add (fn [self val] + (set len (+ len 1)) + (tset self len val) + self)}} + (setmetatable []))) + +(defn partition-by + "Applies `f` to each value in `coll`, splitting it each time `f` +returns a new value. Returns a lazy seq of partitions. Returns a +transducer, if collection is not supplied." + ([f] + (fn* [rf] + (let [a (array) + none {}] + (var pv none) + (fn* + ([] (rf)) + ([result] + (rf (if (empty? a) + result + (let [v (vec a)] + (a:clear) + (core.unreduced (rf result v)))))) + ([result input] + (let [pval pv + val (f input)] + (set pv val) + (if (or (= pval none) + (= val pval)) + (do + (a:add input) + result) + (let [v (vec a)] + (a:clear) + (let [ret (rf result v)] + (when (not (core.reduced? ret)) + (a:add input)) + ret))))))))) + ([f coll] + (map seq* (lazy.partition-by f coll)))) + +(defn partition-all + "Given a collection `coll`, returns a lazy sequence of lists like +`partition`, but may include partitions with fewer than n items at the +end. Accepts addiitonal `step` argument, similarly to `partition`. +Returns a transducer, if collection is not supplied." + ([n] + (fn* [rf] + (let [a (array)] + (fn* + ([] (rf)) + ([result] + (rf (if (= 0 (length a)) + result + (let [v (vec a)] + (a:clear) + (core.unreduced (rf result v)))))) + ([result input] + (a:add input) + (if (= n (length a)) + (let [v (vec a)] + (a:clear) + (rf result v)) + result)))))) + ([n coll] + (map seq* (lazy.partition-all n coll))) + ([n step coll] + (map seq* (lazy.partition-all n step coll)))) + +(defn reductions + "Returns a lazy seq of the intermediate values of the reduction (as +per reduce) of `coll` by `f`, starting with `init`." + ([f coll] (seq* (lazy.reductions f coll))) + ([f init coll] (seq* (lazy.reductions f init coll)))) + +(defn contains? + "Test if `elt` is in the `coll`. It may be a linear search depending +on the type of the collection." + [coll elt] + (lazy.contains? coll elt)) + +(defn distinct + "Returns a lazy sequence of the elements of the `coll` without +duplicates. Comparison is done by equality. Returns a transducer when +no collection is provided." + ([] + (fn* [rf] + (let [seen (setmetatable {} {:__index deep-index})] + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (if (. seen input) + result + (do + (tset seen input true) + (rf result input)))))))) + ([coll] + (seq* (lazy.distinct coll)))) + +(defn dedupe + "Returns a lazy sequence removing consecutive duplicates in coll. +Returns a transducer when no collection is provided." + ([] + (fn* [rf] + (let [none {}] + (var pv none) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [prior pv] + (set pv input) + (if (= prior input) + result + (rf result input)))))))) + ([coll] (core.sequence (dedupe) coll))) + +(defn random-sample + "Returns items from `coll` with random probability of `prob` (0.0 - +1.0). Returns a transducer when no collection is provided." + ([prob] + (filter (fn [] (< (math.random) prob)))) + ([prob coll] + (filter (fn [] (< (math.random) prob)) coll))) + +(defn doall + "Realize whole lazy sequence `seq`. + +Walks whole sequence, realizing each cell. Use at your own risk on +infinite sequences." + [seq] + (seq* (lazy.doall seq))) + +(defn dorun + "Realize whole sequence `seq` for side effects. + +Walks whole sequence, realizing each cell. Use at your own risk on +infinite sequences." + [seq] + (lazy.dorun seq)) + +(defn line-seq + "Accepts a `file` handle, and creates a lazy sequence of lines using +`lines` metamethod. + +# Examples + +Lazy sequence of file lines may seem similar to an iterator over a +file, but the main difference is that sequence can be shared onve +realized, and iterator can't. Lazy sequence can be consumed in +iterator style with the `doseq` macro. + +Bear in mind, that since the sequence is lazy it should be realized or +truncated before the file is closed: + +``` fennel +(let [lines (with-open [f (io.open \"init.fnl\" :r)] + (line-seq f))] + ;; this will error because only first line was realized, but the + ;; file was closed before the rest of lines were cached + (assert-not (pcall next lines))) +``` + +Sequence is realized with `doall` before file was closed and can be shared: + +``` fennel +(let [lines (with-open [f (io.open \"init.fnl\" :r)] + (doall (line-seq f)))] + (assert-is (pcall next lines))) +``` + +Infinite files can't be fully realized, but can be partially realized +with `take`: + +``` fennel +(let [lines (with-open [f (io.open \"/dev/urandom\" :r)] + (doall (take 3 (line-seq f))))] + (assert-is (pcall next lines))) +```" + [file] + (seq* (lazy.line-seq file))) + +(defn iterate + "Returns an infinete lazy sequence of x, (f x), (f (f x)) etc." + [f x] + (seq* (lazy.iterate f x))) + +(defn remove + "Returns a lazy sequence of the items in the `coll` without elements +for wich `pred` returns logical true. Returns a transducer when no +collection is provided." + ([pred] + (filter (complement pred))) + ([pred coll] + (seq* (lazy.remove pred coll)))) + +(defn cycle + "Create a lazy infinite sequence of repetitions of the items in the +`coll`." + [coll] + (seq* (lazy.cycle coll))) + +(defn repeat + "Takes a value `x` and returns an infinite lazy sequence of this value. + +# Examples + +``` fennel +(assert-eq 20 (reduce add (take 10 (repeat 2)))) +```" + [x] + (seq* (lazy.repeat x))) + +(defn repeatedly + "Takes a function `f` and returns an infinite lazy sequence of +function applications. Rest arguments are passed to the function." + [f & args] + (seq* (apply lazy.repeatedly f args))) + +(defn tree-seq + "Returns a lazy sequence of the nodes in a tree, via a depth-first walk. + +`branch?` must be a function of one arg that returns true if passed a +node that can have children (but may not). `children` must be a +function of one arg that returns a sequence of the children. Will +only be called on nodes for which `branch?` returns true. `root` is +the root node of the tree. + +# Examples + +For the given tree `[\"A\" [\"B\" [\"D\"] [\"E\"]] [\"C\" [\"F\"]]]`: + + A + / \\ + B C + / \\ \\ + D E F + +Calling `tree-seq` with `next` as the `branch?` and `rest` as the +`children` returns a flat representation of a tree: + +``` fennel +(assert-eq (map first (tree-seq next rest [\"A\" [\"B\" [\"D\"] [\"E\"]] [\"C\" [\"F\"]]])) + [\"A\" \"B\" \"D\" \"E\" \"C\" \"F\"]) +```" + [branch? children root] + (seq* (lazy.tree-seq branch? children root))) + +(defn interleave + "Returns a lazy sequence of the first item in each sequence, then the +second one, until any sequence exhausts." + ([] (seq* (lazy.interleave))) + ([s] (seq* (lazy.interleave s))) + ([s1 s2] (seq* (lazy.interleave s1 s2))) + ([s1 s2 & ss] (seq* (apply lazy.interleave s1 s2 ss)))) + +(defn interpose + "Returns a lazy sequence of the elements of `coll` separated by +`separator`. Returns a transducer when no collection is provided." + ([sep] + (fn* [rf] + (var started false) + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (if started + (let [sepr (rf result sep)] + (if (core.reduced? sepr) + sepr + (rf sepr input))) + (do + (set started true) + (rf result input))))))) + ([separator coll] + (seq* (lazy.interpose separator coll)))) + +(defn halt-when + "Returns a transducer that ends transduction when `pred` returns `true` +for an input. When `retf` is supplied it must be a `fn` of 2 arguments +- it will be passed the (completed) result so far and the input that +triggered the predicate, and its return value (if it does not throw an +exception) will be the return value of the transducer. If `retf` is +not supplied, the input that triggered the predicate will be +returned. If the predicate never returns `true` the transduction is +unaffected." + ([pred] + (halt-when pred nil)) + ([pred retf] + (fn* [rf] + (let [halt (setmetatable {} {:__fennelview #"#<halt>"})] + (fn* + ([] (rf)) + ([result] + (if (and (map? result) (contains? result halt)) + result.value + (rf result))) + ([result input] + (if (pred input) + (core.reduced {halt true :value (if retf (retf (rf result) input) input)}) + (rf result input)))))))) + +(defn realized? + "Check if sequence's first element is realized." + [s] + (lazy.realized? s)) + +(defn keys + "Returns a sequence of the map's keys, in the same order as `seq`." + [coll] + (assert (or (map? coll) (empty? coll)) "expected a map") + (if (empty? coll) + (lazy.list) + (lazy.keys coll))) + +(defn vals + "Returns a sequence of the table's values, in the same order as `seq`." + [coll] + (assert (or (map? coll) (empty? coll)) "expected a map") + (if (empty? coll) + (lazy.list) + (lazy.vals coll))) + +(defn find + "Returns the map entry for `key`, or `nil` if key is not present in +`coll`." + [coll key] + (assert (or (map? coll) (empty? coll)) "expected a map") + (match (. coll key) + v [key v])) + +(defn sort + "Returns a sorted sequence of the items in `coll`. If no `comparator` +is supplied, uses `<`." + ([coll] + (match (seq coll) + s (seq (itable.sort (vec s))) + _ (list))) + ([comparator coll] + (match (seq coll) + s (seq (itable.sort (vec s) comparator)) + _ (list)))) + +;;; Reduce + +(defn reduce + "`f` should be a function of 2 arguments. If `val` is not supplied, +returns the result of applying `f` to the first 2 items in `coll`, +then applying `f` to that result and the 3rd item, etc. If `coll` +contains no items, f must accept no arguments as well, and reduce +returns the result of calling `f` with no arguments. If `coll` has +only 1 item, it is returned and `f` is not called. If `val` is +supplied, returns the result of applying `f` to `val` and the first +item in `coll`, then applying `f` to that result and the 2nd item, +etc. If `coll` contains no items, returns `val` and `f` is not +called. Early termination is supported via `reduced`. + +# Examples + +``` fennel +(defn- add + ([] 0) + ([a] a) + ([a b] (+ a b)) + ([a b & cs] (apply add (+ a b) cs))) +;; no initial value +(assert-eq 10 (reduce add [1 2 3 4])) +;; initial value +(assert-eq 10 (reduce add 1 [2 3 4])) +;; empty collection - function is called with 0 args +(assert-eq 0 (reduce add [])) +(assert-eq 10.3 (reduce math.floor 10.3 [])) +;; collection with a single element doesn't call a function unless the +;; initial value is supplied +(assert-eq 10.3 (reduce math.floor [10.3])) +(assert-eq 7 (reduce add 3 [4])) +```" + ([f coll] (lazy.reduce f (seq coll))) + ([f val coll] (lazy.reduce f val (seq coll)))) + +(defn reduced + "Terminates the `reduce` early with a given `value`. + +# Examples + +``` fennel +(assert-eq :NaN + (reduce (fn [acc x] + (if (not= :number (type x)) + (reduced :NaN) + (+ acc x))) + [1 2 :3 4 5])) +```" + [value] + (doto (lazy.reduced value) + (-> getmetatable (tset :cljlib/deref #($:unbox))))) + +(defn reduced? + "Returns true if `x` is the result of a call to reduced" + [x] + (lazy.reduced? x)) + +(defn unreduced + "If `x` is `reduced?`, returns `(deref x)`, else returns `x`." + [x] + (if (reduced? x) (deref x) x)) + +(defn ensure-reduced + "If x is already reduced?, returns it, else returns (reduced x)" + [x] + (if (reduced? x) + x + (reduced x))) + +(defn- preserving-reduced [rf] + (fn* [a b] + (let [ret (rf a b)] + (if (reduced? ret) + (reduced ret) + ret)))) + +(defn cat + "A transducer which concatenates the contents of each input, which must be a + collection, into the reduction. Accepts the reducing function `rf`." + [rf] + (let [rrf (preserving-reduced rf)] + (fn* + ([] (rf)) + ([result] (rf result)) + ([result input] + (reduce rrf result input))))) + +(defn reduce-kv + "Reduces an associative table using function `f` and initial value `val`. + +`f` should be a function of 3 arguments. Returns the result of +applying `f` to `val`, the first key and the first value in `tbl`, +then applying `f` to that result and the 2nd key and value, etc. If +`tbl` contains no entries, returns `val` and `f` is not called. Note +that `reduce-kv` is supported on sequential tables and strings, where +the keys will be the ordinals. + +Early termination is possible with the use of `reduced` +function. + +# Examples +Reduce associative table by adding values from all keys: + +``` fennel +(local t {:a1 1 + :b1 2 + :a2 2 + :b2 3}) + +(reduce-kv #(+ $1 $3) 0 t) +;; => 8 +``` + +Reduce table by adding values from keys that start with letter `a`: + +``` fennel +(local t {:a1 1 + :b1 2 + :a2 2 + :b2 3}) + +(reduce-kv (fn [res k v] (if (= (string.sub k 1 1) :a) (+ res v) res)) + 0 t) +;; => 3 +```" + [f val s] + (if (map? s) + (reduce (fn [res [k v]] (f res k v)) val (seq s)) + (reduce (fn [res [k v]] (f res k v)) val (map vector (drop 1 (range)) (seq s))))) + +(defn completing + "Takes a reducing function `f` of 2 args and returns a function +suitable for transduce by adding an arity-1 signature that calls +`cf` (default - `identity`) on the result argument." + ([f] (completing f identity)) + ([f cf] + (fn* + ([] (f)) + ([x] (cf x)) + ([x y] (f x y))))) + +(defn transduce + "`reduce` with a transformation of `f` (`xform`). If `init` is not +supplied, `f` will be called to produce it. `f` should be a reducing +step function that accepts both 1 and 2 arguments, if it accepts only +2 you can add the arity-1 with `completing`. Returns the result of +applying (the transformed) `xform` to `init` and the first item in +`coll`, then applying `xform` to that result and the 2nd item, etc. If +`coll` contains no items, returns `init` and `f` is not called. Note +that certain transforms may inject or skip items." + ([xform f coll] (transduce xform f (f) coll)) + ([xform f init coll] + (let [f (xform f)] + (f (reduce f init (seq coll)))))) + +(defn sequence + "Coerces coll to a (possibly empty) sequence, if it is not already +one. Will not force a lazy seq. `(sequence nil)` yields an empty list, +When a transducer `xform` is supplied, returns a lazy sequence of +applications of the transform to the items in `coll`, i.e. to the set +of first items of each `coll`, followed by the set of second items in +each `coll`, until any one of the `colls` is exhausted. Any remaining +items in other `colls` are ignored. The transform should accept +number-of-colls arguments" + ([coll] + (if (seq? coll) coll + (or (seq coll) (list)))) + ([xform coll] + (let [f (xform (completing #(cons $2 $1)))] + (or ((fn step [coll] + (if-some [s (seq coll)] + (let [res (f nil (first s))] + (cond (reduced? res) (f (deref res)) + (seq? res) (concat res (lazy-seq #(step (rest s)))) + :else (step (rest s)))) + (f nil))) + coll) + (list)))) + ([xform coll & colls] + (let [f (xform (completing #(cons $2 $1)))] + (or ((fn step [colls] + (if (every? seq colls) + (let [res (apply f nil (map first colls))] + (cond (reduced? res) (f (deref res)) + (seq? res) (concat res (lazy-seq #(step (map rest colls)))) + :else (step (map rest colls)))) + (f nil))) + (cons coll colls)) + (list))))) + +;;; Hash map + +(fn map->transient [immutable] + (fn [map] + (let [removed (setmetatable {} {:__index deep-index})] + (->> {:__index (fn [_ k] + (if (not (. removed k)) + (. map k))) + :cljlib/type :transient + :cljlib/conj #(error "can't `conj` onto transient map, use `conj!`") + :cljlib/assoc #(error "can't `assoc` onto transient map, use `assoc!`") + :cljlib/dissoc #(error "can't `dissoc` onto transient map, use `dissoc!`") + :cljlib/conj! (fn [tmap [k v]] + (if (= nil v) + (tset removed k true) + (tset removed k nil)) + (doto tmap (tset k v))) + :cljlib/assoc! (fn [tmap ...] + (for [i 1 (select "#" ...) 2] + (let [(k v) (select i ...)] + (tset tmap k v) + (if (= nil v) + (tset removed k true) + (tset removed k nil)))) + tmap) + :cljlib/dissoc! (fn [tmap ...] + (for [i 1 (select "#" ...)] + (let [k (select i ...)] + (tset tmap k nil) + (tset removed k true))) + tmap) + :cljlib/persistent! (fn [tmap] + (let [t (collect [k v (pairs tmap) + :into (collect [k v (pairs map)] + (values k v))] + (values k v))] + (each [k (pairs removed)] + (tset t k nil)) + (each [_ k (ipairs (icollect [k (pairs* tmap)] k))] + (tset tmap k nil)) + (setmetatable tmap + {:__index #(error "attempt to use transient after it was persistet") + :__newindex #(error "attempt to use transient after it was persistet")}) + (immutable (itable t))))} + (setmetatable {}))))) + +(fn hash-map* [x] + "Add cljlib hash-map meta-info." + (match (getmetatable x) + mt (doto mt + (tset :cljlib/type :hash-map) + (tset :cljlib/editable true) + (tset :cljlib/conj + (fn [t [k v] ...] + (apply core.assoc + t k v + (accumulate [kvs [] _ [k v] (ipairs* [...])] + (doto kvs + (table.insert k) + (table.insert v)))))) + (tset :cljlib/transient (map->transient hash-map*)) + (tset :cljlib/empty #(hash-map* (itable {})))) + _ (hash-map* (setmetatable x {}))) + x) + +(defn assoc + "Associate `val` under a `key`. +Accepts extra keys and values. + +# Examples + +``` fennel +(assert-eq {:a 1 :b 2} (assoc {:a 1} :b 2)) +(assert-eq {:a 1 :b 2} (assoc {:a 1 :b 1} :b 2)) +(assert-eq {:a 1 :b 2 :c 3} (assoc {:a 1 :b 1} :b 2 :c 3)) +```" + ([tbl] + (hash-map* (itable {}))) + ([tbl k v] + (assert (or (nil? tbl) (map? tbl) (empty? tbl)) "expected a map") + (assert (not (nil? k)) "attempt to use nil as key") + (hash-map* (itable.assoc (or tbl {}) k v))) + ([tbl k v & kvs] + (assert (or (nil? tbl) (map? tbl) (empty? tbl)) "expected a map") + (assert (not (nil? k)) "attempt to use nil as key") + (hash-map* (apply itable.assoc (or tbl {}) k v kvs)))) + +(defn assoc-in + "Associate `val` into set of immutable nested tables `t`, via given `key-seq`. +Returns a new immutable table. Returns a new immutable table. + +# Examples + +Replace value under nested keys: + +``` fennel +(assert-eq + {:a {:b {:c 1}}} + (assoc-in {:a {:b {:c 0}}} [:a :b :c] 1)) +``` + +Create new entries as you go: + +``` fennel +(assert-eq + {:a {:b {:c 1}} :e 2} + (assoc-in {:e 2} [:a :b :c] 1)) +```" + [tbl key-seq val] + (assert (or (nil? tbl) (map? tbl) (empty? tbl)) "expected a map or nil") + (hash-map* (itable.assoc-in tbl key-seq val))) + +(defn update + "Update table value stored under `key` by calling a function `f` on +that value. `f` must take one argument, which will be a value stored +under the key in the table. + +# Examples + +Same as `assoc` but accepts function to produce new value based on key value. + +``` fennel +(assert-eq + {:data \"THIS SHOULD BE UPPERCASE\"} + (update {:data \"this should be uppercase\"} :data string.upper)) +```" + [tbl key f] + (assert (or (nil? tbl) (map? tbl) (empty? tbl)) "expected a map") + (hash-map* (itable.update tbl key f))) + + +(defn update-in + "Update table value stored under set of immutable nested tables, via +given `key-seq` by calling a function `f` on the value stored under the +last key. `f` must take one argument, which will be a value stored +under the key in the table. Returns a new immutable table. + +# Examples + +Same as `assoc-in` but accepts function to produce new value based on key value. + +``` fennel +(fn capitalize-words [s] + (pick-values 1 + (s:gsub \"(%a)([%w_`]*)\" #(.. ($1:upper) ($2:lower))))) + +(assert-eq + {:user {:name \"John Doe\"}} + (update-in {:user {:name \"john doe\"}} [:user :name] capitalize-words)) +```" + [tbl key-seq f] + (assert (or (nil? tbl) (map? tbl) (empty? tbl)) "expected a map or nil") + (hash-map* (itable.update-in tbl key-seq f))) + +(defn hash-map + "Create associative table from `kvs` represented as sequence of keys +and values" + [& kvs] + (apply assoc {} kvs)) + +(defn get + "Get value from the table by accessing it with a `key`. +Accepts additional `not-found` as a marker to return if value wasn't +found in the table." + ([tbl key] (get tbl key nil)) + ([tbl key not-found] + (assert (or (map? tbl) (empty? tbl)) "expected a map") + (or (. tbl key) not-found))) + +(defn get-in + "Get value from nested set of tables by providing key sequence. +Accepts additional `not-found` as a marker to return if value wasn't +found in the table." + ([tbl keys] (get-in tbl keys nil)) + ([tbl keys not-found] + (assert (or (map? tbl) (empty? tbl)) "expected a map") + (var (res t done) (values tbl tbl nil)) + (each [_ k (ipairs* keys) :until done] + (match (. t k) + v (set (res t) (values v v)) + _ (set (res done) (values not-found true)))) + res)) + +(defn dissoc + "Remove `key` from table `tbl`. Optionally takes more `keys`." + ([tbl] tbl) + ([tbl key] + (assert (or (map? tbl) (empty? tbl)) "expected a map") + (hash-map* (doto tbl (tset key nil)))) + ([tbl key & keys] + (apply dissoc (dissoc tbl key) keys))) + +(defn merge + "Merge `maps` rght to left into a single hash-map." + [& maps] + (when (some identity maps) + (->> maps + (reduce (fn [a b] (collect [k v (pairs* b) :into a] + (values k v))) + {}) + itable + hash-map*))) + +(defn frequencies + "Return a table of unique entries from table `t` associated to amount +of their appearances. + +# Examples + +Count each entry of a random letter: + +``` fennel +(let [fruits [:banana :banana :apple :strawberry :apple :banana]] + (assert-eq (frequencies fruits) + {:banana 3 + :apple 2 + :strawberry 1})) +```" + [t] + (hash-map* (itable.frequencies t))) + +(defn group-by + "Group table items in an associative table under the keys that are +results of calling `f` on each element of sequential table `t`. +Elements that the function call resulted in `nil` returned in a +separate table. + +# Examples + +Group rows by their date: + +``` fennel +(local rows + [{:date \"2007-03-03\" :product \"pineapple\"} + {:date \"2007-03-04\" :product \"pizza\"} + {:date \"2007-03-04\" :product \"pineapple pizza\"} + {:date \"2007-03-05\" :product \"bananas\"}]) + +(assert-eq (group-by #(. $ :date) rows) + {\"2007-03-03\" + [{:date \"2007-03-03\" :product \"pineapple\"}] + \"2007-03-04\" + [{:date \"2007-03-04\" :product \"pizza\"} + {:date \"2007-03-04\" :product \"pineapple pizza\"}] + \"2007-03-05\" + [{:date \"2007-03-05\" :product \"bananas\"}]}) +```" + [f t] + (hash-map* (pick-values 1 (itable.group-by f t)))) + +(defn zipmap + "Return an associative table with the `keys` mapped to the +corresponding `vals`." + [keys vals] + (hash-map* (itable (lazy.zipmap keys vals)))) + +(defn replace + "Given a map of replacement pairs and a vector/collection `coll`, +returns a vector/seq with any elements `=` a key in `smap` replaced +with the corresponding `val` in `smap`. Returns a transducer when no +collection is provided." + ([smap] + (map #(if-let [e (find smap $)] (. e 2) $))) + ([smap coll] + (if (vector? coll) + (->> coll + (reduce (fn [res v] + (if-let [e (find smap v)] + (doto res (table.insert (. e 2))) + (doto res (table.insert v)))) + []) + itable + vec*) + (map #(if-let [e (find smap $)] (. e 2) $) coll)))) + +;;; Conj + +(defn conj + "Insert `x` as a last element of a table `tbl`. + +If `tbl` is a sequential table or empty table, inserts `x` and +optional `xs` as final element in the table. + +If `tbl` is an associative table, that satisfies `map?` test, +insert `[key value]` pair into the table. + +Mutates `tbl`. + +# Examples +Adding to sequential tables: + +``` fennel +(conj [] 1 2 3 4) +;; => [1 2 3 4] +(conj [1 2 3] 4 5) +;; => [1 2 3 4 5] +``` + +Adding to associative tables: + +``` fennel +(conj {:a 1} [:b 2] [:c 3]) +;; => {:a 1 :b 2 :c 3} +``` + +Note, that passing literal empty associative table `{}` will not work: + +``` fennel +(conj {} [:a 1] [:b 2]) +;; => [[:a 1] [:b 2]] +(conj (hash-map) [:a 1] [:b 2]) +;; => {:a 1 :b 2} +``` + +See `hash-map` for creating empty associative tables." + ([] (vector)) + ([s] s) + ([s x] + (match (getmetatable s) + {:cljlib/conj f} (f s x) + _ (if (vector? s) (vec* (itable.insert s x)) + (map? s) (apply assoc s x) + (nil? s) (cons x s) + (empty? s) (vector x) + (error "expected collection, got" (type s))))) + ([s x & xs] + (apply conj (conj s x) xs))) + +(defn disj + "Returns a new set type, that does not contain the +specified `key` or `keys`." + ([Set] Set) + ([Set key] + (match (getmetatable Set) + {:cljlib/type :hash-set :cljlib/disj f} (f Set key) + _ (error (.. "disj is not supported on " (class Set)) 2))) + ([Set key & keys] + (match (getmetatable Set) + {:cljlib/type :hash-set :cljlib/disj f} (apply f Set key keys) + _ (error (.. "disj is not supported on " (class Set)) 2)))) + +(defn pop + "If `coll` is a list returns a new list without the first +item. If `coll` is a vector, returns a new vector without the last +item. If the collection is empty, raises an error. Not the same as +`next` or `butlast`." + [coll] + (match (getmetatable coll) + {:cljlib/type :seq} (match (seq coll) + s (drop 1 s) + _ (error "can't pop empty list" 2)) + {:cljlib/pop f} (f coll) + _ (error (.. "pop is not supported on " (class coll)) 2))) + +;;; Transients + +(defn transient + "Returns a new, transient version of the collection." + [coll] + (match (getmetatable coll) + {:cljlib/editable true :cljlib/transient f} (f coll) + _ (error "expected editable collection" 2))) + +(defn conj! + "Adds `x` to the transient collection, and return `coll`." + ([] (transient (vec* []))) + ([coll] coll) + ([coll x] + (match (getmetatable coll) + {:cljlib/type :transient :cljlib/conj! f} (f coll x) + {:cljlib/type :transient} (error "unsupported transient operation" 2) + _ (error "expected transient collection" 2)) + coll)) + +(defn assoc! + "Remove `k`from transient map, and return `map`." + [map k & ks] + (match (getmetatable map) + {:cljlib/type :transient :cljlib/dissoc! f} (apply f map k ks) + {:cljlib/type :transient} (error "unsupported transient operation" 2) + _ (error "expected transient collection" 2)) + map) + +(defn dissoc! + "Remove `k`from transient map, and return `map`." + [map k & ks] + (match (getmetatable map) + {:cljlib/type :transient :cljlib/dissoc! f} (apply f map k ks) + {:cljlib/type :transient} (error "unsupported transient operation" 2) + _ (error "expected transient collection" 2)) + map) + +(defn disj! + "disj[oin]. Returns a transient set of the same type, that does not +contain `key`." + ([Set] Set) + ([Set key & ks] + (match (getmetatable Set) + {:cljlib/type :transient :cljlib/disj! f} (apply f Set key ks) + {:cljlib/type :transient} (error "unsupported transient operation" 2) + _ (error "expected transient collection" 2)))) + +(defn pop! + "Removes the last item from a transient vector. If the collection is +empty, raises an error Returns coll" + [coll] + (match (getmetatable coll) + {:cljlib/type :transient :cljlib/pop! f} (f coll) + {:cljlib/type :transient} (error "unsupported transient operation" 2) + _ (error "expected transient collection" 2))) + +(defn persistent! + "Returns a new, persistent version of the transient collection. The +transient collection cannot be used after this call, any such use will +raise an error." + [coll] + (match (getmetatable coll) + {:cljlib/type :transient :cljlib/persistent! f} (f coll) + _ (error "expected transient collection" 2))) + +;;; Into + +(defn into + "Returns a new coll consisting of `to` with all of the items of `from` +conjoined. A transducer `xform` may be supplied. + +# Examples + +Insert items of one collection into another collection: + +```fennel +(assert-eq [1 2 3 :a :b :c] (into [1 2 3] \"abc\")) +(assert-eq {:a 2 :b 3} (into {:a 1} {:a 2 :b 3})) +``` + +Transform a hash-map into a sequence of key-value pairs: + +``` fennel +(assert-eq [[:a 1]] (into (vector) {:a 1})) +``` + +You can also construct a hash-map from a sequence of key-value pairs: + +``` fennel +(assert-eq {:a 1 :b 2 :c 3} + (into (hash-map) [[:a 1] [:b 2] [:c 3]])) +```" + ([] (vector)) + ([to] to) + ([to from] + (match (getmetatable to) + {:cljlib/editable true} + (persistent! (reduce conj! (transient to) from)) + _ (reduce conj to from))) + ([to xform from] + (match (getmetatable to) + {:cljlib/editable true} + (persistent! (transduce xform conj! (transient to) from)) + _ (transduce xform conj to from)))) + +;;; Hash Set + +(fn viewset [Set view inspector indent] + (if (. inspector.seen Set) + (.. "@set" (. inspector.seen Set) "{...}") + (let [prefix (.. "@set" + (if (inspector.visible-cycle? Set) + (. inspector.seen Set) "") + "{") + set-indent (length prefix) + indent-str (string.rep " " set-indent) + lines (icollect [v (pairs* Set)] + (.. indent-str + (view v inspector (+ indent set-indent) true)))] + (tset lines 1 (.. prefix (string.gsub (or (. lines 1) "") "^%s+" ""))) + (tset lines (length lines) (.. (. lines (length lines)) "}")) + lines))) + +(fn hash-set->transient [immutable] + (fn [hset] + (let [removed (setmetatable {} {:__index deep-index})] + (->> {:__index (fn [_ k] + (if (not (. removed k)) (. hset k))) + :cljlib/type :transient + :cljlib/conj #(error "can't `conj` onto transient set, use `conj!`") + :cljlib/disj #(error "can't `disj` a transient set, use `disj!`") + :cljlib/assoc #(error "can't `assoc` onto transient set, use `assoc!`") + :cljlib/dissoc #(error "can't `dissoc` onto transient set, use `dissoc!`") + :cljlib/conj! (fn [thset v] + (if (= nil v) + (tset removed v true) + (tset removed v nil)) + (doto thset (tset v v))) + :cljlib/assoc! #(error "can't `assoc!` onto transient set") + :cljlib/assoc! #(error "can't `dissoc!` a transient set") + :cljlib/disj! (fn [thset ...] + (for [i 1 (select "#" ...)] + (let [k (select i ...)] + (tset thset k nil) + (tset removed k true))) + thset) + :cljlib/persistent! (fn [thset] + (let [t (collect [k v (pairs thset) + :into (collect [k v (pairs hset)] + (values k v))] + (values k v))] + (each [k (pairs removed)] + (tset t k nil)) + (each [_ k (ipairs (icollect [k (pairs* thset)] k))] + (tset thset k nil)) + (setmetatable thset + {:__index #(error "attempt to use transient after it was persistet") + :__newindex #(error "attempt to use transient after it was persistet")}) + (immutable (itable t))))} + (setmetatable {}))))) + +(fn hash-set* [x] + (match (getmetatable x) + mt (doto mt + (tset :cljlib/type :hash-set) + (tset :cljlib/conj + (fn [s v ...] + (hash-set* + (itable.assoc + s v v + (unpack* (let [res []] + (each [ _ v (ipairs [...])] + (table.insert res v) + (table.insert res v)) + res)))))) + (tset :cljlib/disj + (fn [s k ...] + (let [to-remove + (collect [_ k (ipairs [...]) + :into (->> {:__index deep-index} + (setmetatable {k true}))] + k true)] + (hash-set* + (itable.assoc {} + (unpack* + (let [res []] + (each [_ v (pairs s)] + (when (not (. to-remove v)) + (table.insert res v) + (table.insert res v))) + res))))))) + (tset :cljlib/empty #(hash-set* (itable {}))) + (tset :cljlib/editable true) + (tset :cljlib/transient (hash-set->transient hash-set*)) + (tset :cljlib/seq (fn [s] (map #(if (vector? $) (. $ 1) $) s))) + (tset :__fennelview viewset) + (tset :__fennelrest (fn [s i] + (var j 1) + (let [vals []] + (each [v (pairs* s)] + (if (>= j i) + (table.insert vals v) + (set j (+ j 1)))) + (core.hash-set (unpack* vals)))))) + _ (hash-set* (setmetatable x {}))) + x) + +(defn hash-set + "Create hash set. + +Set is a collection of unique elements, which sore purpose is only to +tell you if something is in the set or not." + [& xs] + (let [Set (collect [_ val (pairs* xs) + :into (->> {:__newindex deep-newindex} + (setmetatable {}))] + (values val val))] + (hash-set* (itable Set)))) + +;;; Multimethods + +(defn multifn? + "Test if `mf' is an instance of `multifn'. + +`multifn' is a special kind of table, created with `defmulti' macros +from `macros.fnl'." + [mf] + (match (getmetatable mf) + {:cljlib/type :multifn} true + _ false)) + +(defn remove-method + "Remove method from `multimethod' for given `dispatch-value'." + [multimethod dispatch-value] + (if (multifn? multimethod) + (tset multimethod dispatch-value nil) + (error (.. (tostring multimethod) " is not a multifn") 2)) + multimethod) + +(defn remove-all-methods + "Removes all methods of `multimethod'" + [multimethod] + (if (multifn? multimethod) + (each [k _ (pairs multimethod)] + (tset multimethod k nil)) + (error (.. (tostring multimethod) " is not a multifn") 2)) + multimethod) + +(defn methods + "Given a `multimethod', returns a map of dispatch values -> dispatch fns" + [multimethod] + (if (multifn? multimethod) + (let [m {}] + (each [k v (pairs multimethod)] + (tset m k v)) + m) + (error (.. (tostring multimethod) " is not a multifn") 2))) + +(defn get-method + "Given a `multimethod' and a `dispatch-value', returns the dispatch +`fn' that would apply to that value, or `nil' if none apply and no +default." + [multimethod dispatch-value] + (if (multifn? multimethod) + (or (. multimethod dispatch-value) + (. multimethod :default)) + (error (.. (tostring multimethod) " is not a multifn") 2))) + +core |