Skip to content

a[i]++ holds its receiver and index across three user-code-capable calls before js_dyn_index_set (#7154 shape) #7628

Description

@proggeramlug

Found while migrating expr/instance_misc1.rs onto the Layer 1 rooting API
(#7615 slice 2, #7627). The operand-to-operand half is fixed there; this is the
half that needs machinery the campaign does not yet have.

The window

Expr::IndexUpdate lowers a[i]++ / --a[i] as a read-modify-write over two
operands that are consumed by four different calls, with collection points
between them:

obj_box, idx_box = <re-read once, below the operand group>
old      = js_dyn_index_get(obj_box, idx_box)   ; a getter here is user code
old_num  = js_to_numeric(old)                   ; a valueOf here is user code
new      = js_numeric_step(old_num, step)       ; ditto for a BigInt/object step
           js_dyn_index_set(obj_box, idx_box, new)   ; reads PRE-MOVE registers

obj_box and idx_box are NaN-boxed registers held across all three preceding
calls. If any of them drives an evacuating minor, js_dyn_index_set writes into
abandoned from-space memory: the element update silently does not appear on the
object the program keeps. That is #7154's shape, and the same one #7206 fixed on
the computed-read path.

Reproducible shape: a[i]++ where a's element is an object with a valueOf,
or where a is a Proxy / accessor-bearing object.

Why #7627 did not close it

rooting::with_operands_rooted re-reads its group at one point, at the end
of the operand list. That is the right shape when a single collection point
separates the group from its consumer, and it is what closed the
receiver-across-the-index window here. It is the wrong shape when the operands
are consumed by different instructions with collection points between them —
re-reading above js_dyn_index_get puts them straight back in the window the
roots exist to close.

The raw API already has the primitive: RootedOperands::reread_one, added for
#7154's dynamic-call lowering, whose doc describes exactly this situation. What
Layer 1 lacks is a combinator that exposes a rooted operand group as something
call_with_roots can consume per use — i.e. an Arg variant that carries the
group's per-operand Root / Reload / Reuse decision and materialises it at
the instant each call is emitted, rather than once.

Per the template's rule (#7617), a combinator arrives with its caller and with
a written argument for why the existing ones cannot serve, not ahead of one. This
issue is that argument; the caller is this arm.

Scope note

Expr::PropertyUpdate's generic tail has the same read-modify-write skeleton
(js_object_get_field_by_name_f64js_to_numericjs_numeric_step
js_object_set_field_by_name) over an obj_handle unboxed once above them. It
should be looked at in the same change; it lives in the same file and was left
alone for the same reason.

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