Skip to content

hir: builder fold treats an implicit ToPrimitive as "cannot execute user code" — o.a = "" + weird throws ReferenceError #10357

Description

@proggeramlug

Summary

The builder fold (#6812, crates/perry-hir/src/lower/builder_fold.rs) rewrites

const o: any = {};
o.a = <value>;

into const o: any = { a: <value> }, which moves <value> from after the allocation to before o is initialized. value_is_fold_safe is what makes that sound, and its doc says it admits "only expressions that provably cannot execute user code".

It doesn't. It admits every Expr::Bin operator except in/instanceof, every Expr::Unary except delete, and Expr::Tpl with substitutions. Each of those runs an implicit ToPrimitive/ToNumber/ToString on its operands, and that conversion calls a user valueOf/toString/Symbol.toPrimitive. If that user code reads o, the fold turns a successful read into a TDZ ReferenceError.

Reproduction

function run(): string {
  const weird = { valueOf(): any { return o; } };
  const o: any = {};
  o.a = "" + weird;
  return typeof o.a + ":" + (o.a === "[object Object]" ? "str" : "other");
}
try { console.log(run()); } catch (e: any) { console.log("threw " + e.name); }
output
node --experimental-strip-types string:str
perry main (v0.5.1579, fcd108b) threw ReferenceError

Linux x86_64. Plain perry compile, no flags.

Affected forms

Every value (or, since #10355, every skipped gap statement) that converts an operand:

  • arithmetic/relational/bitwise Bin: a + b, a - b, a < b, a | b, a == b, …
  • numeric Unary: -x, +x, ~x
  • templates with substitutions: `${x}`

Conversion-free forms are fine: literals, identifiers, typeof, void, !, ===/!==, &&/||/??, ?:, and array/object literals built from those.

Fix and its cost

Remove the converting operators from value_is_fold_safe, keeping only the conversion-free forms above. That is a correctness fix, but it narrows #6812's coverage: o.b = r + i, the module doc's own motivating example, would stop folding. It should be measured before landing (benchmarks/object-write-6812, and any real module graph that relies on the fold). Anything smarter needs a type proof that the operands are primitive.

#10355 reuses this predicate for its gap test on purpose, so the two sides stay in sync, and did not change it. Found while validating that PR.

Activity

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