Summary
When an outer arrow callback parameter is tagged as a native-instance (e.g. res in (req, res) => … of an http.createServer handler gets ("http", "ServerResponse")), the tag bleeds into any inner arrow callback that re-binds the same name as its own parameter — so the inner res (which should be a fresh untyped binding, or differently tagged) dispatches as if it were the outer res. Method calls on the inner binding go to the wrong NATIVE_MODULE_TABLE row.
Repro
import { createServer, get as httpGet } from "node:http";
const server = createServer((req: any, res: any) => {
res.end(Buffer.from([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]));
});
server.listen(18993, () => {
httpGet("http://127.0.0.1:18993/", (res: any) => {
// ^^^ same name as outer (req, res)
const chunks: any[] = [];
res.on("data", (c: any) => chunks.push(c));
res.on("end", () => {
console.log(Array.from(Buffer.concat(chunks).slice(0,8)).join(","));
});
});
});
The inner res is the response from http.get, which should be ("http", "IncomingMessage"). But the outer (req, res) callback's res was registered as ("http", "ServerResponse") first, and that tag leaks into the inner scope — so the inner res.on(...) routes through ServerResponse dispatch instead of IncomingMessage.
Workaround in current 1124-followup test: rename the inner param to resp (different identifier — no collision, no leak). That's how test-files/test_issue_1124_client_buffer.ts ships on branch fix/net-createserver-and-http-buffer-body at v0.5.1013.
Root cause hypothesis
The HIR native-instance tag table is keyed by identifier name and lives in some scope that's broader than the lexical arrow body — likely a function-scope or module-scope HashMap<String, NativeInstanceTag> rather than a properly nested scope stack. Pre-scans like pre_scan_node_http_client_callback_params (added in v0.5.1013) register names without first popping/shadowing any outer binding of the same name.
Worth checking:
- Where native-instance tags are inserted (search
crates/perry-hir/src/lower/expr_call.rs and crates/perry-hir/src/lower_patterns.rs for the registration sites added in v0.5.773, v0.5.1011, v0.5.1013).
- Whether the lookup site walks a scope chain or just
.get() on a flat map.
- Whether arrow callbacks open a fresh scope at all in the HIR lowering for these param tags.
Fix sketch
Either:
-
Proper scoping: convert the native-instance tag table to a scope stack — push on arrow-callback enter, pop on exit. Inner params shadow outer names cleanly.
-
Shadow on registration: when a pre-scan registers an inner callback's param name, check for + temporarily save the outer tag, register the inner one, and restore on scope exit. Less invasive than (1).
-
Identifier disambiguation: rename inner-scope identifiers in HIR to be unique (e.g. res$1, res$2). Most invasive but solves a class of related shadowing bugs.
Option 2 is probably the smallest diff if the lowering visitor already has an obvious arrow-enter/exit hook.
Impact
Any compilePackages program that uses both http.createServer and http.get/http.request and reuses the conventional (req, res) parameter name in both will misroute method dispatch in the inner callback. Without the rename workaround, res.on('data'), res.on('end'), res.setEncoding(), etc. on an IncomingMessage go through ServerResponse dispatch rows that don't have those methods, producing silent no-ops or TypeErrors.
Same bug shape probably affects:
net.createServer((sock) => { net.connect(..., (sock) => { sock.write(…) }) }) — inner sock tagged as outer-server's connection-socket type.
fastify.get('/', (req, reply) => { fetch(...).then((reply) => {...}) }) — inner reply mistagged.
Anywhere a user happens to reuse a conventional parameter name across nested native-instance callbacks.
Environment
Refs
Summary
When an outer arrow callback parameter is tagged as a native-instance (e.g.
resin(req, res) => …of anhttp.createServerhandler gets("http", "ServerResponse")), the tag bleeds into any inner arrow callback that re-binds the same name as its own parameter — so the innerres(which should be a fresh untyped binding, or differently tagged) dispatches as if it were the outerres. Method calls on the inner binding go to the wrong NATIVE_MODULE_TABLE row.Repro
The inner
resis the response fromhttp.get, which should be("http", "IncomingMessage"). But the outer(req, res)callback'sreswas registered as("http", "ServerResponse")first, and that tag leaks into the inner scope — so the innerres.on(...)routes throughServerResponsedispatch instead ofIncomingMessage.Workaround in current 1124-followup test: rename the inner param to
resp(different identifier — no collision, no leak). That's howtest-files/test_issue_1124_client_buffer.tsships on branchfix/net-createserver-and-http-buffer-bodyat v0.5.1013.Root cause hypothesis
The HIR native-instance tag table is keyed by identifier name and lives in some scope that's broader than the lexical arrow body — likely a function-scope or module-scope
HashMap<String, NativeInstanceTag>rather than a properly nested scope stack. Pre-scans likepre_scan_node_http_client_callback_params(added in v0.5.1013) register names without first popping/shadowing any outer binding of the same name.Worth checking:
crates/perry-hir/src/lower/expr_call.rsandcrates/perry-hir/src/lower_patterns.rsfor the registration sites added in v0.5.773, v0.5.1011, v0.5.1013)..get()on a flat map.Fix sketch
Either:
Proper scoping: convert the native-instance tag table to a scope stack — push on arrow-callback enter, pop on exit. Inner params shadow outer names cleanly.
Shadow on registration: when a pre-scan registers an inner callback's param name, check for + temporarily save the outer tag, register the inner one, and restore on scope exit. Less invasive than (1).
Identifier disambiguation: rename inner-scope identifiers in HIR to be unique (e.g.
res$1,res$2). Most invasive but solves a class of related shadowing bugs.Option 2 is probably the smallest diff if the lowering visitor already has an obvious arrow-enter/exit hook.
Impact
Any compilePackages program that uses both
http.createServerandhttp.get/http.requestand reuses the conventional(req, res)parameter name in both will misroute method dispatch in the inner callback. Without the rename workaround,res.on('data'),res.on('end'),res.setEncoding(), etc. on anIncomingMessagego throughServerResponsedispatch rows that don't have those methods, producing silent no-ops orTypeErrors.Same bug shape probably affects:
net.createServer((sock) => { net.connect(..., (sock) => { sock.write(…) }) })— innersocktagged as outer-server's connection-socket type.fastify.get('/', (req, reply) => { fetch(...).then((reply) => {...}) })— innerreplymistagged.Anywhere a user happens to reuse a conventional parameter name across nested native-instance callbacks.
Environment
perry 0.5.1013(branchfix/net-createserver-and-http-buffer-body)Refs
res→respto sidestep this.detect_native_instance_creation_with_contextwork fornet.createConnection(same family of HIR scope-tracking).pre_scan_node_http_client_callback_paramsadded incrates/perry-hir/src/lower_patterns.rs(likely contributor to the leak; or at least exercises it).