From 0878c803d73f2309bd9a0488d170710e9dacf41a Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 6 Jun 2026 20:03:49 -0400 Subject: [PATCH] compiler: IFn collections held in a var/local are callable (jolt-vh5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The back end emitted a direct Janet call (f args) for every :var/:local callee, but Janet calling a pvec-table does get (no integer key -> nil), a keyword does the wrong thing, etc. — so (let [v [1 2 3]] (v 0)), a keyword/meta-vector in a binding, all returned nil in compile mode (interpret was fine). Now a direct call is emitted only when the callee is provably a function: :fn / :host always, and a :var whose current root is a function; everything else routes through jolt-call, which dispatches IFn correctly (function fast-path first). User/core fn calls stay direct, so no hot-path regression — fixpoint stage1==stage2==stage3 holds. Conformance 218/218 all modes; full suite green. Spec cases added. --- src/jolt/backend.janet | 19 ++++++++++++++++--- test/spec/vectors-spec.janet | 7 ++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index 6625856..5463087 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -156,8 +156,21 @@ ['do ['var s nil] ['set s body] s]) body)) -(defn- direct-call? [fnode] - (case (fnode :op) :var true :local true :fn true :host true false)) +# A direct Janet call (f args) is only correct when the callee is definitely a +# function: Janet calling a pvec/keyword/etc. does get (or the wrong thing), not +# IFn dispatch. So only emit a direct call for :fn / :host (always functions) and +# a :var whose CURRENT root is a function (the common user/core-fn case). A :var +# holding an IFn COLLECTION (vector/keyword/set used as a fn) or a :local of +# unknown value falls through to jolt-call, which dispatches IFn correctly +# (function fast-path first). Trade-off, like direct-linking: a fn-var redefined +# to a collection after this call was compiled would still emit a direct call. +(defn- direct-call? [ctx fnode] + (case (fnode :op) + :fn true + :host true + :var (let [r (get (cell-for ctx (fnode :ns) (fnode :name)) :root)] + (or (function? r) (cfunction? r))) + false)) # Hot primitives emitted as native Janet ops (host-specific optimization): a # call to clojure.core/+ etc. becomes (+ …) rather than a var deref + variadic @@ -188,7 +201,7 @@ '++ ['+ (in args 0) 1] '-- ['- (in args 0) 1] (tuple nop ;args)) - (direct-call? (node :fn)) (tuple (emit ctx (node :fn)) ;args) + (direct-call? ctx (node :fn)) (tuple (emit ctx (node :fn)) ;args) (tuple jolt-call (emit ctx (node :fn)) ;args))) (defn- emit-vector [ctx node] diff --git a/test/spec/vectors-spec.janet b/test/spec/vectors-spec.janet index 8debf03..90572d8 100644 --- a/test/spec/vectors-spec.janet +++ b/test/spec/vectors-spec.janet @@ -24,7 +24,12 @@ ["count" "3" "(count [1 2 3])"] ["contains? index" "true" "(contains? [:a :b] 1)"] ["contains? past end" "false" "(contains? [:a] 3)"] - ["vector as fn" ":b" "([:a :b :c] 1)"]) + ["vector as fn" ":b" "([:a :b :c] 1)"] + # An IFn collection held in a binding (not just a literal) must dispatch as IFn, + # not as a host call: applies to vectors, keywords, and meta-bearing vectors. + ["vector-in-local as fn" "20" "(let [v [10 20 30]] (v 1))"] + ["keyword-in-local as fn" "7" "(let [k :a] (k {:a 7}))"] + ["meta vector as fn" "10" "((with-meta [10 20] {:k 1}) 0)"]) (defspec "vector / update (persistent)" ["conj appends" "[1 2 3]" "(conj [1 2] 3)"]