Skip to content

const_object_globalization: the borrow-leak gate misses a projection root #1819

Description

@gfx

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_leaksparam_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.rsborrows_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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions