~technomancy/fennel

fennel: re-implement pick-values as a special v1 PROPOSED

~jaawerth: 1
 re-implement pick-values as a special

 4 files changed, 71 insertions(+), 22 deletions(-)
#1250549 .build.yml success
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH fennel] re-implement pick-values as a special Export this patch

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
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