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

@ -1 +1 @@
["^ ","~$eval-list",["^ ","~:fixed-arities",["~#set",[3]],"~:private",true,"~:ns","~$user","~:name","^0","~:type","~:fn","~:col",1,"~:top-ns",null,"~:row",196],"~$resolve-var",["^ ","^;",57,"^9",1,"^1",["^2",[3]],"^6","^<","^4","^5","^:",null,"^7","^8"],"~$syntax-quote*",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^=","^7","^8","^9",1,"^:",null,"^;",22],"~$new-v",["^ ","^;",436,"^9",18,"^6","^>","^4","^5","^:",null],"~$ctor",["^ ","^;",510,"^9",17,"~:varargs-min-arity",0,"^6","^?","^4","^5","^:",null,"^7","^8"],"~$v",["^ ","^;",252,"^9",20,"^6","~$v","^4","^5","^:",null],"~$resolve-sym",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^A","^7","^8","^9",1,"^:",null,"^;",95],"~$first-form",["^ ","^;",198,"^9",3,"^6","^B","^4","^5","^:",null,"^7","~:any"],"~$run-finally",["^ ","^;",406,"^9",13,"^1",["^2",[1]],"^6","^D","^4","^5","^:",null,"^7","^8"],"~$eval-require",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^E","^7","^8","^9",1,"^:",null,"~:arities",["^ ","~i2",["^ ","~:ret","~:nil","~:arglist-str","[ctx spec]"]],"^;",74],"~$alias-sym",["^ ","^;",85,"^9",15,"^6","^J","^4","^5","^:",null],"~$name",["^ ","^;",200,"^9",3,"^6","^K","^4","^5","^:",null,"^7",["^2",[["^ ","~:tag","^H","^;",202,"^9",15,"~:end-row",202,"~:end-col",18],["^ ","~:call",["^ ","~:filename","/Users/yogthos/src/jolt/src/jolt/evaluator.janet","^7","^O","~:lang","~:clj","~:base-lang","^R","~:resolved-ns","^5","^4","^5","^6","^B","~:arity",1],"^;",201,"^9",15,"^M",201,"^N",33]]]],"^P","/Users/yogthos/src/jolt/src/jolt/evaluator.janet","~$macro-fn",["^ ","^1",["^2",[]],"^4","^5","^6","^V","^7","^8","^9",18,"^:",null,"^@",0,"^;",236],"~$val",["^ ","^;",351,"^9",21,"^6","^W","^4","^5","^:",null],"~$parse-arg-names",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^X","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^ ","^7","~:map","~:val",["^ ","~:fixed",["^ ","^;",182,"^M",584,"^9",11,"^N",13]]],"^I","[args-form]"]],"^;",136],"~$special-symbol?",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^10","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^2",["~:boolean"]],"^I","[name]"]],"^;",10],"~$sym-name?",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^12","^7","^8","^9",1,"^:",null,"^;",6],"~$inner",["^ ","^;",160,"^9",17,"^6","^13","^4","^5","^:",null],"~$sym-name-str",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^14","^7","^8","^9",1,"^:",null,"^;",70],"~$unwrap-meta-name",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^15","^7","^8","^9",1,"^:",null,"^;",185]]
["^ ","~$eval-list",["^ ","~:fixed-arities",["~#set",[3]],"~:private",true,"~:ns","~$user","~:name","^0","~:type","~:fn","~:col",1,"~:top-ns",null,"~:row",203],"~$resolve-var",["^ ","^;",57,"^9",1,"^1",["^2",[3]],"^6","^<","^4","^5","^:",null,"^7","^8"],"~$syntax-quote*",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^=","^7","^8","^9",1,"^:",null,"^;",22],"~$new-v",["^ ","^;",441,"^9",18,"^6","^>","^4","^5","^:",null],"~$ctor",["^ ","^;",515,"^9",17,"~:varargs-min-arity",0,"^6","^?","^4","^5","^:",null,"^7","^8"],"~$v",["^ ","^;",259,"^9",20,"^6","~$v","^4","^5","^:",null],"~$resolve-sym",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^A","^7","^8","^9",1,"^:",null,"^;",101],"~$first-form",["^ ","^;",205,"^9",3,"^6","^B","^4","^5","^:",null,"^7","~:any"],"~$run-finally",["^ ","^;",411,"^9",13,"^1",["^2",[1]],"^6","^D","^4","^5","^:",null,"^7","^8"],"~$eval-require",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^E","^7","^8","^9",1,"^:",null,"~:arities",["^ ","~i2",["^ ","~:ret","~:nil","~:arglist-str","[ctx spec]"]],"^;",74],"~$bind-put",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^J","^7","^8","^9",1,"^:",null,"^;",95],"~$alias-sym",["^ ","^;",85,"^9",15,"^6","^K","^4","^5","^:",null],"~$name",["^ ","^;",207,"^9",3,"^6","^L","^4","^5","^:",null,"^7",["^2",[["^ ","~:call",["^ ","~:filename","/Users/yogthos/src/jolt/src/jolt/evaluator.janet","^7","^M","~:lang","~:clj","~:base-lang","^P","~:resolved-ns","^5","^4","^5","^6","^B","~:arity",1],"^;",208,"^9",15,"~:end-row",208,"~:end-col",33],["^ ","~:tag","^H","^;",209,"^9",15,"^T",209,"^U",18]]]],"^N","/Users/yogthos/src/jolt/src/jolt/evaluator.janet","~$macro-fn",["^ ","^1",["^2",[]],"^4","^5","^6","^W","^7","^8","^9",18,"^:",null,"^@",0,"^;",243],"~$val",["^ ","^;",356,"^9",21,"^6","^X","^4","^5","^:",null],"~$parse-arg-names",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^Y","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^ ","^7","~:map","~:val",["^ ","~:fixed",["^ ","^;",189,"^T",589,"^9",11,"^U",13]]],"^I","[args-form]"]],"^;",143],"~$special-symbol?",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^11","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^2",["~:boolean"]],"^I","[name]"]],"^;",10],"~$sym-name?",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^13","^7","^8","^9",1,"^:",null,"^;",6],"~$inner",["^ ","^;",167,"^9",17,"^6","^14","^4","^5","^:",null],"~$sym-name-str",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^15","^7","^8","^9",1,"^:",null,"^;",70],"~$unwrap-meta-name",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^16","^7","^8","^9",1,"^:",null,"^;",192]]

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

57
trace-dv.janet Normal file
View file

@ -0,0 +1,57 @@
(use ./src/jolt/evaluator)
(use ./src/jolt/types)
(use ./src/jolt/reader)
(use ./src/jolt/api)
# Override resolve-sym with tracing
(def old-resolve-sym @{:private true})
# Monkey-patch eval-list to add tracing
(var orig-eval-list nil)
# Just add a direct trace in fn*
(def ctx (init))
(each fp ["/Users/yogthos/src/sci/src/sci/impl/macros.cljc"
"/Users/yogthos/src/sci/src/sci/impl/protocols.cljc"
"/Users/yogthos/src/sci/src/sci/impl/types.cljc"
"/Users/yogthos/src/sci/src/sci/impl/unrestrict.cljc"]
(def src (slurp fp))
(var s src)
(while (> (length (string/trim s)) 0)
(def [f r] (parse-next s)) (set s r)
(if (not (nil? f)) (protect (eval-form ctx @{} f)))))
(def vs (slurp "/Users/yogthos/src/sci/src/sci/impl/vars.cljc"))
(var s vs) (var c 0)
(while (and (> (length (string/trim s)) 0) (< c 27))
(def [f r] (parse-next s)) (set s r) (++ c)
(if (not (nil? f)) (protect (eval-form ctx @{} f))))
(def ls (slurp "/Users/yogthos/src/sci/src/sci/lang.cljc"))
(set s ls)
(while (> (length (string/trim s)) 0)
(def [f r] (parse-next s)) (set s r)
(if (not (nil? f)) (protect (eval-form ctx @{} f))))
# Load utils up to form 35
(def us (slurp "/Users/yogthos/src/sci/src/sci/impl/utils.cljc"))
(set s us) (set c 0)
(while (and (> (length (string/trim s)) 0) (< c 35))
(def [f r] (parse-next s)) (set s r) (++ c)
(if (not (nil? f))
(let [pr (protect (eval-form ctx @{} f))]
(if (not (pr 0))
(printf "utils[%d]: %q\n" c (pr 1))))))
# Now inspect the dynamic-var fn in detail
(def ns (ctx-find-ns ctx "sci.impl.utils"))
(def dv (ns-find ns "dynamic-var"))
(def dv-fn (var-get dv))
# Evaluate (dynamic-var 'foo) manually
(def quoted-foo @[{:jolt/type :symbol :ns nil :name "quote"} {:jolt/type :symbol :ns nil :name "foo"}])
(def dv-call @[{:jolt/type :symbol :ns nil :name "dynamic-var"} quoted-foo])
(printf "Calling dynamic-var('foo):\n")
(def r1 (protect (eval-form ctx @{} dv-call)))
(printf " result: %q\n" (if (r1 0) "OK" (r1 1)))