Skip to content

fix(destructure): apply :or on absence, not on falsiness - #29

Open
BuddhiLW wants to merge 1 commit into
feat/sync-el-version-headersfrom
fix/destructuring-or-defaults
Open

fix(destructure): apply :or on absence, not on falsiness#29
BuddhiLW wants to merge 1 commit into
feat/sync-el-version-headersfrom
fix/destructuring-or-defaults

Conversation

@BuddhiLW

@BuddhiLW BuddhiLW commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes CLJEL-COMPILER map-destructuring-:or (20260710102141-320e86ab).
Fourth in the stack (#26#27#28 → this); bases retarget as each merges.

The bug

Clojure applies a destructuring default only when the key is absent. cljel
emitted (or (get m :x) 5), so a present falsy value was overridden:

(let [{:keys [x] :or {x 5}} {:x nil}] x)
;; => 5      wrong; Clojure gives nil

All four binding forms were affected: :keys, :strs, :syms and explicit.

The runtime half

Repointing the emitter at get's 3-arity is not enough on its own. clel-get
only distinguished absence from falsiness for hash tables — its list and
alist branches used the same or, so the bug would have survived for
alist-backed maps. Both branches now check presence, and index lookups tell a
nil element from an out-of-range index.

A second bug on the same line

The default was read with (get or-map sym), which cannot tell "no default"
from "the default is nil or false". :or {x false} was therefore dropped
entirely. It now uses contains?.

Structure

The four binding forms each carried their own copy of the default logic —
four places to get this right, and the reason one line had two bugs. They now
share one lookup, so the next change to default semantics happens once.

Evidence

Both new ERT tests fail against the previous runtime and pass against this
one. A compiled .cljel exercising all three binding forms was run in batch
Emacs:

present-nil   nil (want nil)     absent      5   (want 5)
present-val   3   (want 3)       strs nil    nil (want nil)
strs absent   1   (want 1)       expl nil    nil (want nil)
expl absent   2   (want 2)

610 Clojure tests, 3071 assertions, 0 failures. 30 ERT tests, 0 unexpected.

Clojure applies a destructuring default only when the key is ABSENT.
cljel emitted (or (get m :x) 5), so {:keys [x] :or {x 5}} over {:x nil}
bound 5 instead of nil. All four binding forms - :keys, :strs, :syms and
explicit - now reach get's 3-arity.

The runtime half matters too: clel-get only distinguished absence from
falsiness for hash tables. Its list and alist branches used the same
`or`, so a 3-arity call would have kept the bug for alist-backed maps.
Both now check presence.

A second bug fell out of the same line. The default was read with
(get or-map sym), which cannot tell "no default" from "the default is
nil or false", so :or {x false} was dropped entirely. Now contains?.

The four binding forms each carried their own copy of the default
logic; they now share one `lookup`, so the next change to default
semantics happens once.

Proven at the execution rung: both new ERT tests fail against the
previous runtime and pass against this one, and a compiled .cljel using
all three binding forms was run in batch Emacs.

Closes 20260710102141-320e86ab.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant