Skip to content

repsel: dependency JS is walled by rule 1 (unbound allocations), not containment — 506 of 746 candidates #7152

Closed

Description

@proggeramlug

#7139 recovered rule-5 eligibility for 84 of 85 CommonJS dependency modules and
recorded that of the ~103 Ptr<Shape> candidates it freed, 100 were
immediately re-denied by rule 2
— reading as "containment is the wall in
dependency JS". Track D (#7034 §3, PR #7149) was scheduled on that basis.

Measured, it is not containment. Two compiler arms — #7139 alone, and
#7139 + #7149 — over 180 real __esModule CJS modules from
real-apps/scriptc/node_modules (perry compile m.js --opt-report --no-link,
180/180 compiled in both, every count identical between arms):

bucket candidates
rule 1 — allocated in expression position, never bound to a local 506
rule 5 — module barrier still armed 99
rule 2 — bare reference 130
rule 2 — call argument 5
rule 2 — array element 1
rule 2 — closure capture 1
rule 2 — undeclared property 1
total candidates 746
selected / consumed 3 / 0

Two things fall out.

1. Rule 1 is the wall, and its shape is known

The 506 unbound allocations break down by position:

position count
constructor argument 182
statement 162
call argument 84
return 64
array element 8
initializer 6

new X(…) written directly into an argument, a statement, or a return, never
bound to a let/const. Rule 1 needs a local to anchor the proof to, and
transpiled/minified code does not create one. This is the .map(x => ({…})) /
return {…} idiom the report already names — at 68% of all candidates it is
larger than every containment position combined by a factor of ~3.5.

2. The rule-2 "bare reference" denials are Perry's own scaffolding

All 130 are the local __cjs_module. That is the CommonJS wrapper's own
binding, not user code — the same family #7139 was narrowing. Whether it is
worth exempting depends on whether __cjs_module's object is ever a useful
Ptr<Shape> candidate at all; if it is not, it should not be a candidate,
and the 130 denials are noise in every report.

Caveat on the corpus

This is not #7139's exact 85-module set (that selection was
"self-contained __esModule leaves"; this one is every third __esModule file
by sorted path, requires allowed). A tighter 82-module self-contained set gave
the same shape: 185 candidates, 100 rule 1, 82 rule 2 — all 82 __cjs_module
bare references, 0 element. The conclusion does not depend on the sample.

What this changes

Nothing about #7149 — it pays on app-shaped TypeScript, where it removes every
guard diamond from a build-then-consume loop. But call argument (#7034 §1)
should not be scheduled on the assumption that it unlocks dependency JS
either
: it is 5 of 746. The dependency-JS lever is rule 1, and its fix is a
different mechanism — anchoring the proof to an allocation site rather than to
a binding.

Activity

  1. proggeramlug commented on Aug 1, 2026

    @proggeramlug
    ContributorAuthor

    PR #7171 handles §2 (the __cjs_module bare references) and, in doing so,
    changes the arithmetic under §1. Two things worth recording here before anyone
    schedules work off the 506.

    1. The bare references are not exemptable, only un-candidate-able — and that
    is now done.
    The reference is var module = __cjs_module;, the statement
    cjs_wrap's own preamble emits immediately after the record. module is a
    reassignable var that the same preamble stores into require.main, and that
    CJS bodies write module.exports = X through; real packages also read
    module.id / .filename / .loaded. The record genuinely escapes for its
    whole life, so no narrowing of rule 2 could promote it — and promotion would
    buy nothing anyway (a one-field record, three reads at module init, loop depth
    0). So #7171 drops it at the seed rather than exempting it at the rule, which
    is what this issue proposed. The recogniser requires the aliasing statement to
    be present, which is what makes the drop provably fact-neutral: that statement
    is the denial.

    2. The rule-1 "constructor argument" bucket is the same scaffolding, and the
    506 needs re-deriving rather than adjusting.
    const __cjs_module = { exports: {} } contains an inner {} allocated in constructor-argument
    position — one per CommonJS module, exactly the shape rule 1 counts as an
    unbound allocation. On a 195-module sample (every third __esModule file by
    sorted path; not this issue's 180-module set — that tree has drifted):

    bucket before #7171 after
    rule 2 — bare reference 140 4
    rule 1 — unbound alloc, constructor argument 196 8
    rule 5 — module barrier 187 132
    rule 1 — unbound alloc, all positions 885 759
    total candidates 1231 914

    188 of the 194 object literal { ... } constructor-argument rows were that
    inner {}. 136 of the 140 bare references and 55 of the rule-5 denials were
    __cjs_module. 379 rows, ~2 per module, 31 % of every Ptr<Shape> candidate
    in the corpus, all of it Perry's own template.

    A caveat that matters for anyone re-measuring: the report dedups
    allocation-site rows per function on (module, function, name, position, rule)
    and every object literal renders as object literal { ... }, so each wrapped
    module contributes exactly one alloc-site row
    . Which context that row reports
    is whichever preamble literal the walk reaches first. Two consequences:

    What did not change: nothing was promoted. Selected/consumed are 3/11 in
    both arms on every module in the corpus, by construction — every suppressed
    value was already denied. This is a report-accuracy fix, not a performance one.
    The dependency-JS lever is still rule 1, and still needs the different
    mechanism this issue describes (anchoring the proof to an allocation site
    rather than to a binding); it is just smaller than 506 by an amount that has to
    be measured, not subtracted.

  2. proggeramlug commented on Aug 14, 2026

    @proggeramlug
    ContributorAuthor

    Both halves are resolved on main @ 514ff7c9a — recommend close, with a fresh census

    §2 — the 130 __cjs_module bare references: fixed and landed

    4cede62f6 (#7171) is on main. Re-measured below, rule-2 containment denials
    are 23 of 838 ptr-shape denials (2.7%), down from this issue's 130 of
    746
    (17.4%). The bucket this section was about is gone.

    §1 — "rule 1 is the wall": replicates, and the number is now computed by the compiler

    ed5f1c749 (#7176, R0) is on main, so summary.by_rule and
    summary.by_alloc_context come out of --opt-report=json directly — the
    re-derivation this issue's numbers needed is now a file read, not a jq line
    in a comment. c9cd73ba5 (#7233, R1) is on main too.

    Caveat first: real-apps/scriptc/node_modules no longer exists in the tree,
    so this issue's exact 180-module slice cannot be reproduced. I used a different
    sample of the same kind of code — every third __esModule .js under the
    repo's own node_modules (376 matched, 126 selected, each copied to its own
    directory), perry compile m.js --no-link --opt-report=json, --profile perry-dev. 124 produced a report, 2 did not. Treat this as a replication of
    the shape, not a strict before/after.

    ptr-shape, 124 modules: 10 selected / 838 denied / 36 consumed.

    rule denied share of ptr-shape denials
    rule 1 (provenance) 726 86.6%
    rule 1 — already served by return-shape 70 8.4%
    rule 2 (containment) 23 2.7%
    rule 5 (module-wide barrier) 16 1.9%
    class admission 3 0.4%

    Rule 1 = 796 of 838 = 95.0%. This issue's central claim holds on an
    independent corpus with a compiler three instrument-fixes newer.

    By allocation position (784 alloc-site rows):

    position rows share
    call argument 265 33.8%
    statement (still the catch-all — #7176 §6) 232 29.6%
    object literal property value 118 15.1%
    return — served by return-shape 70 8.9%
    return — unserved 44 5.6%
    returned expression operand 25 3.2%
    initializer 17 2.2%
    throw 9 1.1%
    array element 3 0.4%
    condition 1 0.1%

    Note the return split: 70 served / 44 unserved. Pre-R1 that was 4
    served of 1842
    (0.2%). #7233 is visibly working.

    Recommendation

    Close. §2 is fixed on main; §1's finding is confirmed but its number now
    lives in the instrument (summary.by_rule) and its reading was corrected by
    #7170 §5, whose R0/R1 have both landed. The live tracker for the rule-1 wall is
    #7170, and everything actionable that remains is ranked there.

    Unmeasured: the original scriptc corpus (deleted); x86-64; anything about
    speed — this is a static census only.

  3. proggeramlug commented on Aug 14, 2026

    @proggeramlug
    ContributorAuthor

    Closing — both halves are resolved on main, per the census above.

    The live consequences were tracked on #7170, which is also being closed with its
    own re-census; the two remaining tiers live on #6759 (heap-position provenance)
    and #7142 (the runtime fallback tier).

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