Problem
Regression from #153 (reduceObjectProp, merged in #157). The Linker synthesizes exactly one shape carrying Just Inline.Always: a foreign name's alias binding, QName moduleName name = ObjectProp (Just Always) (Ref foreign) (PropName name) (see Note [Foreign bindings structure emitted by the Linker] in Linker.hs). isInlinableExpr treats that annotation as "always duplicate this expression at every use site", bypassing the use-count check entirely.
reduceObjectProp folds ObjectProp _ann (LiteralObject _ props) prop to List.lookup prop props, returning the field's value verbatim, annotation included. When a record literal has a field that happens to be one of these Linker-synthesized Always-marked foreign aliases, and something elsewhere projects that same field back out, the projection's result inherits Just Always even though the outer projection itself was never annotated. That annotation then becomes the root annotation of whatever binding the projection belongs to, making isInlinableExpr return True regardless of how many times that binding is actually used.
Concretely, in Golden.TailRecM2Shadow.Test, the Semiring Int dictionary literal has add: Data.Semiring.foreign.intAdd as a field. The module's own top-level add binding projects that field out of the (inlined) dictionary and is referenced twice. Before #157, add's root annotation stayed Nothing, so add correctly stayed a single shared top-level binding. After #157, the fold gives add's root the dictionary field's own Just Always, so add is unconditionally duplicated at both call sites, and DCE then drops the now-unreferenced Data.Semiring.foreign table entirely — the foreign function's raw source ends up duplicated inline at each call site instead of one shared entry in the foreign import table:
-- before
M.Golden_TailRecM2Shadow_Test_add = M.Data_Semiring_foreign.intAdd
a = M.Golden_TailRecM2Shadow_Test_add(o_S_31.a)(o_S_31.b)
-- after
a = (function(x) return function(y) return x + y end end)(o_S_31.a)(o_S_31.b)
For a foreign function with a nontrivial implementation this duplicates real code at every call site instead of one shared table entry.
Approach
The folded result must not inherit an annotation that was never meant to describe it. reduceObjectProp should discard the projected field's own annotation rather than letting it become the root annotation of the rewrite's result.
Prerequisites / Relations
Optimizer bug, independent in the dependency graph. Related to #171 — both concern the interaction of the Linker's auto-@inline always annotation with the inliner — but distinct: this one is an annotation leak in reduceObjectProp, whereas #171 is the bare-Ref alias interaction.
Verification / Measurement
Projecting a field whose value is an Always-marked foreign alias no longer marks the enclosing binding inlinable. In Golden.TailRecM2Shadow.Test, add stays a single shared top-level binding (M.Golden_TailRecM2Shadow_Test_add = M.Data_Semiring_foreign.intAdd) referenced from both call sites, rather than the foreign lambda being duplicated inline at each.
Problem
Regression from #153 (
reduceObjectProp, merged in #157). The Linker synthesizes exactly one shape carryingJust Inline.Always: a foreign name's alias binding,QName moduleName name = ObjectProp (Just Always) (Ref foreign) (PropName name)(see Note [Foreign bindings structure emitted by the Linker] inLinker.hs).isInlinableExprtreats that annotation as "always duplicate this expression at every use site", bypassing the use-count check entirely.reduceObjectPropfoldsObjectProp _ann (LiteralObject _ props) proptoList.lookup prop props, returning the field's value verbatim, annotation included. When a record literal has a field that happens to be one of these Linker-synthesizedAlways-marked foreign aliases, and something elsewhere projects that same field back out, the projection's result inheritsJust Alwayseven though the outer projection itself was never annotated. That annotation then becomes the root annotation of whatever binding the projection belongs to, makingisInlinableExprreturnTrueregardless of how many times that binding is actually used.Concretely, in
Golden.TailRecM2Shadow.Test, theSemiring Intdictionary literal hasadd: Data.Semiring.foreign.intAddas a field. The module's own top-leveladdbinding projects that field out of the (inlined) dictionary and is referenced twice. Before #157,add's root annotation stayedNothing, soaddcorrectly stayed a single shared top-level binding. After #157, the fold givesadd's root the dictionary field's ownJust Always, soaddis unconditionally duplicated at both call sites, and DCE then drops the now-unreferencedData.Semiring.foreigntable entirely — the foreign function's raw source ends up duplicated inline at each call site instead of one shared entry in the foreign import table:For a foreign function with a nontrivial implementation this duplicates real code at every call site instead of one shared table entry.
Approach
The folded result must not inherit an annotation that was never meant to describe it.
reduceObjectPropshould discard the projected field's own annotation rather than letting it become the root annotation of the rewrite's result.Prerequisites / Relations
Optimizer bug, independent in the dependency graph. Related to #171 — both concern the interaction of the Linker's auto-
@inline alwaysannotation with the inliner — but distinct: this one is an annotation leak inreduceObjectProp, whereas #171 is the bare-Refalias interaction.Verification / Measurement
Projecting a field whose value is an
Always-marked foreign alias no longer marks the enclosing binding inlinable. InGolden.TailRecM2Shadow.Test,addstays a single shared top-level binding (M.Golden_TailRecM2Shadow_Test_add = M.Data_Semiring_foreign.intAdd) referenced from both call sites, rather than the foreign lambda being duplicated inline at each.