Skip to content

test(gc): temp-root codegen contract lives only in crates/perry-codegen/tests/, which does not run per-PR #6988

Closed

Description

@proggeramlug

Split out of #6983, where it was raised in review and explicitly declined rather
than silently skipped.

The gap

All of the temp_root family's codegen coverage lives in
crates/perry-codegen/tests/ — temp_root_argument_temporaries.rs (#6972) and
temp_root_operand_temporaries.rs (#6983), 10 tests between them. Per CLAUDE.md:

Integration suites under crates/*/tests/*.rs do not run per-PR (nightly/tag
only) — a regression there can land green and sit red for days. Prefer putting
acceptance coverage in cargo-test-visible unit tests (#5960).

So the assertions pinning the rooting emission contract — push → re-read →
consuming call → release, and the interleaving requirement — are CI-invisible on
the PRs most likely to break them. The runtime half (gc::tests::temp_roots,
in src/) does run per-PR; the codegen half does not.

That matters more than usual for this family, because the failure mode is
silent. A lowering path that quietly goes back to threading a value through an
SSA register produces correct output under the default configuration — the
conservative native-stack scan pins it by accident — and only diverges under
PERRY_CONSERVATIVE_STACK_SCAN=off or an evacuating minor. The emission-contract
tests are the fast signal; the corpus arms are the slow one.

Why it was not fixed in #6983

The assertions need a built Module plus compile_module, so moving them into
src/ unit tests means either constructing a FnCtx by hand or duplicating the
~90 lines of entry_opts() / module_with_init() harness. Doing that once for
the whole family is worth it; doing it piecemeal for one PR's three tests is not,
and would leave the two files inconsistent.

Suggested shape

Extract the harness (entry_opts, module_with_init, ir_for, plus the
ir_for_new class builder from #6983) into something both a src/ unit-test
module and the existing integration tests can call — a #[cfg(test)] helper
module in perry-codegen, or a small pub(crate) test-support module — then
move at least the ordering/interleaving assertions across:

The negative gates matter as much as the positive ones — they are what keeps the
fix from silently becoming a cost on hot paths.

Related: #6951, #6972, #6975, #6983, #5960.

Activity

  1. jdalton commented on Jul 30, 2026

    @jdalton
    Contributor

    Heads-up on a small campaign I'm lining up around the rooting family, routed through this issue first — flagging it here so nobody double-works it and so the sequencing is easy to veto.

    The plan, in order:

    1. test(compat): pinned-Node builtin-module compatibility matrix #7074 (Node builtin compat matrix) — rebased onto current main today and mergeable again; independent measurement infra, not part of the stack.
    2. test(gc): assert the child relocation, not a whole-process copied count #7092 (just opened) — fixes gc test: large_object_old_born_array_slot_write_keeps_young_child_alive asserts an exact whole-nursery copied_objects count and fails on macOS (4249 vs 1) — pre-existing on main #7060 so cargo test -p perry-runtime is green on macOS while working in the GC area. Also independent.
    3. This issue (test(gc): temp-root codegen contract lives only in crates/perry-codegen/tests/, which does not run per-PR #6988) — extract the temp-root harness so the rooting-contract assertions run in per-PR cargo test rather than nightly-only crates/perry-codegen/tests/. This becomes the base of a stack.
    4. Stacked on it, in dependency order: gc: constructor arguments on lower_new's non-class branches (Readline / imported ctor / new Function) and builtin.rs are still not precise roots #6986 (constructor-argument rooting on lower_new's non-class branches), gc: a closure's captured this is stored in an alloca that is not shadow-bound (methods bind theirs) — same family as #7055 #7066 (closure-captured this shadow binding), and runtime: a fourth unrooted-operand family — js_string_coerce as a plain ToString argument coercion (string built-ins, constructors) #6949 (the js_string_coerce ToString-coercion rooting family) — each carrying a sabotage-verified regression test that lands in the per-PR-visible harness from step 3.
    How the stack works from a fork

    I contribute from a fork and can't create base branches in this repo, so this is an annotated stack rather than a GitHub-native one: each PR targets main, is opened in dependency order with a "stacked on #X" note, and until the layer below merges its diff will include that layer's commits. As each layer squash-merges I'll rebase --onto the layers above so the diffs collapse to just their own change. If any of these is already spoken for, or you'd rather the harness extraction wait, say the word and I'll reorder.

  2. proggeramlug commented on Aug 9, 2026

    @proggeramlug
    ContributorAuthor

    Done in #7675.

    crates/perry-codegen/tests/temp_root_argument_temporaries.rs is deleted. Its seven tests live in crates/perry-codegen/src/temp_root_coverage/ — an in-crate #[cfg(test)] module, so they run in the required per-PR cargo-test job instead of the nightly/tag-only integration tier (#5960).

    The shape follows #7653's native_root_coverage rather than being invented: src/ module, hand-built HIR, emit_ir_only: true, no perry compile subprocess. The ~90 lines of entry_opts() / module_with_init() harness the original issue balked at are written once there.

    Coverage moved, per the issue's list:

    • argument accumulator — console_argument_accumulator_is_rooted_re_read_and_written_back: alloc → store into slot → re-read → push → write back → re-read → consume → release, with the ordering asserted between events rather than between textual offsets.
    • the negative gates — a_non_allocating_element_list_pays_for_no_temp_root, operands::a_proven_index_typed_array_read_is_not_temp_rooted, operands::a_buffer_index_read_is_not_temp_rooted. Each is paired with a structurally identical twin that must go the other way (an_array_literal_roots_elements_before_an_allocating_element, operands::a_symbol_keyed_typed_array_read_is_still_temp_rooted, operands::an_unproven_key_typed_array_read_is_still_temp_rooted), so a compiler that roots nothing fails one half and one that roots everything fails the other.

    Each test runs once per lowering through under_both_lowerings, so a PERRY_RS4GC=0 bisection and CI assert the same thing — the divergence #7493 spent a week diagnosing.

    The MapSet and constructor-argument interleaving assertions the issue also names stay in tests/temp_root_operand_temporaries.rs, re-pointed at the same reader (#7503) and green under both lowerings. They are the half that still needs the Class/Module builders; moving them is a follow-up, not a blocker — the family now has a per-PR-visible floor where it had none.

    One caveat worth recording: this does not by itself put the whole family in the required tier. What it puts there is the emission contract's core plus its negative gates, which is what the issue asked for ("move at least the ordering/interleaving assertions across"). #7507 covers getting the remaining suites scoped into e2e-scoped on a perry-codegen source diff.

  3. proggeramlug commented on Aug 9, 2026

    @proggeramlug
    ContributorAuthor

    Merged as 6cdcd79 (#7675). The temp-root emission contract now runs in the required per-PR cargo-test job via crates/perry-codegen/src/temp_root_coverage/, following #7653's native_root_coverage pattern; tests/temp_root_argument_temporaries.rs is deleted. Details in the comment above.

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