Repository navigation
AOT: Promise resolve() called via object-property/method access doesn't wake await site from socket 'data' callback #87
Description
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=95The 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
- await returns stub before setTimeout fires when resolved value is an object literal #77 — same class of bug, fixed for
setTimeout-based dispatch. - Likely same root cause as whatever makes
setImmediate(() => pending.resolve(result))also not wake the await in my driver (I tried that as a workaround — same hang).
Activity
proggeramlug commentedon Apr 18, 2026
Fixed in v0.5.98. Root cause:
js_native_call_methodinperry-runtime/src/object.rshad two field-scan paths (around lines 3116 and 3424) that both gated callable dispatch onfield_val.is_pointer()(POINTER_TAG check). The Promise executor's resolve/reject closures are passed astransmute(ClosureHeader* → f64)— raw pointer bits outside the NaN range — sois_pointer()returned false, the early path fell through toreturn field_val_bits, andbox.resolve(val)became a no-op that returned the raw closure pointer instead of invokingjs_promise_resolve. Fix: always route throughjs_native_call_valuewhich validates CLOSURE_MAGIC internally. Verified against a local-TCP-server repro — the awaiter now resumes.fix: backfill v0.5.97/v0.5.98 source that v0.5.99 dispatch builds on