Symptom
Indexing a local string variable inside a generator (or async) function
returns "" (empty string) for an out-of-bounds position instead of
undefined:
function* g() {
const line = ""; // (or any string local; annotation doesn't matter)
yield 1;
const d = line[0]; // node: undefined perry: "" (typeof "string")
return d === undefined;
}
Minimal reproductions (all fail in perry, all undefined in node):
function drain(g){let r=g.next();while(!r.done)r=g.next();return r.value;}
function* a(){ const line = ""; yield 1; return line[0] === undefined; } // false
function* b(){ const line: any = ""; yield 1; return line[0] === undefined; } // false (even :any)
function* c(){ const line = ""; const d = line[0]; yield 1; return d === undefined; } // false (index before yield)
function* d(line: string){ yield 1; return line[0] === undefined; } // TRUE (a PARAM works)
function* e(){ const line = "ab"; yield 1; return line[0]; } // "a" (in-bounds works)
A plain (non-generator) function with the identical code returns undefined
correctly, and a generator parameter typed string also works — only
generator local variables are affected.
Cause (localized)
A generator/async body is CPS-transformed and its locals are boxed for
cross-state persistence (PreallocateBoxes). After that, a string local's
static type is no longer visible to crates/perry-codegen/src/expr/index_get.rs:
is_string_expr(line) is false (so it misses the js_string_index_get path,
which correctly returns undefined for OOB — see the #3987 comment there), and
static_type_of(line) is neither string nor Any/Unknown (so it misses the
js_dyn_index_get Any-path, which is also correct). It therefore falls to the
generic object-field fallback (arr[stringified-index]), which mishandles the
string receiver and yields "" for OOB. Parameters keep their declared type, so
they route through js_string_index_get and work.
Impact
The yaml package's tokenizer relies on line[n] === undefined (where
line = this.getLine(), a local) to detect end-of-line; with it returning "",
Lexer.parseDocument's switch (line[n]) misses case undefined and takes a
non-advancing default, so the *lex state machine spins forever at 100% CPU.
This is the next hang in the large-bundle YAML-parse module-init path after
#6060 (labeled-continue) and #6065 (charAt dispatch).
Suggested fix direction
Either preserve the local's value type across the generator/async CPS transform
(so is_string_expr/static_type_of see it), or — narrower — route an
IndexGet on a receiver that is provably NOT an array through the tag-aware
runtime dispatcher (js_dyn_index_get) rather than the generic object-field
fallback, so a boxed string is handled by receiver shape at runtime (the Any
path already does this correctly).
Symptom
Indexing a local string variable inside a generator (or async) function
returns
""(empty string) for an out-of-bounds position instead ofundefined:Minimal reproductions (all fail in perry, all
undefinedin node):A plain (non-generator) function with the identical code returns
undefinedcorrectly, and a generator parameter typed
stringalso works — onlygenerator local variables are affected.
Cause (localized)
A generator/async body is CPS-transformed and its locals are boxed for
cross-state persistence (
PreallocateBoxes). After that, a string local'sstatic type is no longer visible to
crates/perry-codegen/src/expr/index_get.rs:is_string_expr(line)is false (so it misses thejs_string_index_getpath,which correctly returns
undefinedfor OOB — see the #3987 comment there), andstatic_type_of(line)is neither string norAny/Unknown(so it misses thejs_dyn_index_getAny-path, which is also correct). It therefore falls to thegeneric object-field fallback (
arr[stringified-index]), which mishandles thestring receiver and yields
""for OOB. Parameters keep their declared type, sothey route through
js_string_index_getand work.Impact
The
yamlpackage's tokenizer relies online[n] === undefined(whereline = this.getLine(), a local) to detect end-of-line; with it returning"",Lexer.parseDocument'sswitch (line[n])missescase undefinedand takes anon-advancing
default, so the*lexstate machine spins forever at 100% CPU.This is the next hang in the large-bundle YAML-parse module-init path after
#6060 (labeled-continue) and #6065 (charAt dispatch).
Suggested fix direction
Either preserve the local's value type across the generator/async CPS transform
(so
is_string_expr/static_type_ofsee it), or — narrower — route anIndexGet on a receiver that is provably NOT an array through the tag-aware
runtime dispatcher (
js_dyn_index_get) rather than the generic object-fieldfallback, so a boxed string is handled by receiver shape at runtime (the Any
path already does this correctly).