~jaawerth: 1 re-implement pick-values as a special 4 files changed, 71 insertions(+), 22 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~technomancy/fennel/patches/53281/mbox | git am -3Learn more about email & git
From: jaawerth <jaawerth@gmail.com> Doing so introduces a number of optimizations and improvements: For (pick-values 1 ...), instead of generating syms to assign and truncate the return values accordingly, we just compile the target expression(s) and wrap the result in ( <ret> ), preserving side effects in the process. For example, `(pick-values 1 (foo))` emits `(foo())` For n = 0, we simply preserve side effects; the result will be either no values or nil depending on outer context. `[(pick-values 0 ...)]` will be `{}`, but `[x (pick-values 0 :y) :z]` will result in `{'x', nil, 'z'}}`. For all n > 1, instead of the `let` form with `n` bindings in the macro version, we simply use destructure to generate the declaration + assignment of n syms. This reduces the number of unnecessary IIFE's even for n > 1. --- BUG FIXES Previously, `(pick-values 0 ...)` would erroneously drop the side effects from the expressions passed to the macro. This new version correctly preserves them. --- NOTES As with `(values)`, `pick-values` for n > 1 does not guarantee order of evaluation for its operand expressions (relevant for side effects). Multi-value assignment in Lua leaves order unspecified as well, but this can still come as a surprise. --- changelog.md | 2 ++ src/fennel/macros.fnl | 19 ------------------- src/fennel/specials.fnl | 31 +++++++++++++++++++++++++++++++ test/core.fnl | 41 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/changelog.md b/changelog.md index 966dbb4..ed88df3 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ deprecated forms. ### New Features +* Macro `pick-values` is now a special, allowing it to avoid emitting IIFE's. * Add `fennel.getinfo` as source-map-aware equivalent of `debug.getinfo`. * Allow `&` and `&as` in the same destructure clause. * More consistent module-not-found warnings for `--require-as-include`. @@ -25,6 +26,7 @@ deprecated forms. ### Bug Fixes +* `(pick-values 0 (side-effects))` now correctly preserves side effects * Don't non-nil assert on `_foo` arguments in `lambda`. * Propagate compile options into `,compile` repl command. * IIFEs emitted by `and`/`or` inside `(fn [...])` now propagate varargs. diff --git a/src/fennel/macros.fnl b/src/fennel/macros.fnl index f5a815e..f651438 100644 --- a/src/fennel/macros.fnl +++ b/src/fennel/macros.fnl @@ -290,24 +290,6 @@ numerical range like `for` rather than an iterator." (for [i 1 n] (tset bindings i (gensym))) `(fn ,bindings (,f ,(unpack bindings))))) -(fn pick-values* [n ...] - "Evaluate to exactly n values. - -For example, - (pick-values 2 ...) -expands to - (let [(_0_ _1_) ...] - (values _0_ _1_))" - (assert (and (= :number (type n)) (<= 0 n) (= n (math.floor n))) - (.. "Expected n to be an integer >= 0, got " (tostring n))) - (let [let-syms (list) - let-values (if (= 1 (select "#" ...)) ... `(values ,...))] - (for [_ 1 n] - (table.insert let-syms (gensym))) - (if (= n 0) `(values) - `(let [,let-syms ,let-values] - (values ,(unpack let-syms)))))) - (fn lambda* [...] "Function literal with nil-checked arguments. Like `fn`, but will throw an exception if a declared argument is passed in as @@ -428,7 +410,6 @@ REPL `,return` command returns values to assert in place to continue execution." :lambda lambda* :λ lambda* :pick-args pick-args* - :pick-values pick-values* :macro macro* :macrodebug macrodebug* :import-macros import-macros* diff --git a/src/fennel/specials.fnl b/src/fennel/specials.fnl index 73ba5bf..5e7c79e 100644 --- a/src/fennel/specials.fnl +++ b/src/fennel/specials.fnl @@ -893,6 +893,7 @@ Method name doesn't have to be known at compile-time; if it is, use :for :while :do :lua :global)) false (where (or "<" ">" "<=" ">=" "=" "not=" "~=") (= (comparator-special-type x) :binding)) false + (where :pick-values (not= 1 (. x 2))) false (where call (. scope.macros call)) false (where ":" (= (method-special-type x) :binding)) false @@ -1481,6 +1482,36 @@ Lua output. The module must be a string literal and resolvable at compile time." (doc-special :tail! ["body"] "Assert that the body being called is in tail position.") +(fn SPECIALS.pick-values [ast scope parent] + (let [n (. ast 2) + vals (utils.list (utils.sym :values) (unpack ast 3))] + (compiler.assert (and (= :number (type n)) (<= 0 n) (= n (math.floor n))) + (.. "Expected n to be an integer >= 0, got " (tostring n))) + (if (= 1 n) + ;; n = 1 can be simplified to (<expr>) in lua output + (let [[[expr]] (compiler.compile1 vals scope parent {:nval 1})] + [(.. "(" expr ")")]) + (= 0 n) + (do + (for [i 3 (length ast)] + (-> (compiler.compile1 (. ast i) scope parent {:nval 0}) + (compiler.keep-side-effects parent nil ast))) + []) + (let [syms (fcollect [_ 1 n &into (utils.list)] + (utils.sym (compiler.gensym scope :pv)))] + ;; Declare exactly n temp bindings for supplied values without `let` + (compiler.destructure syms vals ast scope parent + {:nomulti true :noundef true + :symtype :pv :declaration true}) + syms)))) + +(doc-special :pick-values ["n" "..."] + "Evaluate to exactly n values.\n\nFor example, + (pick-values 2 ...) +expands to + (let [(_0_ _1_) ...] + (values _0_ _1_))") + (fn SPECIALS.eval-compiler [ast scope parent] (let [old-first (. ast 1)] (tset ast 1 (utils.sym :do)) diff --git a/test/core.fnl b/test/core.fnl index a33cbf4..ce5e9eb 100644 --- a/test/core.fnl +++ b/test/core.fnl @@ -167,7 +167,6 @@ (== (let [add (fn [x y z] (+ x y z)) f2 (partial add 1 2)] (f2 6)) 9) (== (let [add (fn [x y] (+ x y)) add2 (partial add)] (add2 99 2)) 101) (== (let [add (fn [x y] (+ x y)) inc (partial add 1)] (inc 99)) 100) - (== (let [f #(values :a :b :c)] [(pick-values 0 (f))]) {}) (== (let [f (fn [x y f2] (+ x (f2 y))) f2 (fn [x y] (* x (+ 2 y))) f3 (fn [f] (fn [x] (f 5 x)))] @@ -179,7 +178,6 @@ (== (let [t {:x 1} f (partial + t.x)] [(f 1) (do (set t.x 2) (f 1))]) [2 2]) (== (pcall (lambda [string] nil) 1) true) - (== (select :# (pick-values 3)) 3) (== (do (tset (getmetatable ::) :__call (fn [s t] (. t s))) (let [res (:answer {:answer 42})] @@ -191,7 +189,6 @@ (let [f (fn [] (set a (+ a 2)))] (f) (f) a)) 15) - (== [(pick-values 4 :a :b :c (values :d :e))] ["a" "b" "c" "d"]) (== ((fn [a & [b {: c}]] (string.format a (+ b c))) "haha %s" 4 {:c 3}) "haha 7") (== ((fn [& {1 _ 2 _ 3 x}] x) :one :two :three) "three") @@ -468,6 +465,43 @@ (x.y:foo :quux)) "bazquux")) +(fn test-pick-values [] + (== (select :# (pick-values 3)) + 3) + (== [(pick-values 0 (select 1 :a :b :c))] + []) + (== [(pick-values 4 :a :b :c (values :d :e))] + ["a" "b" "c" "d"]) + ;; ensure pick-values output respects nval, e.g. in middle of table literal + (== [:X (pick-values 2 :Y :YY) :Z] [:X :Y :Z]) + (== [:X (pick-values 0 :YY) :Y] [:X nil :Y]) + (t.= "return (select(1, \"x\", \"y\", \"z\"))" + (fennel.compile-string "(pick-values 1 (select 1 :x :y :z))")) + (== [(if (= 1 1) (pick-values 1 (select 1 :x :y :z)) :bork)] + [:x]) + (== (do (var i 0) (fn i++ [] (set i (+ i 1)) i) + (doto [(pick-values 1 (i++) (i++) (i++))] + (table.insert i))) + [3 3]) + ;; Ensure (pick-values 0 ...) emits nil when needed, and keeps side effects + (== (do (var i 0) (fn i++ [] (set i (+ i 1)) i) + (doto [:X (pick-values 0 (i++) (i++) (i++)) :Y] + ;; Using tset because, in luajit and luajit only, + ;; (doto [:X (values) :Y] (table.insert :Z)) returns [:X 3 :Y] + (tset 4 i))) + [:X nil :Y 3]) + (== (do (var i 0) (fn i++ [] (set i (+ i 1)) i) + (doto [(pick-values 2 (i++) (select 1 (i++) :X) (values (i++) (i++)))] + (table.insert i))) + [1 2 4]) + (== (let [pack (or table.pack #(doto [$...] (tset :n (select :# $...)))) + t (pack (pick-values 3 (pick-values 1 (values :x :y :z))))] + [t.n ((or _G.unpack table.unpack) t)]) + [3 :x]) + (== (let [pack (or table.pack #(doto [$...] (tset :n (select :# ...))))] + (pack (pick-values 5 :x (do :y) (do :z)))) + {1 :x 2 :y 3 :z :n 5})) + (fn test-with-open [] (== (do (var fh nil) @@ -534,6 +568,7 @@ : test-functions : test-hashfn : test-if + : test-pick-values : test-with-open : test-method-calls : test-comment -- 2.43.4
builds.sr.ht <builds@sr.ht>fennel/patches/.build.yml: SUCCESS in 31s [re-implement pick-values as a special][0] from [~jaawerth][1] [0]: https://lists.sr.ht/~technomancy/fennel/patches/53281 [1]: mailto:jaawerth@gmail.com ✓ #1250549 SUCCESS fennel/patches/.build.yml https://builds.sr.ht/~technomancy/job/1250549