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 cf] (fn ([] (f)) ([x] (cf x)) ([x y] (f x y)))))
;; Canonical loop form: short-circuits on an empty/nil coll before examining n
;; (so (nthrest nil n) is nil without a number check), matching Clojure.
;; Matches Clojure exactly: n<=0 returns coll unchanged; for n>0 the walk yields
;; (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]
(loop [n n xs coll]
(if (and (pos? n) (seq xs))
(recur (dec n) (rest xs))
xs)))
(if (pos? n)
(or (loop [n n xs coll]
(let [s (and (pos? n) (seq xs))]
(if s (recur (dec n) (rest s)) (seq xs))))
(list))
coll))
(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"} ;(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
"Macro: (if-let [binding val-expr] then else?)"
[bindings then-form & else-forms]
(def form-sym (in bindings 0))
(def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"}
@[form-sym val-form]
@[{:jolt/type :symbol :ns nil :name "if"}
form-sym
then-form
(def temp (gensym "if_let__"))
@[(sym* "let*") @[temp val-form]
@[(sym* "if") temp
@[(sym* "let*") @[form-sym temp] then-form]
;else-forms]])
(defn core-when-let
@ -2177,22 +2182,21 @@
[bindings & body]
(def form-sym (in bindings 0))
(def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"}
@[form-sym val-form]
@[{:jolt/type :symbol :ns nil :name "when"}
form-sym
;body]])
(def temp (gensym "when_let__"))
@[(sym* "let*") @[temp val-form]
@[(sym* "if") temp
@[(sym* "let*") @[form-sym temp] @[(sym* "do") ;body]]
nil]])
(defn core-if-some
"Macro: (if-some [binding val-expr] then else?)"
[bindings then-form & else-forms]
(def form-sym (in bindings 0))
(def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"}
@[form-sym val-form]
@[{:jolt/type :symbol :ns nil :name "if"}
@[{:jolt/type :symbol :ns nil :name "some?"} form-sym]
then-form
(def temp (gensym "if_some__"))
@[(sym* "let*") @[temp val-form]
@[(sym* "if") @[(sym* "some?") temp]
@[(sym* "let*") @[form-sym temp] then-form]
;else-forms]])
(defn core-when-some
@ -2200,11 +2204,11 @@
[bindings & body]
(def form-sym (in bindings 0))
(def val-form (in bindings 1))
@[{:jolt/type :symbol :ns nil :name "let*"}
@[form-sym val-form]
@[{:jolt/type :symbol :ns nil :name "when"}
@[{:jolt/type :symbol :ns nil :name "some?"} form-sym]
;body]])
(def temp (gensym "when_some__"))
@[(sym* "let*") @[temp val-form]
@[(sym* "if") @[(sym* "some?") temp]
@[(sym* "let*") @[form-sym temp] @[(sym* "do") ;body]]
nil]])
(defn core-doto
"Macro: (doto obj (method args)...) → let obj, call methods, return obj"