fix: bind-put sentinel for nil values in Janet tables

Janet's (put table key nil) silently drops the key. This caused
nil-valued fn/let/loop/macro parameters to be invisible to resolve-sym,
making them fall through to global symbol resolution.

Added bind-put helper that uses :jolt/nil sentinel for nil values,
and resolve-sym unwraps it back to nil.
This commit is contained in:
Yogthos 2026-06-01 23:41:43 -04:00
parent e1317c0a7e
commit 6e37a270d7
3 changed files with 72 additions and 10 deletions

View file

@ -92,6 +92,12 @@
(ns-import current-ns alias ns-name)))
nil))
(defn- bind-put
"Put a value into bindings. Uses :jolt/nil sentinel for nil values
because Janet's (put table key nil) silently drops the key."
[bindings key value]
(put bindings key (if (nil? value) :jolt/nil value)))
(defn- resolve-sym
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
@ -104,7 +110,8 @@
(if v (var-get v) (error (string "Unable to resolve symbol: " ns "/" name))))))
# Use :jolt/not-found sentinel to distinguish nil binding from absent binding
(let [local (get bindings name :jolt/not-found)]
(if (not= local :jolt/not-found) local
(if (not= local :jolt/not-found)
(if (= local :jolt/nil) nil local)
(let [current-ns (ctx-current-ns ctx) ns (ctx-find-ns ctx current-ns) v (ns-find ns name)]
(if v (var-get v)
# Check clojure.core as auto-referred fallback
@ -239,7 +246,7 @@
(put new-bindings "&env" @{}) # implicit &env for macro bodies (table — nil-safe)
(var i 0)
(each a fixed-names
(put new-bindings a (macro-args i))
(bind-put new-bindings a (macro-args i))
(++ i))
(when rest-name
(put new-bindings rest-name (tuple/slice macro-args i)))
@ -300,7 +307,7 @@
(table/setproto fn-bindings bindings)
(var i 0)
(each arg-name fixed-names
(put fn-bindings arg-name (fn-args i))
(bind-put fn-bindings arg-name (fn-args i))
(++ i))
(when rest-name
(put fn-bindings rest-name (tuple/slice fn-args i)))
@ -328,7 +335,7 @@
(table/setproto fn-bindings bindings)
(var i 0)
(each arg-name fixed-names
(put fn-bindings arg-name (fn-args i))
(bind-put fn-bindings arg-name (fn-args i))
(++ i))
(when rest-name
(put fn-bindings rest-name (tuple/slice fn-args i)))
@ -346,10 +353,8 @@
(let [len (length bind-vec)]
(while (< i len)
(let [sym (bind-vec i)]
# Pre-register with nil to prevent proto fallthrough to global scope
(put new-bindings (sym :name) nil)
(def val (eval-form ctx new-bindings (bind-vec (+ i 1))))
(put new-bindings (sym :name) val)
(bind-put new-bindings (sym :name) val)
(+= i 2))))
(var result nil)
(each body-form body
@ -370,7 +375,7 @@
(table/setproto loop-bindings bindings)
(var j 0)
(each sn sym-names
(put loop-bindings sn (args j))
(bind-put loop-bindings sn (args j))
(++ j))
(put loop-bindings :jolt/loop-fn loop-fn)
(var result nil)
@ -486,7 +491,7 @@
(table/setproto new-bindings bindings)
(var i 0)
(each a arg-names
(put new-bindings a (args i))
(bind-put new-bindings a (args i))
(++ i))
(var result nil)
(each bf body