From 586683ccae68ff88e1317df9de292868751531a2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 23 Jul 2026 15:47:44 -0500 Subject: [PATCH] Fix LLVM-tier link failure on net autosel timeouts; audit every emittable runtime symbol name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LIB_FN_SYMS mapped net.getAutoSelTimeout/setAutoSelTimeout to scr_net_get_auto_sel_timeout/scr_net_set_auto_sel_timeout, but the runtime defines scr_net_get_autosel_timeout/scr_net_set_autosel_timeout (the C backend already used the correct spelling), so any program touching net.getDefaultAutoSelectFamilyAttemptTimeout on the LLVM tier died at link with undefined symbols. - The ABI guard could not see this: the literal-declare source scan only checks fully-literal declare strings, and the emitted scan's corpus slice never exercises the net autosel surface — a table entry whose fixture doesn't exist was invisible from both directions. - llvm-runtime-abi.test.ts grows a third, table-driven direction: every scr_* name the backend sources can ever emit — the static string tables plus every literal @scr_* template reference (interpolated per-type families excluded) — must exist in scr_runtime.h as a prototype or extern data symbol, so a two-string name skew now fails at test time instead of at the user's link step. - parseHeader additionally collects extern data symbols (scr_emitter_vt, scr_error_vts) so vtable-global references audit cleanly. - New corpus fixture 2500-net-autosel-timeout.ts round-trips the default attempt timeout (set then get, integers >= 10 to stay clear of Node's clamp) and is byte-identical vs Node through both backends. --- packages/compiler/src/backend/llvm/emitter.ts | 4 +- .../compiler/test/llvm-runtime-abi.test.ts | 57 +++++++++++++++++-- tests/corpus/2500-net-autosel-timeout.ts | 19 +++++++ 3 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 tests/corpus/2500-net-autosel-timeout.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 132ca65d..d8fffcb1 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -551,8 +551,8 @@ const LIB_FN_SYMS: Record = { "net.sockUnshift": "scr_net_sock_unshift_bytes", "net.sockPipeRes": "scr_http_sock_pipe_res", "net.serverEmitConnection": "scr_net_server_emit_connection", - "net.getAutoSelTimeout": "scr_net_get_auto_sel_timeout", - "net.setAutoSelTimeout": "scr_net_set_auto_sel_timeout", + "net.getAutoSelTimeout": "scr_net_get_autosel_timeout", + "net.setAutoSelTimeout": "scr_net_set_autosel_timeout", "http.reqUrl": "scr_http_req_url", "http.reqMethod": "scr_http_req_method", "http.reqSocket": "scr_http_req_socket", diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts index 7362988e..2cd40d64 100644 --- a/packages/compiler/test/llvm-runtime-abi.test.ts +++ b/packages/compiler/test/llvm-runtime-abi.test.ts @@ -5,7 +5,7 @@ * type-checks its calls against the header), but a .ll `declare` is taken * on faith by the linker, so a disagreement is silent UB that can run * clean under one toolchain and misbehave under another. This test kills - * the class mechanically, from two directions: + * the class mechanically, from three directions: * * 1. Source scan: every fully-literal `declare ... @scr_*` template * string in the backend/llvm sources is checked against the header. @@ -13,6 +13,14 @@ * through the LLVM backend and every `declare @scr_*` in the emitted * .ll — including the generic LIB_FN_SYMS path, whose signatures are * derived from IR arg types per call site — is checked the same way. + * 3. Name-existence scan: every scr_* symbol name the backend sources + * can EVER put in a .ll — the static string tables (LIB_FN_SYMS and + * friends, whose signatures only exist per call site so neither scan + * above sees an unexercised entry) plus every literal @scr_* template + * reference — must exist in the header as a prototype or an extern + * data symbol. This is what catches a two-string name skew (table + * says scr_foo_bar, runtime defines scr_foobar) at test time instead + * of at the user's link step. * * The header parser fails LOUDLY on any C type it cannot map so it can * never silently fall behind the header. */ @@ -112,10 +120,12 @@ function splitParams(argsText: string): string[] { return out; } -/** Parse scr_runtime.h into name → prototype. Only `;`-terminated - * prototypes count: a `static inline` definition has no linkage symbol the - * emitter could declare, so a declare against one must report as missing. */ -async function parseHeader(): Promise<{ protos: Map }> { +/** Parse scr_runtime.h into name → prototype, plus the extern data + * symbols (vtable globals and the like) the emitter references by name. + * Only `;`-terminated prototypes count: a `static inline` definition has + * no linkage symbol the emitter could declare, so a declare against one + * must report as missing. */ +async function parseHeader(): Promise<{ protos: Map; dataSyms: Set }> { const raw = await readFile(headerPath, "utf8"); const src = raw .replace(/\/\*[\s\S]*?\*\//g, " ") @@ -160,7 +170,11 @@ async function parseHeader(): Promise<{ protos: Map }> { const params = (variadic ? parts.slice(0, -1) : parts).map((p) => cParamToLl(p, types)); protos.set(name, { ret: cTypeToLl(retText, types), params, variadic }); } - return { protos }; + const dataSyms = new Set(); + for (const m of src.matchAll(/\bextern\s+[^;(){}]*?\b(scr_[a-z0-9_]+)\s*(?:\[[^\]]*\])?\s*;/g)) { + dataSyms.add(m[1]!); + } + return { protos, dataSyms }; } interface LlDeclare { @@ -225,6 +239,37 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => { expect(failures).toEqual([]); }); + test("every scr_* name the backend sources can emit exists in the header", async () => { + const { protos, dataSyms } = await parseHeader(); + const names = new Map(); // name → first file seen in + for (const file of await readdir(llvmSrcDir)) { + if (!file.endsWith(".ts")) continue; + const src = await readFile(join(llvmSrcDir, file), "utf8"); + // The static string tables: every double-quoted scr_* literal is a + // symbol name some path can hand to the .ll (LIB_FN_SYMS et al.). + for (const m of src.matchAll(/"(scr_[a-z0-9_]+)"/g)) { + if (!names.has(m[1]!)) names.set(m[1]!, file); + } + // Literal @scr_* references inside template strings (declares AND + // call sites). A reference whose name continues with `${...}` is an + // interpolated per-type family (scr_arr_get_${suffix}) — the full + // name only exists per suffix, so those ride the emitted scan. + for (const m of src.matchAll(/@(scr_[a-z0-9_]+)/g)) { + if (src.startsWith("${", m.index + m[0].length)) continue; + if (!names.has(m[1]!)) names.set(m[1]!, file); + } + } + const failures: string[] = []; + for (const [name, file] of names) { + if (protos.has(name) || dataSyms.has(name)) continue; + failures.push(`${file}: ${name} — the backend can emit this symbol but scr_runtime.h declares no such prototype or extern data symbol`); + } + // Extractor guard: the tables alone carry hundreds of names — finding + // few means the scan rotted, not that the backend went quiet. + expect(names.size).toBeGreaterThan(400); + expect(failures).toEqual([]); + }); + test("every declare emitted for the fs/path corpus slice", async () => { const { protos } = await parseHeader(); const failures: string[] = []; diff --git a/tests/corpus/2500-net-autosel-timeout.ts b/tests/corpus/2500-net-autosel-timeout.ts new file mode 100644 index 00000000..c5a78c93 --- /dev/null +++ b/tests/corpus/2500-net-autosel-timeout.ts @@ -0,0 +1,19 @@ +// node:net's process-wide happy-eyeballs attempt budget: the +// getDefaultAutoSelectFamilyAttemptTimeout/setDefaultAutoSelectFamilyAttemptTimeout +// pair round-trips one runtime double. Values stay integers >= 10 (Node +// clamps smaller settings up to 10ms; the fixture stays inside the +// clamp-free region so both implementations answer the stored value). +import { + getDefaultAutoSelectFamilyAttemptTimeout, + setDefaultAutoSelectFamilyAttemptTimeout, +} from "node:net"; + +setDefaultAutoSelectFamilyAttemptTimeout(300); +const a = getDefaultAutoSelectFamilyAttemptTimeout(); +console.log(typeof a, a); + +setDefaultAutoSelectFamilyAttemptTimeout(1234); +console.log(getDefaultAutoSelectFamilyAttemptTimeout()); + +setDefaultAutoSelectFamilyAttemptTimeout(10); +console.log(getDefaultAutoSelectFamilyAttemptTimeout());