Skip to content

fix(tiers): evaluate :or destructuring defaults eagerly in every tier - #367

Merged
csm merged 1 commit into
mainfrom
claude/issue-363-0nskso
Sep 1, 2026
Merged

fix(tiers): evaluate :or destructuring defaults eagerly in every tier#367
csm merged 1 commit into
mainfrom
claude/issue-363-0nskso

Conversation

@csm

@csm csm commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Clojure's destructure expands a symbol carrying an :or entry to
(get m :k default). get is an ordinary function, so the default is
evaluated whether or not the key is present. The tree-walker does that;
the compiled tiers did not, and the two disagreed:

  • lower_with_default lowered the default into the then arm of an
    IsNil branch, so it only ran on a miss. Lower it into the current
    block instead — straight-line and unconditional — and keep the branch
    purely as a select over two already-computed values.

  • execute_ir bound the arity's destructuring patterns into the env via
    bind_fn_params on top of the prologue the lowerer emits, so with the
    eager lowering above each default would run twice per call. The ANF
    lowerer never emits LoadLocal, so those env bindings are never read:
    bind only the named params there (bind_fn_params_positional), and let
    the prologue own the destructuring.

Because tier-up happens partway through a run, and lowering is driven by a
background worker, the old split made a side-effecting or throwing default
fire a nondeterministic number of times: the issue's reproduction reported
133/130/140/130/144 evaluations out of 200 calls across five runs of the
same binary. It now reports 200 in every tier and every run.

Pinned in two places: execution_tier_parity gains an :or case with a
side-effect counter, so tree-walk, eager IR, JIT and AOT must agree on the
count (it fails without either half of this fix), and a new
destructure_or_default_eager test runs the discriminating programs
through the tree-walker and the IR tier side by side.

Fixes #363

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01NLQqFN41sWfs1hF5uyfN8N

Clojure's `destructure` expands a symbol carrying an `:or` entry to
`(get m :k default)`.  `get` is an ordinary function, so the default is
evaluated whether or not the key is present.  The tree-walker does that;
the compiled tiers did not, and the two disagreed:

  - `lower_with_default` lowered the default into the `then` arm of an
    `IsNil` branch, so it only ran on a miss.  Lower it into the current
    block instead — straight-line and unconditional — and keep the branch
    purely as a select over two already-computed values.

  - `execute_ir` bound the arity's destructuring patterns into the env via
    `bind_fn_params` on top of the prologue the lowerer emits, so with the
    eager lowering above each default would run twice per call.  The ANF
    lowerer never emits `LoadLocal`, so those env bindings are never read:
    bind only the named params there (`bind_fn_params_positional`), and let
    the prologue own the destructuring.

Because tier-up happens partway through a run, and lowering is driven by a
background worker, the old split made a side-effecting or throwing default
fire a nondeterministic number of times: the issue's reproduction reported
133/130/140/130/144 evaluations out of 200 calls across five runs of the
same binary.  It now reports 200 in every tier and every run.

Pinned in two places: `execution_tier_parity` gains an `:or` case with a
side-effect counter, so tree-walk, eager IR, JIT and AOT must agree on the
count (it fails without either half of this fix), and a new
`destructure_or_default_eager` test runs the discriminating programs
through the tree-walker and the IR tier side by side.

Fixes #363

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLQqFN41sWfs1hF5uyfN8N
@csm
csm merged commit 29b8da3 into main Sep 1, 2026
5 checks passed
@csm
csm deleted the claude/issue-363-0nskso branch September 1, 2026 18:33
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.

Tier divergence: :or destructuring defaults evaluate eagerly in the interpreter but lazily in the IR tier

2 participants