Skip to content

async transform: await hoisted above earlier comma-sequence operands — awaited receiver reads its pre-assignment value #5925

Description

@proggeramlug

Repro

class Listener {
  async start() { return 42; }
}

class Flow {
  authCodeListener: any = null;
  port: any = null;

  async go() {
    // minified shape: assignment comma-chained with an await that reads the
    // just-assigned field
    this.authCodeListener = new Listener, this.port = await this.authCodeListener.start();
    return this.port;
  }
}

const f = new Flow();
f.go().then((v) => console.log("GOT", v)).catch((e) => console.log("ERR", String(e)));
  • node: GOT 42
  • perry-compiled: ERR TypeError: Cannot read properties of null (reading 'start')

Mechanism

The plain-async pre-pass (async_to_generator.rs) hoists every nested Expr::Await into a let __await_N = await <operand>; above the containing statement. For a comma sequence, that moves the awaited operand's evaluation above the sequence's earlier operands:

// source order:            // after hoisting:
this.l = new Listener,      let __await_N = await this.l.start();  // reads this.l = null!
this.p = await this.l.start()   this.l = new Listener, this.p = __await_N;

Conditional branches (#342) and logical RHS (#5434) already have dedicated lifts for exactly this class of unsound hoist; Expr::Sequence has none. Minifiers/bundlers emit the a = new X, b = await a.m() shape constantly (any const x = new X(); const y = await x.m(); pair gets comma-folded), so real-world async code that works under node fails under perry with a null/undefined member read, or silently computes with stale values when the receiver was previously assigned.

Fix direction

Mirror the #342/#5434 lifts: when a sequence contains an await, lift the non-final operands into statements before the containing statement (each recursively hoisted, preserving evaluation order), leaving the final operand as the expression value. The hoisted __await_N let then lands after the lifted operands. Same shape applies to hoist_awaits_avoiding_top_level (statement-position sequences) and hoist_awaits_in_expr_full (nested sequences). The generator yield hoist (generator/hoist_yields.rs) has the same latent gap for yield-in-sequence and can adopt the identical lift as a follow-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions