Skip to content

codegen: OOB index of a generator/async LOCAL string returns "" instead of undefined (boxed-local loses its type) #6067

Description

@proggeramlug

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions