fix nthrest empty-list semantics + if-let/when-let/if-some/when-some scoping

Two correctness bugs found while migrating:

- nthrest returned nil where Clojure returns () : for n>0 the walk yields
  (seq xs) wrapped in (or ... ()), so an exhausted/nil walk is () not nil
  (only n<=0 returns coll). Both the prior Janet impl and the first overlay
  port got this wrong. nthrest.cljc 13/1/0 -> 14/0/0.

- if-let/when-let/if-some/when-some leaked their binding into the else branch:
  they wrapped the whole if in (let* [name val] ...), so (let [x 5] (if-let
  [x nil] x x)) returned nil instead of 5. Fixed to bind a fresh temp around the
  if and rebind the name only inside the taken branch, matching Clojure.

conformance 218/218 x3, clojure-test-suite 3928 -> 3929.
This commit is contained in:
Yogthos 2026-06-06 23:42:08 -04:00
parent 185cf30c1d
commit f22eb660d3
2 changed files with 33 additions and 26 deletions

View file

@ -122,13 +122,16 @@
([f] (completing f identity)) ([f] (completing f identity))
([f cf] (fn ([] (f)) ([x] (cf x)) ([x y] (f x y))))) ([f cf] (fn ([] (f)) ([x] (cf x)) ([x y] (f x y)))))
;; Canonical loop form: short-circuits on an empty/nil coll before examining n ;; Matches Clojure exactly: n<=0 returns coll unchanged; for n>0 the walk yields
;; (so (nthrest nil n) is nil without a number check), matching Clojure. ;; (seq xs), and an exhausted/nil walk falls back to () via (or ... ()) — so
;; (nthrest nil 100) is () (not nil), while (nthrest nil 0) is nil.
(defn nthrest [coll n] (defn nthrest [coll n]
(loop [n n xs coll] (if (pos? n)
(if (and (pos? n) (seq xs)) (or (loop [n n xs coll]
(recur (dec n) (rest xs)) (let [s (and (pos? n) (seq xs))]
xs))) (if s (recur (dec n) (rest s)) (seq xs))))
(list))
coll))
(defn abs [x] (if (neg? x) (- 0 x) x)) (defn abs [x] (if (neg? x) (- 0 x) x))

View file

@ -2160,16 +2160,21 @@
{:jolt/type :symbol :ns nil :name "or__x"} {:jolt/type :symbol :ns nil :name "or__x"}
@[{:jolt/type :symbol :ns nil :name "or"} ;(tuple/slice exprs 1)]]]))) @[{:jolt/type :symbol :ns nil :name "or"} ;(tuple/slice exprs 1)]]])))
# if-let / when-let / if-some / when-some bind the name ONLY in the then/body
# branch — the else branch must see the surrounding scope, not the binding (so
# (let [x 5] (if-let [x nil] x x)) returns 5, like Clojure). Achieved with a fresh
# temp around the if; the name is rebound to the temp inside the taken branch only.
(defn- sym* [name] {:jolt/type :symbol :ns nil :name name})
(defn core-if-let (defn core-if-let
"Macro: (if-let [binding val-expr] then else?)" "Macro: (if-let [binding val-expr] then else?)"
[bindings then-form & else-forms] [bindings then-form & else-forms]
(def form-sym (in bindings 0)) (def form-sym (in bindings 0))
(def val-form (in bindings 1)) (def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"} (def temp (gensym "if_let__"))
@[form-sym val-form] @[(sym* "let*") @[temp val-form]
@[{:jolt/type :symbol :ns nil :name "if"} @[(sym* "if") temp
form-sym @[(sym* "let*") @[form-sym temp] then-form]
then-form
;else-forms]]) ;else-forms]])
(defn core-when-let (defn core-when-let
@ -2177,22 +2182,21 @@
[bindings & body] [bindings & body]
(def form-sym (in bindings 0)) (def form-sym (in bindings 0))
(def val-form (in bindings 1)) (def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"} (def temp (gensym "when_let__"))
@[form-sym val-form] @[(sym* "let*") @[temp val-form]
@[{:jolt/type :symbol :ns nil :name "when"} @[(sym* "if") temp
form-sym @[(sym* "let*") @[form-sym temp] @[(sym* "do") ;body]]
;body]]) nil]])
(defn core-if-some (defn core-if-some
"Macro: (if-some [binding val-expr] then else?)" "Macro: (if-some [binding val-expr] then else?)"
[bindings then-form & else-forms] [bindings then-form & else-forms]
(def form-sym (in bindings 0)) (def form-sym (in bindings 0))
(def val-form (in bindings 1)) (def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"} (def temp (gensym "if_some__"))
@[form-sym val-form] @[(sym* "let*") @[temp val-form]
@[{:jolt/type :symbol :ns nil :name "if"} @[(sym* "if") @[(sym* "some?") temp]
@[{:jolt/type :symbol :ns nil :name "some?"} form-sym] @[(sym* "let*") @[form-sym temp] then-form]
then-form
;else-forms]]) ;else-forms]])
(defn core-when-some (defn core-when-some
@ -2200,11 +2204,11 @@
[bindings & body] [bindings & body]
(def form-sym (in bindings 0)) (def form-sym (in bindings 0))
(def val-form (in bindings 1)) (def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"} (def temp (gensym "when_some__"))
@[form-sym val-form] @[(sym* "let*") @[temp val-form]
@[{:jolt/type :symbol :ns nil :name "when"} @[(sym* "if") @[(sym* "some?") temp]
@[{:jolt/type :symbol :ns nil :name "some?"} form-sym] @[(sym* "let*") @[form-sym temp] @[(sym* "do") ;body]]
;body]]) nil]])
(defn core-doto (defn core-doto
"Macro: (doto obj (method args)...) → let obj, call methods, return obj" "Macro: (doto obj (method args)...) → let obj, call methods, return obj"