You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
.length on a declared-array/Named receiver whose runtime value has no length returns 0, not undefined — and does not throw on nullish #7853
.length on a value whose declared type is string[] / a Named type, but whose runtime value carries no length, returns 0 where JavaScript returns undefined — and returns 0 where JavaScript throws.
Reachable on main today from plain annotated TypeScript. Reproducer against origin/main @ 0321c6554 (perry 0.5.1467):
A nullish field value is worse: node throws TypeError: Cannot read properties of null (reading 'length'); perry returns 0 and continues.
Note the element read is correct in every row — only .length is wrong.
Where it is
The inline .length arm (crates/perry-codegen/src/expr/property_get.rs, the property == "length" && (is_array_expr || is_string_expr || Named) arm) is properly
guarded: it checks the NaN-box tag, the handle band, GC_TYPE_ARRAY/GC_TYPE_STRING
and the forwarded flag before touching the header. The fallback is what is wrong. js_value_length_f64 (crates/perry-runtime/src/value/dynamic_object.rs) ends in:
// BigInts, Promises, Errors, Maps: no `.length`.// Return 0 to match Perry's existing fallback for missing fields// (JS would produce `undefined`, but the generic PropertyGet slow// path already degrades to 0 here).
_ => return0.0,
and its GC_TYPE_OBJECT arm turns a missing length property into 0.0 too. A
nullish receiver never reaches a throw at all.
The arm is entered on a static type, and Perry enforces no declared type at runtime
— so this is the #7846 family: a claim consumed by a path whose fallback is not the
JavaScript answer.
Why it matters beyond correctness
It is currently blocking a measured optimization. #7854 lets const names = e.names
recover string[] from the receiver's declared type (worth −11.2% on gc-handoff/apps/interp.ts), but has to keep those locals OFF the .length arm
(FnCtx::declared_only_array_locals) precisely because of this, leaving ~3% on the
table. Fixing this unblocks that and removes the special case.
Suggested shape
Make the .length arm's fallback produce the JavaScript answer rather than 0:
nullish receiver throws the ordinary TypeError; a value with no length yields undefined; a plain object's own/inherited length is read by name (that part
already works). Either a new js_value_length_property_f64 used only from this
codegen arm, or a corrected js_value_length_f64 — the latter is a wider behavioural
change (0 → undefined) and wants its own gap-suite sweep either way.
test-files/test_gap_declared_field_type_refine_guarded.ts (added in #7854) already
covers the guarded half of this; it deliberately does not exercise the annotated .length case, because that case fails on main.
Activity
perf(codegen): interp 0.780 -> 0.675 s, iso_miss 1.061 -> 0.967 s — l…