Skip to content

fix(ir): preserve capture-free closure identity - #779

Open
nnunley wants to merge 1 commit into
nooga:mainfrom
nnunley:fix/ir-closure-identity
Open

fix(ir): preserve capture-free closure identity#779
nnunley wants to merge 1 commit into
nooga:mainfrom
nnunley:fix/ir-closure-identity

Conversation

@nnunley

@nnunley nnunley commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Why

Under *ir-compile*, a capture-free fn* literal is a :const node carrying a :fn-template aux. The lowering treated it like any other cheap replayable constant: multiple local references re-ran template-value at each use site, manufacturing a distinct *vm.Func per reference.

Observable damage: closure identity breaks. (let* [f (fn* [x] ...) g f] (identical? f g)) is false under IR compilation and true under the direct compiler; = on the rebound closure fails the same way.

What changed

pkg/rt/core/ir/lower.lg:

  • fn-template-const? recognizes :fn-template/:multi-fn-template const nodes.
  • The cheap-load rematerialization path excludes them, so a function literal is materialized once per lowering site and every local reuse goes through DUP_NTH of that one value.
  • The use-count sink rule treats a fn-template const as always worth a slot: it is an allocation with observable identity, not a replayable scalar.

Distinct textual fn* literals still produce distinct closures ((identical? (fn* [x] x) (fn* [x] x)) stays false).

pkg/rt/generated.manifest / generated.sums are refreshed because lower.lg is a declared generator input.

Verification

New TestIRBytecodePreservesCaptureFreeClosureIdentity in pkg/ir/lisp_closure_template_test.go pins rebinding identity, multi-reference identity, and the distinct-sites negative case under *ir-compile*.

make generate twice is deterministic, make check-generated passes, and go test ./... including the e2e suite passes on this head.

Complementary to #767: that fixes capturing closures at disagreeing joins in lower_go.lg; this fixes capture-free closure identity in bytecode lower.lg. No file overlap.

@nnunley
nnunley force-pushed the fix/ir-closure-identity branch from ba4c7e5 to 3ecd2c7 Compare August 24, 2026 19:50

@nooga nooga left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Identity across rebinding, multi-reference reuse via DUP_NTH, and the distinct-literal negative case are all covered by TestIRBytecodePreservesCaptureFreeClosureIdentity; read lower.lg's diff independently and found nothing. Only the manifest conflicts. Merging after a rebase.

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.

2 participants