diff options
| author | Andrey Orst <andreyorst@gmail.com> | 2020-11-08 21:13:47 +0300 |
|---|---|---|
| committer | Andrey Orst <andreyorst@gmail.com> | 2020-11-08 21:13:47 +0300 |
| commit | 910bfcf768c2305a6885b0d1f491561d09ebd9ca (patch) | |
| tree | cd88174ffdb222df8b61ecf68796452580d9dee4 | |
| parent | cf18cb390b2ba9ac852b52b22beb9fda0d4ab7d2 (diff) | |
feature(macros): add metadata macros, doc, and some tests
| -rw-r--r-- | .dir-locals.el | 3 | ||||
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.org | 75 | ||||
| -rw-r--r-- | core.fnl | 4 | ||||
| -rw-r--r-- | macros/core.fnl | 25 | ||||
| -rw-r--r-- | macros/fn.fnl | 18 | ||||
| -rw-r--r-- | test/core.fnl | 6 | ||||
| -rw-r--r-- | test/fn.fnl | 76 | ||||
| -rw-r--r-- | test/macros.fnl | 27 | ||||
| -rw-r--r-- | test/test.fnl | 2 |
10 files changed, 154 insertions, 87 deletions
diff --git a/.dir-locals.el b/.dir-locals.el index 0d0d3cd..2f24cfa 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,7 +1,8 @@ ;;; Directory Local Variables ;;; For more information see (info "(emacs) Directory Variables") -((fennel-mode . ((eval . (put 'defmethod 'fennel-indent-function 'defun)) +((fennel-mode . ((eval . (put 'when-meta 'fennel-indent-function 'defun)) + (eval . (put 'defmethod 'fennel-indent-function 'defun)) (eval . (put 'defmulti 'bfennel-indent-function 'defun)) (eval . (put 'deftest 'fennel-indent-function 'defun)) (eval . (put 'testing 'fennel-indent-function 'defun)) @@ -10,7 +10,7 @@ all: $(LUASOURCES) ${LUASOURCES}: $(FNLSOURCES) %.lua: %.fnl - fennel --lua $(LUA) --metadata --compile $< > $@ + fennel --lua $(LUA) --compile $< > $@ clean: rm -f *.lua @@ -39,13 +39,12 @@ luacov-console: | luacov luacov-stats: test/core.lua test/macros.lua test/fn.lua @$(foreach test, $?, $(LUA) -lluarocks.loader -lluacov $(test);) - help: @echo "make -- run tests and create lua library" >&2 @echo "make test -- run tests" >&2 @echo "make clean -- remove lua files" >&2 @echo "make luacov -- build coverage report (requires working tests)" >&2 - @echo "make luacov-console -- build coverage report (requires working tests)" >&2 + @echo "make luacov-console -- build coverage report for luacov-console (requires working tests)" >&2 @echo "make help -- print this message and exit" >&2 -include .depend.mk @@ -15,6 +15,52 @@ Even though it is project is experimental, the goals of this project are: * Macros List of macros provided by the library. +** Metadata macros +Metadata in Fennel is a pretty tough subject, as there's no such thing as metadata in Lua. +Therefore, the metadata usage in Fennel is more limited compared to Clojure. +This library provides some facilities for metadata management, which are experimental and should be used with care. + +There are several important gotchas about using metadata. + +First, note that this works only when used with Fennel, and only when =(require fennel)= works. +For compiled Lua library this feature is turned off. + +Second, try to avoid using metadata with anything else than tables and functions. +When storing function or table as a key into metatable, its address is used, while when storing string of number, the value is used. +This, for example, may cause documentation collision, when you've set some variable holding a number value to have certain docstring, and later you've defined another variable with the same value, but different docstring. +While this isn't a major breakage, it may confuse if someone will explore your code in the REPL with =doc=. + +Lastly, note that prior to Fennel 0.7.1[fn:1] =import-macros= wasn't respecting =--metadata= switch. +So if you're using Fennel < 0.7.1 this stuff will only work if you use =require-macros= instead of =import-macros=. + +*** =when-meta= +This macros is a wrapper that compiles away if metadata support was not enabled. +What this effectively means, is that everything that is wrapped with this macro will disappear from the resulting Lua code if metadata is not enabled when compiling with =fennel --compile=. + +*** =with-meta= +Attach metadata to a value. + +#+begin_src fennel + >> (local foo (with-meta (fn [...] (let [[x y z] [...]] (+ x y z))) + {:fnl/arglist [:x :y :z :...] + :fnl/docstring "sum first three values"})) + >> (doc foo) + (foo x y z ...) + sum first three values +#+end_src + +When metadata feature is not enabled, returns the value without additional metadata. + +*** =meta= +Get metadata table from object: + +#+begin_src fennel + >> (meta (with-meta {} {:meta "data"})) + { + :meta "data" + } +#+end_src + ** =def= and =defonce= =def= is wrappers around =local= which can declare variables inside namespace, and as local at the same time: @@ -32,7 +78,7 @@ List of macros provided by the library. Both =ns.a= and =a= refer to the same value. -=defonce= ensures that the binding isn't overriden by another =defonce=: +=defonce= ensures that the binding isn't overridden by another =defonce=: #+begin_src fennel >> (defonce ns {}) @@ -44,7 +90,7 @@ Both =ns.a= and =a= refer to the same value. 42 #+end_src -Both =def= and =defonce= support literal attribute table as first argument, or a :dynamic keyword, that uses Fennel =var= instead of =local=: +Both =def= and =defonce= support literal metadata table as first argument, or a :dynamic keyword, that uses Fennel =var= instead of =local=: #+begin_src fennel >> (def {:dynamic true} a 10) @@ -57,6 +103,22 @@ Both =def= and =defonce= support literal attribute table as first argument, or a 42 #+end_src +Documentation string can be attached to value via =:doc= keyword. +However it is not recommended to attach metadata to everything except tables and functions: + +#+begin_src fennel + ;; Bad, may overlap with existing documentation for 299792458, if any + >> (def {:doc "The speed of light in m/s"} c 299792458) + >> (doc c) + c + The speed of light in m/s + + ;; OK + >> (def {:doc "default connection options"} + defaults {:port 1234 + :host localhost}) +#+end_src + ** =fn*= Clojure's =fn= equivalent. Returns a function of fixed amount of arguments by doing runtime dispatch based on argument count. @@ -271,6 +333,7 @@ It accepts the multi-fn table as its first argument, the dispatch value as secon =:default= is a special method which gets called when no other methods were found for given dispatch value. + * Functions Here are some important functions from the library. Full set can be examined by requiring the module. @@ -452,4 +515,10 @@ Compose functions into one function. #+end_src # LocalWords: Luajit VM arity runtime multi Cljlib fn mapv kv REPL -# LocalWords: namespaced namespace eq +# LocalWords: namespaced namespace eq metatable Lua defonce arglist +# LocalWords: namespaces defmulti defmethod metamethod butlast +# LocalWords: prepend LocalWords docstring + + +* Footnotes +[fn:1] https://todo.sr.ht/~technomancy/fennel/18#event-56799 @@ -2,8 +2,8 @@ (local insert table.insert) (local unpack (or table.unpack _G.unpack)) -(import-macros {: fn* : fn&} :macros.fn) -(import-macros {: when-some : if-some : when-let} :macros.core) +(require-macros :macros.fn) +(require-macros :macros.core) (fn* core.apply "Apply `f' to the argument list formed by prepending intervening diff --git a/macros/core.fnl b/macros/core.fnl index b073d27..def2f4c 100644 --- a/macros/core.fnl +++ b/macros/core.fnl @@ -1,7 +1,8 @@ -(import-macros {: fn* : fn&} :macros.fn) +(require-macros :macros.fn) (local core {}) (local unpack (or table.unpack _G.unpack)) (local insert table.insert) +(local meta-enabled (pcall _SCOPE.specials.doc (list (sym :doc) (sym :doc)) _SCOPE _CHUNK)) (fn multisym->sym [s] (if (multi-sym? s) @@ -152,18 +153,22 @@ (fn string? [x] (= (type x) :string)) +(fn& core.when-meta [...] + (when meta-enabled `(do ,...))) + (fn* core.with-meta [val meta] - `(let [val# ,val - (res# fennel#) (pcall require :fennel)] - (if res# - (each [k# v# (pairs ,meta)] - (fennel#.metadata:set val# k# v#))) - val#)) + (if (not meta-enabled) val + `(let [val# ,val + (res# fennel#) (pcall require :fennel)] + (if res# + (each [k# v# (pairs ,meta)] + (fennel#.metadata:set val# k# v#))) + val#))) (fn* core.meta [v] - `(let [(res# fennel#) (pcall require :fennel)] - (if res# - (. fennel#.metadata ,v)))) + (when-meta + `(let [(res# fennel#) (pcall require :fennel)] + (if res# (. fennel#.metadata ,v))))) (fn* core.defmulti [name & opts] diff --git a/macros/fn.fnl b/macros/fn.fnl index 2d4a753..24f909d 100644 --- a/macros/fn.fnl +++ b/macros/fn.fnl @@ -1,14 +1,16 @@ (local unpack (or table.unpack _G.unpack)) (local insert table.insert) (local sort table.sort) +(local meta-enabled (pcall _SCOPE.specials.doc (list (sym :doc) (sym :doc)) _SCOPE _CHUNK)) (fn with-meta [val meta] - `(let [val# ,val - (res# fennel#) (pcall require :fennel)] - (if res# - (each [k# v# (pairs ,meta)] - (fennel#.metadata:set val# k# v#))) - val#)) + (if (not meta-enabled) val + `(let [val# ,val + (res# fennel#) (pcall require :fennel)] + (if res# + (each [k# v# (pairs ,meta)] + (fennel#.metadata:set val# k# v#))) + val#))) (fn gen-arglist-doc [args] (if (list? (. args 1)) @@ -252,8 +254,8 @@ namespaced functions. See `fn*' for more info." (let [docstring (if (string? doc?) doc? nil) (name-wo-namespace namespaced?) (multisym->sym name) arg-list (if (sym? name-wo-namespace) - (if (string? doc?) args doc?) - name-wo-namespace) + (if (string? doc?) args doc?) + name-wo-namespace) arglist-doc (gen-arglist-doc arg-list) body (if (sym? name) (if (string? doc?) diff --git a/test/core.fnl b/test/core.fnl index c05f2ec..557f042 100644 --- a/test/core.fnl +++ b/test/core.fnl @@ -1,6 +1,6 @@ -(import-macros {: fn*} :macros.fn) -(import-macros {: into : defmethod : defmulti} :macros.core) -(import-macros {: assert-eq : assert-ne : assert* : testing : deftest} :test.test) +(require-macros :macros.fn) +(require-macros :macros.core) +(require-macros :test.test) (local {: apply diff --git a/test/fn.fnl b/test/fn.fnl index 7ea7c0a..44e280d 100644 --- a/test/fn.fnl +++ b/test/fn.fnl @@ -1,47 +1,49 @@ -(import-macros {: assert-eq : assert-ne : assert* : testing : deftest} :test.test) -(import-macros {: meta} :macros.core) +(require-macros :test.test) +(require-macros :macros.core) (local {: eq} (require :core)) ;; required for testing -(import-macros {: fn* : fn&} :macros.fn) +(require-macros :macros.fn) (deftest fn* - (testing fn*-meta - (fn* f - "docstring" - [x] x) - (assert-eq (meta f) {:fnl/docstring "docstring" - :fnl/arglist ["x"]}) + (when-meta + (testing fn*-meta + (fn* f + "docstring" + [x] x) + (assert-eq (meta f) {:fnl/docstring "docstring" + :fnl/arglist ["x"]}) - (fn* f - "docstring" - ([x] x)) - (assert-eq (meta f) {:fnl/docstring "docstring" - :fnl/arglist ["x"]}) + (fn* f + "docstring" + ([x] x)) + (assert-eq (meta f) {:fnl/docstring "docstring" + :fnl/arglist ["x"]}) - (fn* f - "docstring" - ([x] x) - ([x y] (+ x y))) - (assert-eq (meta f) {:fnl/docstring "docstring" - :fnl/arglist ["\n [x]" - "\n [x y]"]}) + (fn* f + "docstring" + ([x] x) + ([x y] (+ x y))) + (assert-eq (meta f) {:fnl/docstring "docstring" + :fnl/arglist ["\n [x]" + "\n [x y]"]}) - (fn* f - "docstring" - ([x] x) - ([x y] (+ x y)) - ([x y & z] (+ x y))) - (assert-eq (meta f) {:fnl/docstring "docstring" - :fnl/arglist ["\n [x]" - "\n [x y]" - "\n [x y & z]"]}))) + (fn* f + "docstring" + ([x] x) + ([x y] (+ x y)) + ([x y & z] (+ x y))) + (assert-eq (meta f) {:fnl/docstring "docstring" + :fnl/arglist ["\n [x]" + "\n [x y]" + "\n [x y & z]"]})))) (deftest fn& - (testing fn&-meta - (fn& f "docstring" [x] x) - (assert-eq (meta f) {:fnl/docstring "docstring" - :fnl/arglist ["x"]}) + (when-meta + (testing fn&-meta + (fn& f "docstring" [x] x) + (assert-eq (meta f) {:fnl/docstring "docstring" + :fnl/arglist ["x"]}) - (fn& f "docstring" [...] [...]) - (assert-eq (meta f) {:fnl/docstring "docstring" - :fnl/arglist ["..."]}))) + (fn& f "docstring" [...] [...]) + (assert-eq (meta f) {:fnl/docstring "docstring" + :fnl/arglist ["..."]})))) diff --git a/test/macros.fnl b/test/macros.fnl index 6341433..638a29d 100644 --- a/test/macros.fnl +++ b/test/macros.fnl @@ -1,18 +1,7 @@ -(import-macros {: assert-eq : assert-ne : assert* : testing : deftest} :test.test) +(require-macros :test.test) (local {: eq : identity} (require :core)) ;; required for testing -(import-macros - {: if-let - : when-let - : if-some - : when-some - : into - : defmethod - : defmulti - : defonce - : def - : meta - : with-meta} :macros.core) +(require-macros :macros.core) (deftest into (testing into @@ -131,18 +120,18 @@ (deftest meta (testing with-meta - (assert-eq (meta (with-meta :a {:k :v})) {:k :v})) + (assert-eq (meta (with-meta :a {:k :v})) (when-meta {:k :v}))) (testing def-meta (def {:doc "x"} x 10) - (assert-eq (meta x) {:fnl/docstring "x"}) + (assert-eq (meta x) (when-meta {:fnl/docstring "x"})) (def {:doc "x" :dynamic true} x 10) - (assert-eq (meta x) {:fnl/docstring "x"})) + (assert-eq (meta x) (when-meta {:fnl/docstring "x"}))) (testing defonce-meta (defonce {:doc "x"} x 10) - (assert-eq (meta x) {:fnl/docstring "x"}) + (assert-eq (meta x) (when-meta {:fnl/docstring "x"})) (defonce {:doc "y"} x 20) - (assert-eq (meta x) {:fnl/docstring "x"}) + (assert-eq (meta x) (when-meta {:fnl/docstring "x"})) (defonce {:doc "y" :dynamic true} y 20) - (assert-eq (meta y) {:fnl/docstring "y"}))) + (assert-eq (meta y) (when-meta {:fnl/docstring "y"})))) diff --git a/test/test.fnl b/test/test.fnl index 4b40832..ff7c8bc 100644 --- a/test/test.fnl +++ b/test/test.fnl @@ -1,4 +1,4 @@ -(import-macros {: fn*} :macros.fn) +(require-macros :macros.fn) ;; requires `eq' from core.fnl to be available at runtime (fn* assert-eq |