ReviewOS

zig-utils/zig-js

Compiler: scan deferred class frame captures once

#619
Closed chris opened this 10 days ago ·
10 days ago

Parents: #465, #493, #496 Related: #615, #616, #618

Problem

classDeferredBodiesCaptureFrame walks every deferred class member body once for every frame binding in the current and enclosing plain-function scopes. A function with many locals plus a large method/field/static-block surface therefore multiplies frame-binding count by deferred-class AST size during bytecode admission, even when the class references only globals.

At the original classifier baseline, eval_class could capture Environment-backed bindings but could not recover an arbitrary VM frame slot, so real captures required rejection. #803 has since implemented exact live frame projections. The classifier still must remain conservative and exact; broad class rejection is not an optimization.

Scope

  • Freeze production parse + plain-function admission/compile rows at 1K/2K/4K frame widths with method/field/static-block and early/late/global-only controls.
  • Collect frame-slot names across the exact FnScope ancestry once, excluding bindings already backed by Environment Records.
  • Traverse deferred class bodies once against exact frame-name membership while leaving eager superclass and computed-key evaluation unchanged.
  • Preserve conservative shadowing, TDZ policy, class template/chunk structure, unsupported-reason identity, and allocation accounting.
  • Keep the zero/one-frame-name common path allocation-free.

No-workaround rules

  • No class syntax refusal, heuristic cutoff, skipped deferred member kind, eager-key conflation, tree-walker fallback presented as optimization, source rewriting, or process-global cache.
  • A new class member/AST node kind must not silently create a frame-capture false negative.

Acceptance

  • Benchmark-first rows preserve exact admission/rejection reason, structural checksum, and allocation replay across every control.
  • Parent → candidate exact evidence demonstrates bounded classifier scaling and names the complete scored boundary.
  • The original classifier change preserved real-frame rejection and global/Environment-backed admission. Subsequent #803 implements real-frame admission through live projections; global-only and Environment-backed controls remain admitted.
  • Focused compiler/class tests, full units, relevant Test262 class/function/lexical subtrees, and applicable TSan gates pass with exact before → after accounting.
  • Evidence and parent issues are cross-linked without promoting diagnostic host noise.

Subsequent capture implementation boundary

f83c4191 — feat(vm): retain live deferred class captures (#803) implements the previously rejected capture boundary and updates production compiler guards/checksums to validate actual capture plans, including complete binding descriptors. Ten class-frame and two TDZ diagnostic rows pass, with structural checks and allocation observations; exact rows are retained under /Users/chris/.cache/zig-js-803.Ll0xKj/bench-class-validation.tsv and bench-tdz-final.tsv.

These are one-sample dirty-tree diagnostics, not publishable classifier scaling evidence. The original reports describe the earlier rejection boundary. Keep this issue open: a complete parent/candidate bounded-scaling comparison still needs to name its exact classifier versus capture-plan compilation boundary. The semantic/GC changes and their four required-bytecode gains are tracked in #803, with construction prerequisites #804/#805; they must not be described as a classifier throughput improvement.

10 days ago

Benchmark contract pushed in be27ecd4 (bench(frontend): add deferred class frame witness (#619)).

The production frontend runner now freezes ten parse + plain-function admission/compile rows: global-only deferred members at 1,024/2,048/4,096 frame widths, first/last method captures, instance-field capture, static-block capture, eager computed-key and superclass controls, and an Environment-backed repeated-body capture. Every row validates compiled-versus-unsupported admission, exact rejection reason, complete chunk structure, checksum, and an untimed identical-work allocation replay.

All 10/10 contracts pass. The existing #618 1K control remains exact and dependency audit stays at 15 classified edges / 0 migrations / 60 tools. A discarded occupied-host probe shows the global-only instruction sequence growing about 150M → 579M → 2.24B as width doubles, confirming the static multiplication, but this benchmark-only commit makes no timing or publication claim. The single-scan implementation starts from this exact first parent.

10 days ago

Single-scan implementation pushed in 8bbbf402 (perf(compiler): scan deferred class frames once (#619)).

The compiler now collects ordinary and non-Environment-backed lexical frame-slot names across the exact FnScope ancestry once, then scans each deferred method, field initializer, and static block once against exact membership. Eager computed keys remain outside the deferred scan, Environment-backed captures remain eligible, and zero/one-name cases allocate no table. The existing exhaustive AST walker remains conservative about shadowing and prevents new node kinds from silently becoming false negatives.

Validation at the implementation commit:

  • 10/10 frozen benchmark controls preserve exact admission/rejection identity, checksum, and allocation replay;
  • expanded class admission matrix covers global-only methods, eager computed keys, Environment-backed method captures, and frame captures from methods, fields, and static blocks;
  • compiler-filtered units: 70 passed, 0 failed, 0 leaked;
  • focused frontend: 12/12;
  • dependency audit: 15 classified edges, 0 migration targets, 60 classified tools.

A non-evidence occupied-host diagnostic changed the 1K/2K/4K global-only retired-instruction sequence from roughly 150M/579M/2.24B to 14M/29M/58M, consistent with removing the multiplicative scan. Exact isolated parent→candidate evidence, full units, and relevant Test262 accounting remain pending; no timing or conformance claim is made from that diagnostic.

10 days ago

Full integration unit gate at candidate main 8bbbf402: 1,733 passed, 1 platform skip, 0 failed, 0 leaked across 10 shards in 238.1 seconds. Exact parent/candidate Test262 class/function/lexical accounting remains pending and will run from the frozen be27ecd4 and 8bbbf402 binaries without overlapping other corpus or host jobs.

10 days ago

Incremental exact correctness evidence for #619:

Frozen identities:

  • parent be27ecd46dbb20b1bca090a256ad5927ae84b981 Test262 SHA-256 024cc0cc6e7410700fb08d2c5b83c735abd3ca7f9c12745b4ecb842ea67381a2
  • candidate 8bbbf402d723815b19ad3de5da4d698ac1a74cd5 Test262 SHA-256 241d768312530153e433178150fffe9f9c8e1cea91af5820ebb00513964889cb

Each retained pair had clear process snapshots before, between, and after, and byte-identical transcripts:

  • expressions/function: 0 fail / 264 pass → 0 / 264
  • statements/function: 0 / 451 → 0 / 451
  • block-scope: 0 / 145 → 0 / 145
  • statements/let: 0 / 145 → 0 / 145
  • statements/const: 0 / 136 → 0 / 136

Subtotal: 0 fail / 1,141 pass → 0 / 1,141, zero flips.

Compiler-focused TSan also passes 71/71 with zero failures/leaks, including the exact global-only/frame-capture class admission matrix. The full 1,733-pass / one-skip unit gate remains green.

This is deliberately partial: statements/class (4,367 files before configured exclusions) and expressions/class (4,059) remain pending an uninterrupted serial parent/candidate window. No full class-boundary or performance claim is made yet.

10 days ago

Prepared fresh exact historical Test262 runners for the two remaining class boundaries:

  • parent be27ecd, dev.1441 runner SHA-256 a9f1b6f5782288f8233c49b73b30186c754e24284d1c4f963a4de2b00775f063
  • candidate 8bbbf40, dev.1441 runner SHA-256 ff7dc68fd260efeb1bdc23698cce6c54f889da89a2656c240650886fcc5a5b05

Current dev.1818 cannot build these frozen revisions because their ArrayList API still uses getLast, so both sides use the preserved historical toolchain that built the original evidence.

Two statements/class parent attempts each produced the plausible 0 fail / 4,367 pass denominator, but neither is retained: the first wrapper lost its immediate postflight before encountering a Home Zig graph, and the corrected rerun detected another Home Zig process at postflight. The candidate and expressions/class runs were deliberately not started. No correctness box or class-boundary claim changes; the exact binaries remain staged locally for a genuinely quiet serial window.

10 days ago

Completed the two previously pending exact historical class boundaries in a quiet serial window.

Frozen inputs:

  • parent: be27ecd46dbb20b1bca090a256ad5927ae84b981; rebuilt runner SHA-256 e80bbb797e63d2ab709273a55b2cf292537c2e2858d416201dee09aee0c50d0c
  • candidate: 8bbbf402d723815b19ad3de5da4d698ac1a74cd5; rebuilt runner SHA-256 8fa89132f82918be82184bcd68d2968ba56507443eeb34896ed0133c00e14d9e
  • Zig: 0.17.0-dev.1441+d5181a9c9
  • Test262: 4249661388e5d3f92a85186213da140a6481490f

Each parent/candidate run had a clear high-CPU Zig/corpus preflight and immediate postflight. Results:

  • test/language/statements/class: 0 fail / 4,367 pass -> 0 / 4,367, byte-identical transcript
  • test/language/expressions/class: 0 / 4,059 -> 0 / 4,059, byte-identical transcript

Combined with the previously retained function/block/let/const rows, the complete measured #619 Test262 surface is 0 fail / 9,567 pass -> 0 / 9,567, zero flips.

Focused compiler/frontend, compiler TSan, and the full unit suite were already green in the earlier comments. The correctness acceptance item is now complete. The exact algorithmic-growth artifact remains separately pending under #670; no performance or timing claim is made from these corpus runs.

10 days ago

Follow-up evidence from #800 verification: a class setter capturing an enclosing ordinary-function frame local (for example, function outer(){var writes=0; class B{set x(v){writes++;}} ...}) still produces the causal template_plain_rejected_unsupported_lowering result under required bytecode. Moving the diagnostic store counter to an explicit realm property admits the otherwise-identical numeric-update witness; no engine fallback was added. This matches this issue’s explicit retained real-frame-capture boundary, and is not claimed resolved by #799/#800.

10 days ago

Completed and published the remaining exact-parent bounded-scaling evidence.

The seven-pair quiet-reference comparison scores only normalized retired instructions and exact allocation replay across the frozen 1,024/2,048/4,096 global-only deferred-class rows. Parent and candidate binaries use byte-identical one-commit measurement overlays, and the candidate remains the exact first child of the benchmark parent.

Results:

  • parent 1K→4K instruction growth: 14.9477x, exponent 1.951
  • candidate 1K→4K instruction growth: 4.1586x, exponent 1.028
  • candidate/parent instructions: 0.0927x, 0.0496x, 0.0258x
  • instruction RSD: below 0.06% for every variant/width
  • allocations/job P/C: 9,381/9,390, 18,617/18,627, 37,067/37,078
  • bytes/job P/C: 5,004,418/5,074,130, 11,871,578/12,010,946, 21,319,562/21,598,218
  • checksums: exact at all widths

The small deterministic candidate allocation increase is published explicitly. Wall, throughput, latency, cycle, energy, RSS, thermal, and full-efficiency observations remain diagnostic and are not promoted. The 1K/4K ordinary rows retain their blocked full-efficiency decisions rather than weakening that gate.

All three performance-attribution-v3 inputs, the algorithmic-growth-v3 aggregate, all 320 documentation links, the 50-page BunPress build, and git diff --check pass. Correctness remains the previously measured 9,567 -> 9,567 targeted Test262 passes with zero flips, plus the focused/full/TSan gates already recorded above. This satisfies the remaining evidence and cross-link acceptance items and closes #619; parent relationships #465, #493, and #496 remain recorded in the issue.

Sign in to comment on this issue.