Skip to content

repsel: a Ptr<Shape>-proven numeric field still loads NaN-boxed — extend the raw-f64 field load past its scalar-replaced gate #7770

Description

@proggeramlug

The measurement

On main @ v0.5.1448, a Ptr<Shape>-promoted local — the case that already works — still coerces every numeric field read.

class P { constructor(public x: number, public y: number) {} }
function run(n: number): number {
  const a: P[] = [];
  for (let i = 0; i < n; i++) a.push(new P(i, i + 1));
  let s = 0;
  for (let i = 0; i < a.length; i++) { const r = a[i]; s += r.x + r.y; }
  return s;
}

--opt-report confirms the promotion succeeds: local r -> Ptr<Shape> (in function run). And the by-name machinery is gone from the IR — no js_object_get_field_by_name_f64, no js_typed_feedback_class_field_get_guard, no js_throw_type_error_property_access.

But subtracting a build-only variant to isolate the read loop leaves:

js_array_get_f64   x1     <- separate issue, see "Related"
js_number_coerce   x2     <- THIS ISSUE, one per field read
js_gc_loop_safepoint x1   <- by design (#7721)

Two declared-number fields, on a shape that is proven, read through a path with no dynamism left — and each one still pays a coercion, because the slot it loads from holds a NaN-boxed double.

Why this is small

The representation already exists on both sides:

  • Runtime: typed shapes carry a raw_f64_mask beside the pointer mask (gc/layout.rs), and the GC already understands slots that hold raw f64 rather than NaN-boxed values.
  • Codegen: typed_shape.rs computes raw_f64_mask_words and declares it, and expr/property_get/helpers.rs::lower_raw_f64_class_field_get_for_number_context already lowers a numeric field read to a raw load.

That function is gated on .scalar_replaced — objects proven never to escape — via scalar_replaced_field_is_raw_f64 and scalar_replaced_field_raw_f64_store_state.

Ptr<Shape> is a weaker condition that the collector already computes (collectors/ptr_shape.rs: provenance + containment). A Ptr<Shape> local's dynamic class is exactly C, which is precisely what the raw-f64 decision needs. The work is extending an existing gate to an existing proof, not building new machinery.

Note emit_element_shape_field_load (expr/element_shape_guard.rs, "run per access inside the fast clone") is the other place a proven field load happens — check whether the coercion in the measurement above comes from there rather than from the generic path, and fix whichever is live.

Acceptance criteria

  1. The read loop above emits zero js_number_coerce for r.x / r.y.
  2. --opt-report still shows local r -> Ptr<Shape>; the promotion must not be traded away for the coercion.
  3. The store side agrees with the load side. A slot read as raw f64 must have been written as raw f64. A mismatch is a silent wrong value, not a crash — this is the direction the change can be quietly wrong in, and it needs a test that writes through every path that can reach the field (constructor, this.x = v, obj.x = v from outside, Object.assign, a setter, a delete then re-add).
  4. The GC still traces correctly. raw_f64_mask and the pointer mask must stay disjoint — a slot in both, or in neither, is either a missed root or a scanned number. Run PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800 over the reproducer and assert the [gc-zeal] verdict shows non-zero copying_minors and moved_objects — a clean run that collected nothing proves nothing.
  5. A field whose declared type is number but which receives a non-number at runtime (Perry does not enforce declared types — CLAUDE.md, Known Limitations) must still produce Node-identical output. Cover: (p as any).x = "s", = null, = {}, = 1n.
  6. Protected floors hold on the pinned quiet mini, interleaved, best-of-N, output byte-verified against Node before timing.

Traps

Related

#7766 (rule-1 provenance, the hop before this one), and the element-fetch issue filed alongside this one (the js_array_get_f64 in the same measurement). Closed context: #7510 (layout side tables), #7149 / #7034 §3 (the element rule), #7480 / #7669 / #7701 (the element-shape guarded clone).

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions