Skip to content

fix(runtime): addition ToPrimitive ordering, boxed-object coercion, error propagation - #3630

Merged
proggeramlug merged 2 commits into
mainfrom
fix-addition-toprimitive-3562-3563-3564
May 31, 2026
Merged

fix(runtime): addition ToPrimitive ordering, boxed-object coercion, error propagation#3630
proggeramlug merged 2 commits into
mainfrom
fix-addition-toprimitive-3562-3563-3564

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Closes #3563

Summary

Implements the ECMAScript + operator's ToPrimitive(default-hint) coercion so
objects, Dates, and boxed primitive wrappers participate in addition the way
Node does, and so Symbol/BigInt/throwing-valueOf operands propagate the right
error. The fully-closed issue here is #3563 (boxed Boolean/Number/String
coercion — all 10 Test262 rows byte-match Node). #3562 and #3564 get substantial
partial progress (see "Scope / dropped sub-cases").

Implementation

  • New runtime helper to_primitive_for_add (value/to_string.rs):
    ToPrimitive(value, "default") — primitives pass through, boxed
    Number/String/Boolean unwrap to their payload, Dates use the string
    hint, plain objects/arrays/class-instances consult [Symbol.toPrimitive]
    then fall back to valueOftoString (the default-hint order, reverse of
    the existing string-hint ordinary_to_primitive_string). Reuses the existing
    call_method_for_primitive invoker and the shared recursion-depth guard.
  • boxed_primitive_value exposed (pub(crate)) from
    builtins::formatting::boxed_primitives so the wrapper payload is reachable
    from the addition path.
  • js_dynamic_string_or_number_add (value/dynamic_arith.rs) now applies
    to_primitive_for_add to both operands first, then decides string-concat
    vs numeric add per spec. A Symbol primitive throws TypeError; a throwing
    valueOf/toString propagates; mixed BigInt/Number still throws via the
    existing both_bigint_or_throw.
  • Codegen dispatch (expr/binary.rs): a + b where neither side is a
    definite string now routes to the runtime helper unless both operands
    are provably numeric (BigInt operands keep the dedicated js_dynamic_add
    fast path). This fixes the previous bug where {} + 1 / obj + 1 took the
    inline js_number_coerce(obj) + n path and produced NaN. Hot provably-
    numeric loops (sum + arr[i], fib(n-1)+fib(n-2)) stay on the inline fadd
    path — unchanged.
  • String-concat slow paths (string/concat.rs): js_string_concat_value /
    js_value_concat_string now apply to_primitive_for_add (default hint) to
    the value operand before stringifying, and throw on a Symbol. This makes
    ({valueOf(){return 5}}) + "x" produce "5x" (valueOf-first) instead of
    "[object Object]x" (toString-first). The numeric fast path that handles
    "item_" + i is untouched — primitives short-circuit to_primitive_for_add
    with a single tag check.

No new HIR variant. No new codegen-emitted #[no_mangle] symbol (the helpers
are plain pub(crate) Rust fns; js_dynamic_string_or_number_add already
existed), so no keepalive anchors needed.

Validation

Gap test test-files/test_gap_addition_toprimitive_3562plus.ts is byte-identical
to node --experimental-strip-types under the default auto-optimize compile:

[object Object]1   ({}+1)
1                  ([]+1)
6                  ({valueOf(){return 5}})+1
5x                 ({valueOf(){return 5}})+"x"   <- default-hint valueOf-first
T!                 ({toString(){return "T"}})+"!"
true               (new Date(0)+"").startsWith("Thu Jan 01 1970")
5                  new Number(2)+3
ab                 new String("a")+"b"
2                  new Boolean(true)+1
1                  new Number(1)+null
1undefined         new String("1")+undefined
true1              new Boolean(true)+"1"
symbol:TypeError   Symbol()+1 throws
bigint:TypeError   1n+1 throws
thrower:boom       {valueOf(){throw}}+1 propagates

A dedicated 10-row #3563 boxed-primitive matrix (Boolean/Number/String × true/
1/null/undefined/string) is byte-identical to Node.

Regression evidence (changed the hot + dispatch — verified byte-identical to Node)

  • test_bool_arithmetic — IDENTICAL
  • test_edge_numeric — IDENTICAL
  • test_edge_strings — IDENTICAL
  • test_edge_operators — IDENTICAL
  • test_edge_type_coercion — IDENTICAL
  • test_edge_truthiness — IDENTICAL
  • test_gap_2786_2880_2782_2789_string_semantics — IDENTICAL
  • test_gap_2754_2907_2908_bigint_semantics — IDENTICAL
  • test_edge_complex_patterns — IDENTICAL

Guards: ./scripts/check_file_size.sh (no file > 2000 lines),
cargo fmt --all -- --check (clean), cargo test -p perry-runtime value
(78 passed), cargo test -p perry-hir (all passed).

Scope / dropped sub-cases

@proggeramlug
proggeramlug merged commit 3c900ec into main May 31, 2026
@proggeramlug
proggeramlug deleted the fix-addition-toprimitive-3562-3563-3564 branch May 31, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime: coerce boxed Boolean, Number, and String objects in addition

1 participant