Skip to content

Keep the projection's annotation when folding a record projection - #193

Merged
Unisay merged 1 commit into
mainfrom
issue-169/object-prop-annotation-leak
Jul 6, 2026
Merged

Keep the projection's annotation when folding a record projection#193
Unisay merged 1 commit into
mainfrom
issue-169/object-prop-annotation-leak

Conversation

@Unisay

@Unisay Unisay commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #169.

The leak

reduceObjectProp folded { foo: v }.foo to v verbatim, root annotation included. The Linker marks exactly one shape with Just Always: a foreign accessor alias, ObjectProp (Just Always) (Ref foreign) (PropName name) (see Note [Foreign bindings structure emitted by the Linker]). The leak takes three hops: the inliner copies such an accessor into a record literal's field (a typeclass dictionary), something projects that field back out, and the fold hands the field's Just Always to the result. That annotation then becomes the root annotation of whatever binding received the fold, so isInlinableExpr passes unconditionally: the binding gets duplicated at every use site and dropped from the uber-module bindings, regardless of its use count.

The fix

The fold result now carries the projection node's own annotation. This needed a new setAnn in IR.Types (the symmetric setter to getAnn; nothing on main could replace a root annotation without touching the rest of the tree), applied on both the LiteralObject and ObjectUpdate paths.

Why the projection's annotation and not a plain reset to noAnn: the rewrite result occupies the projection's position in the tree, and the fall-through ObjectUpdate branch already keeps ann. A reset would also silently swallow an explicit user @inline pragma placed on the projection. A test pins this choice: a field annotated Just Always projected by a node annotated Just Never folds to a Just Never result, which distinguishes the fix both from the bug (Just Always) and from the reset variant (Nothing).

Tests, demonstrated red first

Three rule-level tests (leak out of a record literal, leak out of an update patch, the semantics pin above) and one UberModule-level test: a binding referenced twice whose RHS folds to an Always-annotated field must survive optimizedUberModule. All four failed before the fix (the first two returned Just Always) and pass after it.

Goldens

No golden changed. The issue predicted that Golden.TailRecM2Shadow.Test would get its shared add binding back, but on current main that chain no longer routes the leaked annotation through a shared binding: optimizer changes merged since the issue was filed reshaped it. I verified this directly by running the golden on a pre-fix build; its output is identical to the fixed build's. The leak is latent on the committed test programs, which is why the regression tests pin it at the rule and pipeline level instead. The eval/golden.txt oracles are untouched.

Relations

Related to #171, which concerns the same Linker Just Always annotation but a different interaction (the bare Ref alias), and stays open.

#169)

reduceObjectProp folded a record projection to the field's value
verbatim, root annotation included. The Linker marks foreign accessor
aliases Just Always (see Note [Foreign bindings structure emitted by
the Linker]); once such an accessor is inlined into a record literal's
field and the field is projected back out, the leaked Just Always
became the root annotation of whatever binding received the fold,
making isInlinableExpr pass unconditionally: the binding was duplicated
at every use site and dropped from the uber-module bindings regardless
of its use count.

The fold result now carries the projection node's own annotation (new
setAnn in IR.Types, the symmetric setter to getAnn), on both the
LiteralObject and ObjectUpdate paths. Carrying the projection's
annotation rather than resetting to noAnn matches the fall-through
ObjectUpdate branch, which already keeps ann, and respects an explicit
user @inline pragma placed on the projection.

Demonstrated red first: three rule-level tests (leak out of a literal,
leak out of an update patch, an annotated projection keeping its own
annotation) plus an UberModule-level test where a twice-referenced
binding whose RHS folds to an Always-annotated field must survive
optimizedUberModule. All four failed before the fix and pass after it.

No golden churn: the TailRecM2Shadow chain cited by the issue no longer
routes through a shared binding on current main (verified by running
the golden on a pre-fix build: output is identical), so the leak is
latent there; the new tests pin it directly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an optimizer annotation leak in reduceObjectProp where folding a record projection could incorrectly propagate a field’s root annotation (notably the Linker’s Just Always foreign-accessor annotation) onto unrelated expressions/bindings, causing unintended unconditional inlining.

Changes:

  • Preserve the projection node’s root annotation on the folded result when reducing ObjectProp through LiteralObject and ObjectUpdate.
  • Add setAnn to IR.Types as a root-annotation setter symmetric to getAnn.
  • Add regression tests covering literal and update-folding cases plus an end-to-end optimizedUberModule manifestation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
test/Language/PureScript/Backend/IR/Optimizer/Spec.hs Adds regression tests ensuring reduceObjectProp does not leak Just Always and preserves the projection’s own annotation; includes an UberModule-level test for the previously observed inlining/DCE fallout.
lib/Language/PureScript/Backend/IR/Types.hs Introduces setAnn to replace only the root annotation of a RawExp while leaving all nested annotations intact.
lib/Language/PureScript/Backend/IR/Optimizer.hs Updates reduceObjectProp to use setAnn so folded projections retain the projection’s annotation rather than inheriting the projected field’s.

@Unisay Unisay self-assigned this Jul 6, 2026
@Unisay
Unisay marked this pull request as ready for review July 6, 2026 18:09
@Unisay
Unisay merged commit cf82642 into main Jul 6, 2026
3 checks passed
@Unisay
Unisay deleted the issue-169/object-prop-annotation-leak branch July 6, 2026 18:11
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.

reduceObjectProp leaks the FFI @inline always annotation onto unrelated bindings, forcing unconditional duplication

2 participants