fix: map literals evaluate in source order; land the local-callee inline

jolt-p3c: Clojure evaluates map-literal entries left to right, but the
reader represented map forms as bare janet structs, so entries ran in
hash order. The reader now carries [k v ...] source order out-of-band —
on a struct PROTOTYPE (keys/kvs/length ignore protos, so macros that
get/keys literal map forms see no change; jolt-equal? was already
structural) and as a plain field on the phm rep (nil key/value). The
analyzer (form-map-pairs), the interpreter's map eval, both
syntax-quote walks, and core-sqmap (the lowered `{...} builder — the
array-map case, where Clojure also preserves insertion order) all honor
it, so the order survives macroexpansion in both modes.

jolt-507 root-caused: the parked inline put a LOCAL in janet call-head
position for the first time, and janet resolves head symbols against
the macro table before lexical upvalues — clojure.core/repeat's
self-name local expanded as janet's (repeat n & body) macro, compiling
the self-call into a countdown loop returning nil. Everything in the
issue (interpose, interleave) traced to that one name collision. The
emitter now rebinds local callees to reserved _fp$ symbols (argument
positions never consult the macro table), and the inline — direct
calls for function locals, jolt-call only for IFn-collection
leftovers — lands. Spec rows pin locals named repeat/seq/with called
in head position.

Gate green, suite 4718 steady, bench even with main.
This commit is contained in:
Yogthos 2026-06-12 14:09:50 -04:00
parent 34d32ea3bf
commit 15d599c0f3
6 changed files with 110 additions and 22 deletions

View file

@ -330,6 +330,22 @@
body (if (symbol? d-expr) body ['let [d d-expr] body])]
(wrap body))))
(direct-call? ctx fnode) (tuple (emit ctx fnode) ;args)
# Local callee (closure param, let-bound fn, defn self-name): inline the
# function check so the overwhelmingly-common function case is a direct
# janet call with no variadic arg-tuple packing — jolt-call only handles
# the IFn-collection leftovers (jank's dynamic_call removal, jolt-507).
# The callee is rebound to a reserved _fp$ symbol first: a raw jolt local
# name in janet CALL-HEAD position resolves against janet's macro table
# before the lexical upvalue, so a local named like a janet core macro
# (clojure.core/repeat's self-name vs janet's repeat macro) would expand
# as that macro. Argument positions (the old jolt-call shape, the rebind
# here) never consult the macro table, so the rebind is safe.
(= :local (fnode :op))
(let [fsym (jsym)]
['let [fsym (emit ctx fnode)]
['if ['function? fsym]
(tuple fsym ;args)
(tuple jolt-call fsym ;args)]])
(tuple jolt-call (emit ctx fnode) ;args)))
(defn- emit-vector [ctx node]

View file

@ -169,7 +169,17 @@
(tuple/slice r))
# Map builder: parts are alternating k v (no splicing in map syntax-quote).
(defn core-sqmap [& parts] (kvs->map (array ;parts)))
(defn core-sqmap [& parts]
# A syntax-quoted map template is Clojure's array-map case: construction
# order is source order and must survive into the built map, which usually
# becomes a FORM whose entries the evaluator walks (jolt-p3c). Same
# carriers as the reader: struct prototype / phm field.
(def kvs (array ;parts))
(def m (kvs->map kvs))
(cond
(struct? m) (struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs)
(table? m) (do (put m :jolt/kv-order (tuple/slice kvs)) m)
m))
# Set builder: like core-sqvec but yields a set, so `#{~@a} splices into a set.
(defn core-sqset [& parts]

View file

@ -261,9 +261,15 @@
(make-phs ;result))
(and (struct? form) (get form :jolt/type)) form
(struct? form)
(do (var kvs @[]) (each k (keys form)
(array/push kvs (syntax-quote* ctx bindings k gsmap))
(array/push kvs (syntax-quote* ctx bindings (get form k) gsmap))) (struct ;kvs))
(do (var kvs @[])
(def order (form-kv-order form))
(if order
(each x order (array/push kvs (syntax-quote* ctx bindings x gsmap)))
(each k (keys form)
(array/push kvs (syntax-quote* ctx bindings k gsmap))
(array/push kvs (syntax-quote* ctx bindings (get form k) gsmap))))
# keep carrying source order through nested syntax-quote (jolt-p3c)
(struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs))
form))
# Syntax-quote LOWERING: instead of evaluating a `(...) form to a value (what
@ -306,9 +312,12 @@
@[(sqsym* "quote") form]
(struct? form)
(do (var parts @[(sqsym* "__sqmap")])
(each k (keys form)
(array/push parts (syntax-quote-lower ctx k gsmap))
(array/push parts (syntax-quote-lower ctx (get form k) gsmap)))
(def order (form-kv-order form))
(if order
(each x order (array/push parts (syntax-quote-lower ctx x gsmap)))
(each k (keys form)
(array/push parts (syntax-quote-lower ctx k gsmap))
(array/push parts (syntax-quote-lower ctx (get form k) gsmap))))
parts)
@[(sqsym* "quote") form])))
@ -2353,19 +2362,26 @@
(error (string "No reader function for tag " tag))))
(if (get form :jolt/type)
(error (string "Unexpected tagged form: " (form :jolt/type)))
# plain map literal: evaluate keys and values
(let [kvs @[]]
(each k (keys form)
(array/push kvs (eval-form ctx bindings k))
(array/push kvs (eval-form ctx bindings (get form k))))
# plain map literal: evaluate keys and values in SOURCE order when
# the reader order rides along (jolt-p3c), hash order otherwise
(let [kvs @[]
order (form-kv-order form)]
(if order
(each x order (array/push kvs (eval-form ctx bindings x)))
(each k (keys form)
(array/push kvs (eval-form ctx bindings k))
(array/push kvs (eval-form ctx bindings (get form k)))))
(build-eval-map kvs))))))))
# A phm map-literal FORM (reader emits one for {:a nil} etc., which a struct
# would have dropped): evaluate its key/value forms and rebuild, preserving nil.
(phm? form)
(let [kvs @[]]
(each e (phm-entries form)
(array/push kvs (eval-form ctx bindings (in e 0)))
(array/push kvs (eval-form ctx bindings (in e 1))))
(let [kvs @[]
order (form-kv-order form)]
(if order
(each x order (array/push kvs (eval-form ctx bindings x)))
(each e (phm-entries form)
(array/push kvs (eval-form ctx bindings (in e 0)))
(array/push kvs (eval-form ctx bindings (in e 1)))))
(build-eval-map kvs))
(array? form)
(if (= 0 (length form))

View file

@ -22,6 +22,7 @@
(use ./evaluator)
(use ./core)
(import ./phm :as phm)
(import ./reader :as rdr)
# ---------------------------------------------------------------------------
# Form introspection
@ -54,7 +55,18 @@
(defn h-elements [form] (make-vec form))
(defn h-vector-items [form] (make-vec form))
(defn h-map-pairs [form]
(if (phm/phm? form)
# reader forms carry source order (jolt-p3c) — Clojure evaluates literal
# entries left to right; hash order only for constructed maps
(def order (rdr/form-kv-order form))
(cond
order
(do (def out @[])
(var i 0)
(while (< i (length order))
(array/push out (make-vec [(in order i) (in order (+ i 1))]))
(+= i 2))
(make-vec out))
(phm/phm? form)
(make-vec (map (fn [e] (make-vec [(in e 0) (in e 1)])) (phm/phm-entries form)))
(make-vec (map (fn [k] (make-vec [k (get form k)])) (keys form)))))
(defn h-set-items [form] (make-vec (form :value)))

View file

@ -305,7 +305,24 @@
(defn- reader-map [kvs]
(var has-nil false) (var i 0)
(while (< i (length kvs)) (when (nil? (in kvs i)) (set has-nil true) (break)) (++ i))
(if has-nil (phm/make-phm kvs) (struct ;kvs)))
# Source order rides along out-of-band (jolt-p3c): struct iteration is hash
# order, but Clojure evaluates literal entries left to right. A struct
# PROTOTYPE carries it without changing the form's map behavior (keys/kvs/
# length ignore protos; jolt-equal? compares maps structurally); the phm rep
# (nil key/value present) gets a plain extra field.
(if has-nil
(let [m (phm/make-phm kvs)]
(put m :jolt/kv-order (tuple/slice kvs))
m)
(struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs)))
(defn form-kv-order
"Source-ordered [k v k v ...] tuple of a map FORM (nil for maps built at
runtime, which carry no reader order)."
[form]
(cond
(struct? form) (get (struct/getproto form) :jolt/kv-order)
(table? form) (get form :jolt/kv-order)))
(defn read-map [s pos]
# pos is at opening brace

View file

@ -35,10 +35,16 @@
["collection key" ":v" "(get {[1 2] :v} [1 2])"]
["collection value-equal key" ":v" "(get {[1 2] :v} (vector 1 2))"]
["computed key" "1" "(get {(keyword \"a\") 1} :a)"]
# NOTE: entry ORDER is reader-hash order today, not source order (Clojure
# evaluates array-map literals left-to-right) — pinned loosely; see jolt-p3c
["values evaluate exactly once each" "[1 2 3]"
"(do (def log (atom [])) {:a (swap! log conj 1) :b (swap! log conj 2) :c (swap! log conj 3)} (vec (sort (deref log))))"]
# jolt-p3c: literal entries evaluate left to right in SOURCE order (the
# reader carries [k v ...] order on a struct prototype / phm field)
["values evaluate in source order" "[1 2 3]"
"(do (def log (atom [])) {:a (swap! log conj 1) :b (swap! log conj 2) :c (swap! log conj 3)} (deref log))"]
["keys evaluate before their values, pairwise" "[:k1 :v1 :k2 :v2]"
"(do (def log (atom [])) {(do (swap! log conj :k1) :a) (do (swap! log conj :v1) 1) (do (swap! log conj :k2) :b) (do (swap! log conj :v2) 2)} (deref log))"]
["source order with a nil value (phm form)" "[1 2 3]"
"(do (def log (atom [])) {:a (do (swap! log conj 1) nil) :b (swap! log conj 2) :c (swap! log conj 3)} (deref log))"]
["source order through syntax-quote" "[1 2]"
"(do (def log (atom [])) (defmacro m-p3c [] `{:a ~(list 'swap! 'log 'conj 1) :b ~(list 'swap! 'log 'conj 2)}) (m-p3c) (deref log))"]
["count" "3" "(count {:a 1 :b 2 :c 3})"]
["equality with phm" "true" "(= {:a 1 :b 2} (assoc {:a 1} :b 2))"]
["keys work after assoc" "2" "(:b (assoc {:a 1 :b 2} :c 3))"]
@ -55,3 +61,14 @@
["PI" "true" "(< 3.14 clojure.math/PI 3.15)"]
["require + alias" "5" "(do (require '[clojure.math :as m]) (long (m/hypot 3 4)))"]
["as a value" "[1 2]" "(mapv (comp long clojure.math/sqrt) [1 4])"])
# jolt-507: a jolt local in janet CALL-HEAD position must not resolve as a
# janet core macro of the same name (repeat/seq/loop are janet macros). The
# emitter rebinds local callees to reserved _fp$ symbols.
(defspec "calls / locals named like janet macros"
["local fn named repeat" "[1 1]" "(let [repeat (fn [x] [x x])] (repeat 1))"]
["local fn named seq" ":end" "((fn seq [n] (if (pos? n) (seq (dec n)) :end)) 2)"]
["local fn named loop2" "[2 1]" "(let [with (fn [a b] [b a])] (with 1 2))"]
["overlay repeat self-call (regression)" "(quote (0 0 0))" "(take 3 (repeat 0))"]
["closure param called" "42" "((fn [f] (f 41)) inc)"]
["param holding a keyword (IFn leftover)" "1" "((fn [f] (f {:a 1})) :a)"])