Skip to content

test(async): #1013 — pin Promise.all + array destructure regression#1064

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-af5805d92429912e7
May 19, 2026
Merged

test(async): #1013 — pin Promise.all + array destructure regression#1064
proggeramlug merged 1 commit into
mainfrom
worktree-agent-af5805d92429912e7

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #1013.

Summary

The reporter's bug — const [a, b] = await Promise.all([fn1(), fn2()]) returning a === undefined / b === undefined — is already fixed on current main as a side effect of two PRs that landed after the reported HEAD (13dc587a / v0.5.1004):

  1. fix(hir): #1001 — restore built-in static identity in member position (regression from #973) #1007 (fix(hir): #1001 — restore built-in static identity in member position) collapses Promise.all from PropertyGet { PropertyGet { GlobalGet(0), "Promise" }, "all" } to PropertyGet { GlobalGet(0), "all" }.
  2. fix: address #945 scalar method allocation regression (rebased from #980) #1030 (fix: address #945 scalar method allocation regression) added is_global_constructor_expr in the codegen Promise-static dispatch, which accepts both the shallow (GlobalGet(0)) and deeper (PropertyGet { GlobalGet(0), "Promise" }) receiver shapes.

This PR adds a regression test (test-files/test_issue_1013_promise_all_destructure.ts) that pins the byte-for-byte node match. No code change.

Root cause (pre-fix)

At 13dc587a, the HIR for Promise.all([fetchA(), fetchB()]) was PropertyGet { object: PropertyGet { GlobalGet(0), "Promise" }, property: "all" }. The codegen Promise-static dispatch only matched matches!(object.as_ref(), Expr::GlobalGet(_)), so this deeper shape fell through to the generic js_native_call_method("all", …) block — which called "all" against the global Promise constructor function. No such method on a function value, returned 0.0. The awaited value was then a number, and [a, b] = 0.0 produced a=0, b=undefined.

Bisect

Commit Behavior
13dc587a (#1010, reporter HEAD) a: 0 typeof: number / b: undefined
42ec277c (#949 test harden, parent of #1007) Still buggy
f78361f1 (#1007 HIR collapse) Fixeda: "hello-from-A" / b: {"plan":"pro"}
3856caad (current main) Fixed

Repro

async function fetchA(): Promise<string> { return "hello-from-A"; }
async function fetchB(): Promise<{ plan: string }> { return { plan: "pro" }; }
async function main() {
    const [a, b] = await Promise.all([fetchA(), fetchB()]);
    console.log("a:", JSON.stringify(a), "typeof:", typeof a);
    console.log("b:", JSON.stringify(b), "typeof:", typeof b);
    console.log("b.plan:", b?.plan);
}
main();

Expected (node --experimental-strip-types):

a: "hello-from-A" typeof: string
b: {"plan":"pro"} typeof: object
b.plan: pro

Test plan

No version bump or changelog change.

…HIR collapse

`const [a, b] = await Promise.all([fn1(), fn2()])` returned undefined
under Perry HEAD=13dc587a (v0.5.1004, pre-#1007). Bisected: the bug
disappears at f78361f (#1007 — restore built-in static identity in
member position), which collapses `Promise.all` from
`PropertyGet { PropertyGet { GlobalGet(0), "Promise" }, "all" }` down
to `PropertyGet { GlobalGet(0), "all" }`. The codegen Promise-static
dispatch at that commit only matched the bare `GlobalGet(_)` receiver,
so the deeper shape fell through to `js_native_call_method("all", …)`
against the Promise constructor — returning 0.0 (no such method on a
function value) and breaking the destructure.

Current main is doubly-protected: (1) the #1007 HIR collapse keeps
the shallow shape, and (2) PR #1030 introduced `is_global_constructor_expr`,
which accepts both shapes at the codegen dispatch. No code change is
required — the bug is fixed in current main as a side effect of
#1007 + #1030. This test pins the byte-for-byte node match so a
future regression of either piece (the HIR collapse or the helper)
fails immediately. Closes #1013.
@proggeramlug proggeramlug merged commit 1a51a2f into main May 19, 2026
9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-af5805d92429912e7 branch May 19, 2026 06:04
@andrewtdiz
Copy link
Copy Markdown
Contributor

This looks like the right root cause for #1013: the bad Promise.all receiver shape explains why sequential awaits worked while tuple destructuring from Promise.all failed.

One coverage suggestion before merge: the original issue’s repro shape used cross-module imported async helpers that awaited and then returned property reads, e.g. return account.access_token. The current test pins same-file async functions returning literals/object literals, which covers the dispatch bug but not the exact reported async/module/property-return shape.

A stronger regression would add a small fixture module exporting getAccessToken() / getUserPlan(), each awaiting an inner lookup and returning user.accessToken / user.plan, then destructure:

const [accessToken, userPlan] = await Promise.all([
  getAccessToken("u1"),
  getUserPlan("u1"),
]);

Not a blocker if the intent is only to pin the bisected root cause, but it would guard the full #1013 surface more directly.

proggeramlug added a commit that referenced this pull request May 19, 2026
…ape (#1068)

Per PR #1064 review (@andrewtdiz): the original gscmaster-api repro used
cross-module imported async helpers that awaited an inner lookup and
returned a property read off the resolved value (`return user.accessToken`).
The previous test only pinned the same-file literal-return shape, which
covers the dispatch bug but not the exact reported async/module/property
boundary.

Added `test-files/fixtures/issue_1013/user_lookup.ts` exporting
`getAccessToken` / `getUserPlan` against an inner `await lookupUser(id)`,
returning property reads. The test now destructures both shapes from
`Promise.all` and verifies byte-for-byte parity with Node.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants