Skip to content

AOT: Promise resolve() called via object-property/method access doesn't wake await site from socket 'data' callback #87

Closed

Description

@proggeramlug

Environment

  • perry 0.5.97
  • macOS arm64 (darwin 25.4.0)

Problem

When a Promise's resolve function is stored as a property on an object and later invoked via property access (e.g. obj.resolve(val)) from inside a net.Socket 'data' event handler, the outer await never wakes. Calling resolve(val) directly (with the bare captured identifier) works correctly.

Related to #77 (which fixed the setTimeout path), but this variant involves property-access dispatch rather than a captured closure binding — and it still hangs under 0.5.97.

Minimal repro

// repro.ts
import * as net from 'node:net';

async function readOneChunk(host: string, port: number): Promise<number> {
    return new Promise<number>((resolve, reject) => {
        const sock = net.createConnection(host as never, port as never);
        const box = { resolve: resolve };  // store on an object
        sock.on('data', (chunk: Buffer) => {
            console.log('[data] len=' + chunk.length + ' before resolve');
            box.resolve(chunk.length);       // <-- call via property access
            console.log('[data] after resolve');
            sock.destroy();
        });
        sock.on('error', (e: Error) => { reject(e); });
    });
}

async function main() {
    // Any TCP server that sends ≥ 1 chunk works; MySQL's HandshakeV10
    // is a convenient test target.
    const len = await readOneChunk('84.32.98.163', 3306);
    console.log('main: resolved len=' + len);
}
main().catch(e => { console.error('err:', e.message); });
perry compile repro.ts -o /tmp/repro && /tmp/repro
# Observed:
#   [data] len=95 before resolve
#   [data] after resolve
#   (hangs indefinitely — "main: resolved len=..." never prints)
#
# Expected (Node/Bun):
#   [data] len=95 before resolve
#   [data] after resolve
#   main: resolved len=95

The same program with resolve(chunk.length) in place of box.resolve(chunk.length) completes correctly under 0.5.97.

Hit in real code

@perry/mysql's runTextQuery() stores resolve on a PendingQuery struct that hangs off the connection state map; finishResultset() (invoked from the socket 'data' callback) calls pending.resolve(result). Under AOT the driver authenticates correctly and receives the query response, but the await conn.query(...) hangs forever. Under Bun / Node the same code resolves instantly.

I've captured the trace from inside the driver showing resolve() returns normally from the callback, but the awaiting coroutine never resumes:

[onData] packet[0..3] seq=1..4 — all parsed correctly
[finishRS] statusFlags=0x2
[finishRS] before resolve
[finishRS] after resolve
<hang>

Expected

box.resolve(val) from a socket 'data' callback behaves identically to the bare resolve(val) — the awaiting coroutine resumes on the next microtask tick.

Related

Activity

  1. proggeramlug commented on Apr 18, 2026

    @proggeramlug
    ContributorAuthor

    Fixed in v0.5.98. Root cause: js_native_call_method in perry-runtime/src/object.rs had two field-scan paths (around lines 3116 and 3424) that both gated callable dispatch on field_val.is_pointer() (POINTER_TAG check). The Promise executor's resolve/reject closures are passed as transmute(ClosureHeader* → f64) — raw pointer bits outside the NaN range — so is_pointer() returned false, the early path fell through to return field_val_bits, and box.resolve(val) became a no-op that returned the raw closure pointer instead of invoking js_promise_resolve. Fix: always route through js_native_call_value which validates CLOSURE_MAGIC internally. Verified against a local-TCP-server repro — the awaiter now resumes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions