fix(runtime): addition ToPrimitive ordering, boxed-object coercion, error propagation - #3630
Merged
Merged
Conversation
added 2 commits
May 31, 2026 11:52
…ive-3562-3563-3564
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3563
Summary
Implements the ECMAScript
+operator's ToPrimitive(default-hint) coercion soobjects, 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/Stringcoercion — all 10 Test262 rows byte-match Node). #3562 and #3564 get substantial
partial progress (see "Scope / dropped sub-cases").
Implementation
to_primitive_for_add(value/to_string.rs):ToPrimitive(value, "default")— primitives pass through, boxedNumber/String/Booleanunwrap to their payload, Dates use the stringhint, plain objects/arrays/class-instances consult
[Symbol.toPrimitive]then fall back to
valueOf→toString(the default-hint order, reverse ofthe existing string-hint
ordinary_to_primitive_string). Reuses the existingcall_method_for_primitiveinvoker and the shared recursion-depth guard.boxed_primitive_valueexposed (pub(crate)) frombuiltins::formatting::boxed_primitivesso the wrapper payload is reachablefrom the addition path.
js_dynamic_string_or_number_add(value/dynamic_arith.rs) now appliesto_primitive_for_addto both operands first, then decides string-concatvs numeric add per spec. A
Symbolprimitive throwsTypeError; a throwingvalueOf/toStringpropagates; mixed BigInt/Number still throws via theexisting
both_bigint_or_throw.expr/binary.rs):a + bwhere neither side is adefinite string now routes to the runtime helper unless both operands
are provably numeric (BigInt operands keep the dedicated
js_dynamic_addfast path). This fixes the previous bug where
{} + 1/obj + 1took theinline
js_number_coerce(obj) + npath and producedNaN. Hot provably-numeric loops (
sum + arr[i],fib(n-1)+fib(n-2)) stay on the inlinefaddpath — unchanged.
string/concat.rs):js_string_concat_value/js_value_concat_stringnow applyto_primitive_for_add(default hint) tothe 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_" + iis untouched — primitives short-circuitto_primitive_for_addwith a single tag check.
No new HIR variant. No new codegen-emitted
#[no_mangle]symbol (the helpersare plain
pub(crate)Rust fns;js_dynamic_string_or_number_addalreadyexisted), so no keepalive anchors needed.
Validation
Gap test
test-files/test_gap_addition_toprimitive_3562plus.tsis byte-identicalto
node --experimental-strip-typesunder the default auto-optimize compile: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— IDENTICALtest_edge_numeric— IDENTICALtest_edge_strings— IDENTICALtest_edge_operators— IDENTICALtest_edge_type_coercion— IDENTICALtest_edge_truthiness— IDENTICALtest_gap_2786_2880_2782_2789_string_semantics— IDENTICALtest_gap_2754_2907_2908_bigint_semantics— IDENTICALtest_edge_complex_patterns— IDENTICALGuards:
./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
valueOf(fix(codegen): #168 — specialize i32 loop counter for number-typed bounds #171) and Date (perf(runtime): #92 BigInt arena alloc + BigInt(str) === value comparison #172) rows now matchNode, but the function-source
toStringrows (perf(runtime): fast-path small Buffer.alloc via per-thread bump slab #173f1 + 1,--target web: codegen diverges based on
file:dep path — strips FFI imports OR emits invalid WASM (#687 stack fallthru) #183{} + function(){}) require retaining function source text — Perry onlystores the function name, not source, so
String(f1)returns[object Object](pre-existing, broad infra). The assignment/order-of-evalReferenceError rows (fix(stdlib): #174 Fastify end-to-end integration test + two dispatch fixes #175/Various Errors on fresh install #176) are undeclared-variable / GetValue-ordering
codegen, not addition coercion.
Symbol-to-string (Wire TS-callable codegen dispatch for perry/plugin (21 runtime FFI functions currently unreachable from user code) #189), and lhs-throws ordering (Wire Canvas widget into the LLVM codegen (currently web/wasm only) #190) all work now, but the
undeclared-variable ReferenceError rows (SIGTRAP crash when a program mixes Buffer-typed and Uint8Array-typed function parameters #169/perf(runtime): eliminate heap allocation in parseFloat; fix Infinity (#92) #170) need TDZ/undeclared-
reference semantics (broad infra, separate subsystem).
e.namerather thane.constructor.name:.constructoron a runtime-built
TypeErroris a separate pre-existing gap, orthogonal toaddition.