Skip to content

Commit

Permalink
fix varargs expansion size
Browse files Browse the repository at this point in the history
There's a single branch that handles varargs dynamically (at runtime).
  • Loading branch information
xificurC committed Nov 30, 2023
1 parent d0d38e7 commit 9037712
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 76 deletions.
136 changes: 71 additions & 65 deletions src/hyperfiddle/electric.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,47 @@ executors are allowed (i.e. to control max concurrency, timeouts etc). Currently

(hyperfiddle.electric/def system-time-secs "seconds since 1970 Jan 1" (/ system-time-ms 1000.0))

(defmacro fn*
"Low-level construct. Use `hyperfiddle.electric/fn` instead.
Bare-bone reactive anonymous function. Single arity, no arity check, no variadic args support, no self-recur."
;; G: `e/fn*` produces a smaller program than `e/fn`. Experts can use `e/fn*` in internals and libraries.
;; Users should default to `e/fn`.
[args & body]
(let [debug-info {::dbg/type :reactive-fn
::dbg/meta (select-keys (meta &form) [:file :line])}]
(if (seq args)
`(::lang/closure (let [~@(interleave args lang/arg-sym)] ~@body) ~debug-info)
`(::lang/closure (do ~@body) ~debug-info))))

(cc/defn- -splicev [args] (into [] cat [(pop args) (peek args)]))
(hyperfiddle.electric/def Apply*
(hyperfiddle.electric/fn* [F args]
(let [spliced (-splicev args)]
(case (count spliced)
0 (new F)
1 (new F (nth spliced 0))
2 (new F (nth spliced 0) (nth spliced 1))
3 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2))
4 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3))
5 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4))
6 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5))
7 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6))
8 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7))
9 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8))
10 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9))
11 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10))
12 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11))
13 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12))
14 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13))
15 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14))
16 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15))
17 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16))
18 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17))
19 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18))
20 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18) (nth spliced 19))))))

(defmacro apply [F & args] `(new Apply* ~F [~@args]))

(cc/defn -check-recur-arity [provided actual fname]
(when (not= provided actual)
(throw (ex-info (str "You `recur`d in " (or fname "<unnamed-efn>") " with " provided
Expand All @@ -252,22 +293,23 @@ executors are allowed (i.e. to control max concurrency, timeouts etc). Currently
(let [~@(interleave args lang/arg-sym)] ~@body)))]
(new lang/rec ~@(take (count args) lang/arg-sym)))]))

#?(:clj (cc/defn- -build-vararg-arities [?name args body]
(let [npos (-> args count (- 2)), unvarargd (-> args pop pop (conj (peek args)))]
(into [] (map (cc/fn [n]
[n `(binding [lang/rec (::lang/closure (case (-check-recur-arity lang/%arity ~(inc npos) '~?name)
(let [~@(interleave unvarargd lang/arg-sym)] ~@body)))]
(new lang/rec ~@(take npos lang/arg-sym)
~(let [rst (into [] (comp (drop npos) (take (- n npos))) lang/arg-sym)]
(when (seq rst) ; varargs value is `nil` when no args provided
(if (map? (peek args))
(if (even? (count rst))
(list* `hash-map rst) ; (MapVararg. :x 1)
`(merge (hash-map ~@(pop rst)) ~(peek rst))) ; (MapVararg. :x 1 {:y 2})
(list* `vector rst))))))]))
(range npos 21)))))

#?(:clj (cc/defn ->narity-vec [arities] (into (sorted-set) (comp (map (cc/partial remove #{'&})) (map count)) arities)))
#?(:clj (cc/defn- -build-vararg-arity [?name args body]
(let [npos (-> args count (- 2)), unvarargd (-> args pop pop (conj (peek args))), v (gensym "varargs")]
`(binding [lang/rec (::lang/closure (case (-check-recur-arity lang/%arity ~(inc npos) '~?name)
(let [~@(interleave unvarargd lang/arg-sym)] ~@body)))]
(new lang/rec ~@(take npos lang/arg-sym)
(let [~v (into [] (drop ~npos) lang/%args)]
(when (seq ~v) ; varargs value is `nil` when no args provided
~(if (map? (peek args))
`(if (even? (count ~v))
(cc/apply hash-map ~v) ; (MapVararg. :x 1)
(merge (cc/apply hash-map (pop ~v)) (peek ~v))) ; (MapVararg. :x 1 {:y 2})
v))))))))

#?(:clj (cc/defn ->narity-set [arities]
(into (sorted-set) (comp (map #(take-while (complement #{'&}) %)) (map count)) arities)))
#?(:clj (cc/defn arity-holes [arity-set]
(remove arity-set (range (reduce max arity-set)))))

(cc/defn -throw-arity [?name nargs arities]
(throw (ex-info (str "You called " (or ?name "<unnamed-efn>") " with " nargs
Expand All @@ -294,41 +336,33 @@ executors are allowed (i.e. to control max concurrency, timeouts etc). Currently
(defmacro fn [& args]
(let [[?name args2] (if (symbol? (first args)) [(first args) (rest args)] [nil args])
arities (cond-> args2 (vector? (first args2)) list)
arity-set (->narity-set (map first arities))
{positionals false, varargs true} (group-by (comp varargs? first) arities)
_ (check-only-one-vararg! ?name (mapv first varargs))
_ (check-arity-conflicts! ?name (mapv first positionals) (ffirst varargs))
positional-branches (into [] (map (cc/fn [[args & body]] (-build-fn-arity ?name args body))) positionals)
vararg-branches (when (seq varargs) (-build-vararg-arities ?name (ffirst varargs) (nfirst varargs)))]
positional-branches (into [] (map (cc/fn [[args & body]] (-build-fn-arity ?name args body))) positionals)]
(list `check-electric `fn
(list ::lang/closure
(-> `(case lang/%arity
~@(into [] (comp cat cat) [positional-branches vararg-branches])
(-throw-arity '~?name lang/%arity ~(->> arities (eduction (map first)) ->narity-vec (str/join ", "))))
~@(into [] (comp cat cat) [positional-branches])
~@(if (seq varargs)
(conj [(arity-holes arity-set) `(-throw-arity '~?name lang/%arity ~(str/join ", " arity-set))]
(-build-vararg-arity ?name (ffirst varargs) (nfirst varargs)))
[`(-throw-arity '~?name lang/%arity ~(str/join ", " arity-set))])
#_(-throw-arity '~?name lang/%arity ~(->> arities (eduction (map first)) ->narity-set (str/join ", "))))
(?bind-self ?name))
{::dbg/name ?name, ::dbg/type (or (::dbg/type (meta ?name)) :reactive-fn)
::dbg/meta (merge (select-keys (meta &form) [:file :line])
(select-keys (meta ?name) [:file :line]))}))))

(defmacro fn*
"Low-level construct. Use `hyperfiddle.electric/fn` instead.
Bare-bone reactive anonymous function. Single arity, no arity check, no variadic args support, no self-recur."
;; G: `e/fn*` produces a smaller program than `e/fn`. Experts can use `e/fn*` in internals and libraries.
;; Users should default to `e/fn`.
[args & body]
(let [debug-info {::dbg/type :reactive-fn
::dbg/meta (select-keys (meta &form) [:file :line])}]
(if (seq args)
`(::lang/closure (let [~@(interleave args lang/arg-sym)] ~@body) ~debug-info)
`(::lang/closure (do ~@body) ~debug-info))))

(defmacro defn [sym & fdecl]
(let [[_defn sym' & _] (macroexpand `(cc/defn ~sym ~@fdecl))] ; GG: docstring support
`(hyperfiddle.electric/def ~sym' (hyperfiddle.electric/fn ~(vary-meta sym' merge {::dbg/type :reactive-defn}
(meta &form)
(meta sym'))
~@(if (string? (first fdecl)) ; GG: skip docstring
(rest fdecl)
fdecl)))))
(meta &form)
(meta sym'))
~@(if (string? (first fdecl)) ; GG: skip docstring
(rest fdecl)
fdecl)))))

(defmacro ^:no-doc defn* [sym & fdecl]
(let [[_defn sym' & _] (macroexpand `(cc/defn ~sym ~@fdecl))] ; GG: docstring support
Expand Down Expand Up @@ -478,34 +512,6 @@ Quoting it directly is idiomatic as well."
inhibiting all further reactive updates."
[x] `(check-electric snapshot (new (-snapshot (hyperfiddle.electric/fn* [] ~x)))))

(cc/defn- -splicev [args] (into [] cat [(pop args) (peek args)]))
(hyperfiddle.electric/defn* Apply* [F args]
(let [spliced (-splicev args)]
(case (count spliced)
0 (new F)
1 (new F (nth spliced 0))
2 (new F (nth spliced 0) (nth spliced 1))
3 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2))
4 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3))
5 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4))
6 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5))
7 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6))
8 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7))
9 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8))
10 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9))
11 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10))
12 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11))
13 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12))
14 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13))
15 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14))
16 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15))
17 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16))
18 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17))
19 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18))
20 (new F (nth spliced 0) (nth spliced 1) (nth spliced 2) (nth spliced 3) (nth spliced 4) (nth spliced 5) (nth spliced 6) (nth spliced 7) (nth spliced 8) (nth spliced 9) (nth spliced 10) (nth spliced 11) (nth spliced 12) (nth spliced 13) (nth spliced 14) (nth spliced 15) (nth spliced 16) (nth spliced 17) (nth spliced 18) (nth spliced 19)))))

(defmacro apply [F & args] `(new Apply* ~F [~@args]))

(cc/defn ->Object [] #?(:clj (Object.) :cljs (js/Object.))) ; private

;; low-level, most powerful, hardest to use
Expand Down
6 changes: 5 additions & 1 deletion src/hyperfiddle/electric/impl/lang.clj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

(def ^{::type ::node, :doc "for loop/recur impl"} rec)
(def ^{::type ::node, :doc "for runtime arity check"} %arity)
(def ^{::type ::node, :doc "for runtime varargs"} %args)
(def ^{::type ::node, :doc "for self-recur"} %closure)
(def ^{::type ::node, :doc "for try/catch"} exception)
(def ^{::type ::node, :doc "for case"} %case-test)
Expand Down Expand Up @@ -451,7 +452,10 @@
(= "js" (namespace f)) (assoc ::ir/tag 'js))
(analyze-binding env ; electric join
(list* `%closure f `%arity (count args) (interleave arg-sym args))
(fn [_] (ir/variable (ir/sub (+ 2 (count args)))))))
(fn [env]
#_(ir/variable (ir/sub (+ 2 (count args))))
(analyze-binding env [`%args `[~@(take (count args) arg-sym)]]
(fn [_] (ir/variable (ir/sub (+ 3 (count args)))))))))
(fail! env "Wrong number of arguments - new" {}))

;; (. java.time.Instant now)
Expand Down
1 change: 1 addition & 0 deletions src/hyperfiddle/electric/impl/lang.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

(def ^{::type ::node, :doc "for loop/recur impl"} rec)
(def ^{::type ::node, :doc "for runtime arity check"} %arity)
(def ^{::type ::node, :doc "for runtime varargs"} %args)
(def ^{::type ::node, :doc "for self-recur"} %closure)
(def ^{::type ::node, :doc "for try/catch"} exception)
(def ^{::type ::node, :doc "for case"} %case-test)
Expand Down
2 changes: 1 addition & 1 deletion src/hyperfiddle/electric_local_def.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

(cc/defn ->single-peer-config [env]
(let [p (if (and (:js-globals env) (contrib.cljs-target/do-nodejs true)) :client :server)]
(doto {::lang/peers {p (if (:js-globals env) :cljs :clj)}, ::lang/current p, ::lang/me p} prn)))
{::lang/peers {p (if (:js-globals env) :cljs :clj)}, ::lang/current p, ::lang/me p}))

(cc/defn pair [c s]
(m/sp
Expand Down
22 changes: 13 additions & 9 deletions test/hyperfiddle/electric_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1875,34 +1875,34 @@
% := ["foo" "foo"]))

(tests "e/fn varargs"
(with ((l/local (new (e/fn [x & xs] (tap [x xs])) 1 2 3 4)) tap tap)
(with ((l/single+ {} (new (e/fn [x & xs] (tap [x xs])) 1 2 3 4)) tap tap)
% := [1 [2 3 4]]))
(tests "e/fn varargs recursion with recur"
(with ((l/local (new (e/fn [x & xs] (tap [x xs])) 1 2 3 4)) tap tap)
(with ((l/single+ {} (new (e/fn [x & xs] (tap [x xs])) 1 2 3 4)) tap tap)
% := [1 [2 3 4]]))
(tests "e/fn varargs recur is arity-checked"
(with ((l/local (tap (try (new (e/fn [x & xs] (recur)) 1 2 3)
(with ((l/single+ {} (tap (try (new (e/fn [x & xs] (recur)) 1 2 3)
(catch ExceptionInfo e e)))) tap tap)
(ex-message %) := "You `recur`d in <unnamed-efn> with 0 arguments but it has 2 positional arguments"))

(l/defn MapVararg [& {:keys [x] :or {x 1} :as mp}] [x mp])
(tests "map vararg with no args is nil"
(with ((l/local (tap (MapVararg.))) tap tap)
(with ((l/single+ {} (tap (MapVararg.))) tap tap)
% := [1 nil]))
(tests "map vararg with kw args"
(with ((l/local (tap (MapVararg. :x 2))) tap tap)
(with ((l/single+ {} (tap (MapVararg. :x 2))) tap tap)
% := [2 {:x 2}]))
(tests "map vararg with map arg"
(with ((l/local (tap (MapVararg. {:x 2}))) tap tap)
(with ((l/single+ {} (tap (MapVararg. {:x 2}))) tap tap)
% := [2 {:x 2}]))
(tests "map vararg with mixture"
(with ((l/local (tap (MapVararg. :y 3 {:x 2}))) tap tap)
(with ((l/single+ {} (tap (MapVararg. :y 3 {:x 2}))) tap tap)
% := [2 {:x 2, :y 3}]))
(tests "map vararg trailing map takes precedence"
(with ((l/local (tap (MapVararg. :x 3 {:x 2}))) tap tap)
(with ((l/single+ {} (tap (MapVararg. :x 3 {:x 2}))) tap tap)
% := [2 {:x 2}]))
(tests "map vararg with positional arguments"
(with ((l/local (tap (new (e/fn [a & {:keys [x]}] [a x]) 1 :x 2))) tap tap)
(with ((l/single+ {} (tap (new (e/fn [a & {:keys [x]}] [a x]) 1 :x 2))) tap tap)
% := [1 2]))

(tests "e/fn recur is arity checked"
Expand All @@ -1919,6 +1919,10 @@
(tests "(new VarArgs 1 2 3)"
(with ((l/local (tap (new VarArgs 1 2 3))) tap tap)
% := [1 [2 3]]))
(tests "varargs arity is checked"
(with ((l/local (tap (try (new VarArgs)
(catch ExceptionInfo e e)))) tap tap)
(ex-message %) := "You called VarArgs with 0 arguments but it only supports 1"))

(tests "e/apply"
(with ((l/single+ {} (tap (e/apply VarArgs [1 2 3]))) tap tap)
Expand Down

0 comments on commit 9037712

Please sign in to comment.