Skip to content

fix(runtime): for-of over a dynamically-typed string drives the String iterator (#4892)#4893

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4892-dynamic-string-forof
Jun 10, 2026
Merged

fix(runtime): for-of over a dynamically-typed string drives the String iterator (#4892)#4893
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4892-dynamic-string-forof

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #4892. for…of (and spread / Array.from / array-destructuring) over a string value whose static type is unknown — v: any, or a value flowing out of a segmenter result / destructure — threw TypeError: (string).next is not a function. Statically-typed string for-of was unaffected.

Root cause

The dynamic path routes through the generic iterator protocol via js_get_iterator (lazy for-of, #4786). That helper resolves [Symbol.iterator] by reading the method off an object. For a string primitive there is no object slot, so js_object_get_symbol_property found nothing and js_get_iterator fell through to return val_f64 — handing the string itself back as the "iterator". The loop then called .next() on a plain string and threw.

Strings already have a correct String iterator object (string/iter_object.rs::string_values_iter, code-point iteration) installed on String.prototype[Symbol.iterator]; the dynamic path just never reached it.

Fix

One short-circuit in js_get_iterator (crates/perry-runtime/src/symbol.rs): when the value is a string primitive (heap STRING_TAG or inline SSO short string), build the real String iterator via string_values_iter, mirroring the array short-circuit at the top of the function. SSO strings are materialized to a *const StringHeader via the existing js_get_string_pointer_unified.

Verification

Against node --experimental-strip-types:

  • f(v: any){ for (const c of v) n++ }f("ab") = 2 ✅
  • segmenter-destructured for (const {segment} of seg.segment("a👍b")) for (const ch of segment) ... = 3 ✅
  • static-typed control still works ✅
  • spread [...v], Array.from(v), const [a,b]=v over dynamic strings — all code-point correct ✅
  • non-iterable primitive (37) still throws TypeError ✅; arrays still iterate ✅

End-to-end (the live ink blocker): with #4891 also on main, string-width@7+ now matches Node for mixed strings — stringWidth("a👍b") = 4 (was the hangulClusterWidthfor (const character of visibleSegment) throw). stringWidth of "abc"/"👍"/"👨‍👩‍👧" = 3/2/2, all matching Node.

cargo test -p perry-runtime (RUST_TEST_THREADS=1): 1007 passed; the only failure is the pre-existing TZ-dependent date::tests::test_full_year_setters_revive_invalid_date_only, which fails on pristine main too.

Related

… js_get_iterator (#4892)

for-of over a dynamically-typed string value (`for (c of v)` where v: any,
or a segmenter-/destructure-derived string) routed through the generic
iterator protocol via js_get_iterator. The generic [Symbol.iterator] lookup
only resolves the method off an object, so for a string PRIMITIVE it found
nothing and js_get_iterator returned the string unchanged; the lazy loop then
called .next() on the string itself, throwing "(string).next is not a
function". Short-circuit string primitives (heap STRING_TAG + inline SSO) to
string_values_iter, mirroring the array short-circuit at the top.
@proggeramlug proggeramlug force-pushed the worktree-fix-4892-dynamic-string-forof branch from c9746b9 to 242beec Compare June 10, 2026 10:07
@proggeramlug proggeramlug merged commit bea4f5c into main Jun 10, 2026
12 of 13 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-4892-dynamic-string-forof branch June 10, 2026 10:07
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.

for-of over a dynamically-typed string throws (string).next is not a function — next string-width wall after #4889

1 participant