Context
const_object_globalization hoists a constant aggregate into a module global. That is only sound while nothing hands the hoisted object's storage back out to a caller that mutates it — otherwise every caller shares one object. SequenceLiteralBuilder::build(&self) -> List<T> { return *self; } is exactly that shape: it returns the referent, and the caller keeps no copy because the literal it borrowed was fresh. core:zlib's build_huffman_tree accumulated its sym_list across calls that way and wrote a literal-tree symbol index into the 30-element distance-tree lengths.
The gate added for that (local_leaks_through_call / ref_args_that_escape, both via callee_ref_param_leaks → param_storage_escapes) asks, per call argument, whether the callee delivers its parameter's referent out.
The gap
The caller-side predicate borrows_local matches only x, &x and &mut x. The callee-side one it is paired with, projection_roots_at, already reads a field, index, cast or deref chain as naming the root's storage. So a constant borrowed through a projection slips the gate:
struct Holder { inner: List<i32> }
let h = Holder { inner: [] };
h.inner.build() // borrows h's storage; `borrows_local` sees no `h`
h is still hoisted, the callee hands the referent back, and a later push accumulates across calls — the build_huffman_tree failure mode reached through a projection instead of a bare local.
Raised in review of #1810.
Why it is not fixed there
Walking FieldAccess / Index / Cast in borrows_local closes it and costs four fixtures:
const_global_scalar_read
const_object_globalization_large_array
const_object_globalization_large_ref_array
const_object_globalization_long_literal_let
The loss is the widening multiplied by the callee side's over-approximation: callee_ref_param_leaks counts an unknown callee as leaking, so once projections are in scope, names[i % 2] handed to Display::fmt as a receiver is enough to refuse the let names hoist. const_object_globalization_long_literal_let is precisely that program. While only bare locals were in scope, that over-approximation never showed.
No reproduction exists for the projection shape today, so the branch keeps the bare-local test rather than trading four real optimizations for a theoretical hole.
What to do
Close the gap without giving up the hoists. The gate is what needs sharpening, not the widening that exposed it:
- Make
callee_ref_param_leaks precise enough that an ordinary consumer stops reading as a leak — a trait method whose body is known, a &self receiver that only reads. The unknown-callee verdict is what makes the widened predicate expensive.
- Then widen
borrows_local to the projection chains projection_roots_at already walks, and confirm the four fixtures still hoist.
- Add an e2e fixture for the projection shape, red before the change.
wado run -O2 --optimize-inline-threshold 0 is the configuration that keeps build a call and makes the bare-local version of this bug observable (see tests/integration/const_global_builder_alias.rs).
Pointers
wado-compiler/src/optimize/const_object_globalization.rs — borrows_local, ref_args_that_escape, local_leaks_through_call, callee_ref_param_leaks, param_storage_escapes, projection_roots_at
wado-compiler/tests/integration/const_global_builder_alias.rs — the bare-local regression test
docs/wep-2026-05-31-const-object-globalization.md
Context
const_object_globalizationhoists a constant aggregate into a module global. That is only sound while nothing hands the hoisted object's storage back out to a caller that mutates it — otherwise every caller shares one object.SequenceLiteralBuilder::build(&self) -> List<T> { return *self; }is exactly that shape: it returns the referent, and the caller keeps no copy because the literal it borrowed was fresh.core:zlib'sbuild_huffman_treeaccumulated itssym_listacross calls that way and wrote a literal-tree symbol index into the 30-element distance-tree lengths.The gate added for that (
local_leaks_through_call/ref_args_that_escape, both viacallee_ref_param_leaks→param_storage_escapes) asks, per call argument, whether the callee delivers its parameter's referent out.The gap
The caller-side predicate
borrows_localmatches onlyx,&xand&mut x. The callee-side one it is paired with,projection_roots_at, already reads a field, index, cast or deref chain as naming the root's storage. So a constant borrowed through a projection slips the gate:his still hoisted, the callee hands the referent back, and a laterpushaccumulates across calls — thebuild_huffman_treefailure mode reached through a projection instead of a bare local.Raised in review of #1810.
Why it is not fixed there
Walking
FieldAccess/Index/Castinborrows_localcloses it and costs four fixtures:The loss is the widening multiplied by the callee side's over-approximation:
callee_ref_param_leakscounts an unknown callee as leaking, so once projections are in scope,names[i % 2]handed toDisplay::fmtas a receiver is enough to refuse thelet nameshoist.const_object_globalization_long_literal_letis precisely that program. While only bare locals were in scope, that over-approximation never showed.No reproduction exists for the projection shape today, so the branch keeps the bare-local test rather than trading four real optimizations for a theoretical hole.
What to do
Close the gap without giving up the hoists. The gate is what needs sharpening, not the widening that exposed it:
callee_ref_param_leaksprecise enough that an ordinary consumer stops reading as a leak — a trait method whose body is known, a&selfreceiver that only reads. The unknown-callee verdict is what makes the widened predicate expensive.borrows_localto the projection chainsprojection_roots_atalready walks, and confirm the four fixtures still hoist.wado run -O2 --optimize-inline-threshold 0is the configuration that keepsbuilda call and makes the bare-local version of this bug observable (seetests/integration/const_global_builder_alias.rs).Pointers
wado-compiler/src/optimize/const_object_globalization.rs—borrows_local,ref_args_that_escape,local_leaks_through_call,callee_ref_param_leaks,param_storage_escapes,projection_roots_atwado-compiler/tests/integration/const_global_builder_alias.rs— the bare-local regression testdocs/wep-2026-05-31-const-object-globalization.md