From db30118a0e61206cb291c5b795f6f1f93e826a3f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 19:18:01 -0500 Subject: [PATCH 1/2] Store tty.isatty and process.cwd as function values Fixes #2408 Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com> Fixes #2402 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + .../src/frontend/lowering/builtins/process.ts | 5 ++ .../lowering/expressions/property-access.ts | 25 ++++--- .../src/frontend/lowering/lower-stmts.ts | 10 +++ .../compiler/src/frontend/lowering/lowerer.ts | 69 ++++++++++++++++--- .../src/frontend/lowering/surfaces.ts | 35 +++++++--- .../compiler/src/ir/builtin-signatures.ts | 1 + .../tests/corpus/process-cwd-value.cjs.json | 6 ++ .../tests/corpus/process-cwd-value.mjs.json | 6 ++ .../corpus/tty-isatty-value-typed.ts.json | 6 ++ .../tests/corpus/tty-isatty-value.mjs.json | 6 ++ packages/runtime/src/scr_lib.c | 6 ++ packages/runtime/src/scr_lib.h | 1 + tests/corpus/process-cwd-value.cjs | 7 ++ tests/corpus/process-cwd-value.mjs | 22 ++++++ tests/corpus/tty-isatty-value-typed.ts | 10 +++ tests/corpus/tty-isatty-value.mjs | 21 ++++++ tests/diagnostics/stdlib.ts | 2 +- tests/harness/__snapshots__/stdlib.ts.txt | 8 +-- .../corpus-costs/process-cwd-value.cjs.json | 1 + .../corpus-costs/process-cwd-value.mjs.json | 1 + .../tty-isatty-value-typed.ts.json | 1 + .../corpus-costs/tty-isatty-value.mjs.json | 1 + 24 files changed, 221 insertions(+), 31 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value.mjs.json create mode 100644 tests/corpus/process-cwd-value.cjs create mode 100644 tests/corpus/process-cwd-value.mjs create mode 100644 tests/corpus/tty-isatty-value-typed.ts create mode 100644 tests/corpus/tty-isatty-value.mjs create mode 100644 tests/harness/corpus-costs/process-cwd-value.cjs.json create mode 100644 tests/harness/corpus-costs/process-cwd-value.mjs.json create mode 100644 tests/harness/corpus-costs/tty-isatty-value-typed.ts.json create mode 100644 tests/harness/corpus-costs/tty-isatty-value.mjs.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02479a922..0237bf2e3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -381,6 +381,7 @@ export const LIB_FN_SYMS: Record = { "process.stdio": "scr_process_stdio", "process.stderrWrite": "scr_process_stderr_write", "process.isTTY": "scr_process_is_tty", + "process.isTTYValue": "scr_process_is_tty_value", "date.now": "scr_date_now", "date.newNow": "scr_date_now", "date.newMs": "scr_date_new_ms", diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 56379a443..9668d6494 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -118,6 +118,7 @@ const INERT_LIB_CALLS = new Set([ "process.platform", "process.execPath", "process.isTTY", + "process.isTTYValue", "worker.data", "worker.isMainThread", "worker.parentPort", diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 2eac7a17d..8654770ba 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -740,6 +740,11 @@ export function lowerProcessProperty( "process.env as a value of this type (read one variable: process.env.NAME or process.env[name])", ); } + // process.cwd as a value: the process module's member (surfaces.ts). + if (member === "cwd") { + const value = lowerer.lowerBuiltinCallableValue({ module: "process", member }, loc); + if (value) return value; + } if (member === "exit" || member === "cwd" || member === "getuid" || member === "kill") { lowerer.unsupported("SC1090", expr, `process methods as values (call '${member}' directly)`); } diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index e566ed292..ea08cec65 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -24,7 +24,14 @@ import { localPrivateBrandClass, privateNameClass, } from "../private-brands.js"; -import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js"; +import { + type Lowerer, + PoisonError, + builtinValueNameOf, + jsFuncNameOf, + neverTaintedJsType, + own, +} from "../lowerer.js"; import { ARRAY_METHODS, MAP_METHODS, @@ -1202,13 +1209,15 @@ export function lowerPropertyAccessExpr( (id) => lowerer.unions.get(id), ) ) { - const fnName = jsFuncNameOf(expr.expression); - // A builtin function value whose length is not its adapter's - // parameter count (`Math.max.length` is 2). - const fnLength = - probed.kind === "closure" - ? lowerer.builtinCallableValueLengths.get(probed.fnName) - : undefined; + // A builtin function value keeps Node's name, and its length when that + // is not its adapter's parameter count (`Math.max.length` is 2). Any + // other function is named by its definition, through const aliases. + const identity = lowerer.builtinFunctionIdentity(probed); + const fnName = + identity.fnName ?? + builtinValueNameOf(expr.expression, lowerer) ?? + jsFuncNameOf(expr.expression, lowerer); + const fnLength = identity.fnLength; const boxed: IrExpr = { kind: "dynFrom", value: probed, diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ee991091f..b948a4890 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -4738,6 +4738,16 @@ function stdlibGlobalTokenDestructure( ) { // A data member reads its value once, exactly like the spelled // `process.` read the destructuring performs. + } else if ( + globalName === "process" && + propName === "cwd" && + (value = lowerer.lowerBuiltinCallableValue( + { module: "process", member: propName }, + locOf(el), + )) !== null + ) { + // process.cwd reads the directory when it is called, so the detached + // function is the same one every other spelling reads. } else { lowerer.unsupported( "SC1031", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 417f13c20..f916708e2 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2104,6 +2104,40 @@ function anyPiecedFuncType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType * their own name, anonymous function/arrow expressions their * NamedEvaluation home (a variable initializer or property assignment). * Null when nothing names the value (the box stays anonymous). */ +/** The name Node gives the builtin module function `node` reads, through + * const aliases (`const get = process.cwd`), when the module table names its + * value differently from its member (BuiltinModuleFn.valueName); else null. */ +export function builtinValueNameOf(node: ts.Node, lowerer: Lowerer): string | null { + let n: ts.Node = node; + const seen = new Set(); + for (;;) { + while (ts.isParenthesizedExpression(n)) n = n.expression; + if (!ts.isIdentifier(n)) break; + const imported = lowerer.builtinImportOf(n); + if (imported) + return builtinModuleFnOf(lowerer, imported.module, imported.member)?.valueName ?? null; + const symbol = lowerer.resolveValueSymbol(n); + const declaration = symbol?.valueDeclaration; + if ( + !symbol || + seen.has(symbol) || + !declaration || + !ts.isVariableDeclaration(declaration) || + !declaration.initializer || + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0 + ) + return null; + seen.add(symbol); + n = declaration.initializer; + } + if (!ts.isPropertyAccessExpression(n)) return null; + const module = lowerer.isStdlibGlobal(n.expression, "process") + ? "process" + : lowerer.builtinNamespaceModuleOf(n.expression); + if (module === null) return null; + return builtinModuleFnOf(lowerer, module, n.name.text)?.valueName ?? null; +} + export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { let n: ts.Node = node; while (ts.isParenthesizedExpression(n)) n = n.expression; @@ -12303,20 +12337,13 @@ export class Lowerer { liveReference(expr.type) || (expr.type.kind === "union" && (this.unions.get(expr.type.unionId)?.arms.some(liveReference) ?? false)); - // A builtin function's adapter boxes under the builtin's name, the - // name Node's function carries (`[Function: randomUUID]`). - const builtinName = - expr.kind === "closure" ? this.builtinCallableValueNames.get(expr.fnName) : undefined; - const builtinLength = - expr.kind === "closure" ? this.builtinCallableValueLengths.get(expr.fnName) : undefined; return { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc, ...(liveRef ? { liveRef: true as const } : {}), - ...(builtinName !== undefined ? { fnName: builtinName } : {}), - ...(builtinLength !== undefined ? { fnLength: builtinLength } : {}), + ...this.builtinFunctionIdentity(expr), }; } return expr; @@ -12464,7 +12491,13 @@ export class Lowerer { ) { return { kind: "dynCheck", - value: { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc }, + value: { + kind: "dynFrom", + value: expr, + type: DYN, + loc: expr.loc, + ...this.builtinFunctionIdentity(expr), + }, type: expected, loc: expr.loc, }; @@ -16899,7 +16932,10 @@ export class Lowerer { if (!fnName) { fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); - this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); + this.builtinCallableValueNames.set( + fnName, + fn.valueName ?? display.slice(display.lastIndexOf(".") + 1), + ); if (fn.valueLength !== undefined) this.builtinCallableValueLengths.set(fnName, fn.valueLength); const params = funcType.params.map((type, index) => ({ @@ -16991,6 +17027,19 @@ export class Lowerer { return { kind: "closure", fnName, captures: [], type: funcType, loc }; } + /** The name and length a builtin function's adapter boxes under: the ones + * Node's function carries (`[Function: randomUUID]`). Empty for any other + * value. */ + builtinFunctionIdentity(expr: IrExpr): { fnName?: string; fnLength?: number } { + if (expr.kind !== "closure") return {}; + const name = this.builtinCallableValueNames.get(expr.fnName); + const length = this.builtinCallableValueLengths.get(expr.fnName); + return { + ...(name !== undefined ? { fnName: name } : {}), + ...(length !== undefined ? { fnLength: length } : {}), + }; + } + isBuiltinCallableValue(expr: IrExpr): boolean { return ( expr.kind === "closure" && diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 03b65bd54..cef6f91bd 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -823,6 +823,9 @@ export interface BuiltinModuleFn { /** The function value's `length` when it is not the count of its * required value parameters (`Math.max`'s 2 over its rest pack). */ valueLength?: number; + /** The function value's `name` when it is not the member's + * (`process.cwd` is Node's `wrappedCwd`). */ + valueName?: string; } export type BuiltinValueParam = @@ -1954,10 +1957,29 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable { transcode: { fn: "buffer.transcode", params: [DYN, DYN, DYN], result: BYTES_U8 }, }, tty: { - isatty: { fn: "process.isTTY", params: [F64], result: BOOL }, + isatty: { + fn: "process.isTTY", + params: [F64], + result: BOOL, + // As a value (loggers store `tty.isatty`), the adapter takes any + // argument: a value that is not an integral fd answers false, and is + // never coerced. + valueFn: "process.isTTYValue", + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + valueLength: 1, + }, }, process: { loadEnvFile: { fn: "process.loadEnvFile", params: [DYN], result: VOID }, + // Every spelling of process.cwd as a value is this one function, which + // reads the directory when it is called. + cwd: { + fn: "process.cwd", + params: [], + result: STRING, + valueParams: [], + valueName: "wrappedCwd", + }, }, // node:string_decoder's surface is the StringDecoder CLASS — new/write/ // end are special-cased (lowerNew + lowerStringDecoderMethodCall); no @@ -2289,6 +2311,9 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"], randomUUID: ["crypto.randomUUIDOpts"], }, + tty: { + isatty: ["process.isTTYValue"], + }, os: { // lowerOsUserInfoCall assembles the record from scalar libCalls. userInfo: ["os.userHomedir", "os.userShell"], @@ -2720,12 +2745,6 @@ function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] { fns: ["process.release"], note: "the compatibility target's release metadata as one stable read-only object", }, - { - id: "node-builtin.process.cwd", - kind: "node-builtin", - name: "process.cwd", - fns: ["process.cwd"], - }, { id: "node-builtin.process.chdir", kind: "node-builtin", @@ -2860,7 +2879,7 @@ function buildAmbientSurfaceFns(): readonly AmbientSurfaceRow[] { id: "node-builtin.process.isTTY", kind: "node-builtin", name: "process.isTTY", - fns: ["process.isTTY"], + fns: ["process.isTTY", "process.isTTYValue"], note: "the isTTY read on process.stdin/stdout/stderr — one surface across the three streams", }, { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 024f11277..918b6785f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -5337,6 +5337,7 @@ function buildLibFnSignatures() { /** Validated isatty(3) probe. Stream property lowering maps false to * undefined; tty.isatty exposes the boolean result directly. */ "process.isTTY": { argTypes: [F64], result: BOOL }, + "process.isTTYValue": { argTypes: [DYN], result: BOOL }, // Like process.envGet: the result is the module's interned // `number | undefined` union — checked by arms in the libCall case. /** Terminal width over an fd literal (1/2 — process.stdout/stderr diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.cjs.json new file mode 100644 index 000000000..2d6348884 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/process-cwd-value.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.mjs.json new file mode 100644 index 000000000..165dc4ae4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/process-cwd-value.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/process-cwd-value.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value-typed.ts.json new file mode 100644 index 000000000..5dd1fbc01 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/tty-isatty-value-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value.mjs.json new file mode 100644 index 000000000..5aa2813a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/tty-isatty-value.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/tty-isatty-value.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 67f18c0d8..aa1301d78 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -5126,6 +5126,12 @@ bool scr_process_is_tty(double fd) { return isatty((int)fd) != 0; } +/* tty.isatty as a function value (borrowed argument): a value that is not a + * number answers false and is never coerced, as Node's integer test does. */ +bool scr_process_is_tty_value(const ScrDyn *fd) { + return fd != NULL && fd->kind == SCR_DYN_NUM && scr_process_is_tty(fd->v.num); +} + /* Terminal geometry for process.stdout/stderr.columns/rows: ioctl(TIOCGWINSZ) on * the stream's fd, exactly Node's tty.WriteStream source of truth. A * non-TTY stream, or a terminal that refuses the ioctl, answers -1 and diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index fa88873af..2dacf8cde 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -386,6 +386,7 @@ double scr_atomics_notify_dyn(const ScrDyn *, double, double); * non-TTY streams expose undefined (SEMANTICS.md). stdin_destroy is a * documented no-op. */ bool scr_process_is_tty(double fd); +bool scr_process_is_tty_value(const ScrDyn *fd); void scr_process_stdin_destroy(void); void scr_process_stdin_set_raw_mode(bool raw); diff --git a/tests/corpus/process-cwd-value.cjs b/tests/corpus/process-cwd-value.cjs new file mode 100644 index 000000000..6a6d616c6 --- /dev/null +++ b/tests/corpus/process-cwd-value.cjs @@ -0,0 +1,7 @@ +// process.cwd stored as a value from CommonJS: the same function as the +// process module's member, read when it is called. +const processModule = require("node:process"); +const get = process.cwd; +console.log(get === processModule.cwd, get.name, get() === process.cwd()); +const { cwd } = processModule; +console.log(cwd === get, typeof cwd()); diff --git a/tests/corpus/process-cwd-value.mjs b/tests/corpus/process-cwd-value.mjs new file mode 100644 index 000000000..016b914e6 --- /dev/null +++ b/tests/corpus/process-cwd-value.mjs @@ -0,0 +1,22 @@ +// process.cwd stored as a value: one identity across the global, named, +// namespace and default forms, Node's name and length, and the directory +// read when it is called. +import { cwd } from "node:process"; +import * as processModule from "node:process"; +import processDefault from "node:process"; +import { mkdtempSync, realpathSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const get = process.cwd; +console.log(typeof get, get.name, get.length); +console.log(get === cwd, get === processModule.cwd, get === processDefault.cwd); +console.log(get() === process.cwd(), cwd() === process.cwd()); + +const start = get(); +const dir = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-cwd-value-"))); +process.chdir(dir); +console.log(get() === dir, cwd() === dir, [0].map(() => get())[0] === dir); +process.chdir(start); +console.log(get() === start); +rmSync(dir, { recursive: true }); diff --git a/tests/corpus/tty-isatty-value-typed.ts b/tests/corpus/tty-isatty-value-typed.ts new file mode 100644 index 000000000..7ec7bfb31 --- /dev/null +++ b/tests/corpus/tty-isatty-value-typed.ts @@ -0,0 +1,10 @@ +// A typed alias of tty.isatty works as a callback with its declared +// signature. +import * as tty from "node:tty"; + +const isTerminal: (fd: number) => boolean = tty.isatty; +function describe(fds: number[], test: (fd: number) => boolean): string[] { + return fds.map((fd) => `${fd}:${test(fd)}`); +} +console.log(describe([-1, 0.5, 2 ** 40], isTerminal).join(" ")); +console.log(isTerminal === tty.isatty, [-1].map(isTerminal)); diff --git a/tests/corpus/tty-isatty-value.mjs b/tests/corpus/tty-isatty-value.mjs new file mode 100644 index 000000000..56aab317e --- /dev/null +++ b/tests/corpus/tty-isatty-value.mjs @@ -0,0 +1,21 @@ +// tty.isatty stored as a value keeps the direct predicate's answers, its +// identity across import forms, and its name and length. Values that are +// not integral file descriptors answer false without being coerced. +import * as tty from "node:tty"; +import { isatty } from "node:tty"; +import ttyDefault from "node:tty"; + +const probe = tty.isatty; +console.log(typeof probe, probe.name, probe.length); +console.log(probe === isatty, probe === ttyDefault.isatty); +console.log(probe(-1), probe(1.5), probe(2 ** 31), probe(Number.NaN)); +console.log([-1, 0.5].map(probe)); +// Called through an untyped callback, values that are not numbers answer +// false without being coerced. +function forward(predicate, value) { + return predicate(value); +} +let reads = 0; +const object = { valueOf() { reads++; return 1; } }; +console.log(forward(probe, undefined), forward(probe, "1"), forward(probe, 1n), forward(probe, null), forward(probe, object), reads); +console.log(forward(isatty, -1), forward(tty.isatty, 2 ** 31)); diff --git a/tests/diagnostics/stdlib.ts b/tests/diagnostics/stdlib.ts index d88b5da2b..e8565c38d 100644 --- a/tests/diagnostics/stdlib.ts +++ b/tests/diagnostics/stdlib.ts @@ -6,7 +6,7 @@ import { mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs"; import { writeFile } from "node:fs/promises"; const read = readFileSync; console.log(read); -const cwd = process.cwd; +const exit = process.exit; const p = process; const env = process.env; const flo = Math.floor; // fixed-arity Math functions have native value forms diff --git a/tests/harness/__snapshots__/stdlib.ts.txt b/tests/harness/__snapshots__/stdlib.ts.txt index dbe79859a..83e222a99 100644 --- a/tests/harness/__snapshots__/stdlib.ts.txt +++ b/tests/harness/__snapshots__/stdlib.ts.txt @@ -3,13 +3,13 @@ stdlib.ts:8:40 - error SC1090: a builtin function alias as an escaping value (ca 7 | import { writeFile } from "node:fs/promises"; 8 | const read = readFileSync; console.log(read); | ^~~~ - 9 | const cwd = process.cwd; + 9 | const exit = process.exit; -stdlib.ts:9:13 - error SC1090: process methods as values (call 'cwd' directly) are not supported yet +stdlib.ts:9:14 - error SC1090: process methods as values (call 'exit' directly) are not supported yet 8 | const read = readFileSync; console.log(read); - 9 | const cwd = process.cwd; - | ^~~~~~~~~~~ + 9 | const exit = process.exit; + | ^~~~~~~~~~~~ 10 | const p = process; stdlib.ts:13:15 - error SC1090: string methods as values (call 'toUpperCase' directly) are not supported yet diff --git a/tests/harness/corpus-costs/process-cwd-value.cjs.json b/tests/harness/corpus-costs/process-cwd-value.cjs.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/process-cwd-value.cjs.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/corpus-costs/process-cwd-value.mjs.json b/tests/harness/corpus-costs/process-cwd-value.mjs.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/process-cwd-value.mjs.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/corpus-costs/tty-isatty-value-typed.ts.json b/tests/harness/corpus-costs/tty-isatty-value-typed.ts.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/tty-isatty-value-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/corpus-costs/tty-isatty-value.mjs.json b/tests/harness/corpus-costs/tty-isatty-value.mjs.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/tty-isatty-value.mjs.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } From e506d19cd6128a98e6d4f114a907a2e1227d60ef Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 21:50:00 -0500 Subject: [PATCH 2/2] Name destructured builtins, parenthesized functions and util.debuglog as Node does Reading the name of a function stored in a const followed const aliases to the definition, but stopped at destructuring: `const { cwd } = process` gave the binding's name instead of Node's "wrappedCwd". A member destructured from the process global or a builtin module's namespace now gives the module table's name for it. An anonymous function wrapped in parentheses (`const f = (function () {})`) answered an empty name. As in Node, it takes the name of the binding or property it initializes through the parentheses. A builtin read as a property (`util.debuglog.name`) answered the name its shared adapter was first created under, so after `util.debug` it answered "debug". The name now comes from the property and the module table, and util.debug's value is named "debuglog", as in Node. --- .../lowering/expressions/property-access.ts | 16 +++--- .../compiler/src/frontend/lowering/lowerer.ts | 53 +++++++++++++++---- .../src/frontend/lowering/surfaces.ts | 2 + .../corpus/function-name-const-alias.mjs.json | 6 +++ tests/corpus/function-name-const-alias.mjs | 20 +++++++ .../function-name-const-alias.mjs.json | 1 + 6 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-name-const-alias.mjs.json create mode 100644 tests/corpus/function-name-const-alias.mjs create mode 100644 tests/harness/corpus-costs/function-name-const-alias.mjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index ea08cec65..bf954dc13 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1209,15 +1209,15 @@ export function lowerPropertyAccessExpr( (id) => lowerer.unions.get(id), ) ) { - // A builtin function value keeps Node's name, and its length when that - // is not its adapter's parameter count (`Math.max.length` is 2). Any - // other function is named by its definition, through const aliases. - const identity = lowerer.builtinFunctionIdentity(probed); + // A function is named by its definition, through const aliases: a + // builtin module member whose Node name differs from the member + // (`process.cwd` is `wrappedCwd`) by the module table, anything else + // by the property or function it reads. A builtin function value + // keeps its length when that is not its adapter's parameter count + // (`Math.max.length` is 2). const fnName = - identity.fnName ?? - builtinValueNameOf(expr.expression, lowerer) ?? - jsFuncNameOf(expr.expression, lowerer); - const fnLength = identity.fnLength; + builtinValueNameOf(expr.expression, lowerer) ?? jsFuncNameOf(expr.expression, lowerer); + const fnLength = lowerer.builtinFunctionIdentity(probed).fnLength; const boxed: IrExpr = { kind: "dynFrom", value: probed, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index f916708e2..9e544433e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2118,24 +2118,51 @@ export function builtinValueNameOf(node: ts.Node, lowerer: Lowerer): string | nu return builtinModuleFnOf(lowerer, imported.module, imported.member)?.valueName ?? null; const symbol = lowerer.resolveValueSymbol(n); const declaration = symbol?.valueDeclaration; + if (!symbol || seen.has(symbol) || !declaration) return null; + seen.add(symbol); + // A member destructured from a module or the process global + // (`const { cwd } = process`). + if (ts.isBindingElement(declaration)) { + const pattern = declaration.parent; + const holder = pattern?.parent; + const key = declaration.propertyName ?? declaration.name; + if ( + declaration.dotDotDotToken || + !pattern || + !ts.isObjectBindingPattern(pattern) || + !holder || + !ts.isVariableDeclaration(holder) || + !holder.initializer || + (ts.getCombinedNodeFlags(holder) & ts.NodeFlags.Const) === 0 || + !ts.isIdentifier(key) + ) + return null; + return builtinMemberValueName(lowerer, holder.initializer, key.text); + } if ( - !symbol || - seen.has(symbol) || - !declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer || (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0 ) return null; - seen.add(symbol); n = declaration.initializer; } if (!ts.isPropertyAccessExpression(n)) return null; - const module = lowerer.isStdlibGlobal(n.expression, "process") + return builtinMemberValueName(lowerer, n.expression, n.name.text); +} + +/** BuiltinModuleFn.valueName of `member` read from `receiver`, the process + * global or a builtin module's namespace; null for anything else. */ +function builtinMemberValueName( + lowerer: Lowerer, + receiver: ts.Expression, + member: string, +): string | null { + const module = lowerer.isStdlibGlobal(receiver, "process") ? "process" - : lowerer.builtinNamespaceModuleOf(n.expression); + : lowerer.builtinNamespaceModuleOf(receiver); if (module === null) return null; - return builtinModuleFnOf(lowerer, module, n.name.text)?.valueName ?? null; + return builtinModuleFnOf(lowerer, module, member)?.valueName ?? null; } export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { @@ -2165,14 +2192,18 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { if (ts.isPropertyAccessExpression(n)) return n.name.text; if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) && n.name) return n.name.text; if (ts.isFunctionExpression(n) || ts.isArrowFunction(n)) { - const p = n.parent; - if (p && ts.isVariableDeclaration(p) && p.initializer === n && ts.isIdentifier(p.name)) { + // An anonymous function takes the name of the binding or property it + // initializes, through parentheses (`const f = (function () {})`). + let outer: ts.Node = n; + while (outer.parent && ts.isParenthesizedExpression(outer.parent)) outer = outer.parent; + const p = outer.parent; + if (p && ts.isVariableDeclaration(p) && p.initializer === outer && ts.isIdentifier(p.name)) { return p.name.text; } - if (p && ts.isBindingElement(p) && p.initializer === n && ts.isIdentifier(p.name)) { + if (p && ts.isBindingElement(p) && p.initializer === outer && ts.isIdentifier(p.name)) { return p.name.text; } - if (p && ts.isPropertyAssignment(p) && p.initializer === n && ts.isIdentifier(p.name)) { + if (p && ts.isPropertyAssignment(p) && p.initializer === outer && ts.isIdentifier(p.name)) { return p.name.text; } } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index cef6f91bd..dff1d1008 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1897,6 +1897,7 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable { { mode: "optional", type: DYN, defaultValue: undefined }, ], }, + // util.debug is util.debuglog, under its name. debug: { fn: "util.debuglog", params: [DYN, DYN], @@ -1905,6 +1906,7 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable { { mode: "required", type: DYN }, { mode: "optional", type: DYN, defaultValue: undefined }, ], + valueName: "debuglog", }, }, // The util/types spoke supplies the probe name to the shared brand check. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-name-const-alias.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-name-const-alias.mjs.json new file mode 100644 index 000000000..04ed0d7a0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/function-name-const-alias.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/function-name-const-alias.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/function-name-const-alias.mjs b/tests/corpus/function-name-const-alias.mjs new file mode 100644 index 000000000..a534c50ec --- /dev/null +++ b/tests/corpus/function-name-const-alias.mjs @@ -0,0 +1,20 @@ +// The name of a function value read through a const binding: a builtin +// keeps Node's name (including when destructured), an anonymous function or +// arrow takes the binding's name through parentheses, and an alias of a +// named function keeps the function's name. +import * as path from "node:path"; +import * as processModule from "node:process"; + +const { cwd } = process; +const { cwd: renamed } = processModule; +const join = path.join; +const parenthesized = (function () {}); +const arrow = (() => 0); +const nested = ((function () {})); +function named() {} +const alias = named; +const aliasOfAlias = alias; +console.log(cwd.name, renamed.name, join.name); +console.log(parenthesized.name, arrow.name, nested.name); +console.log(alias.name, aliasOfAlias.name); +console.log(cwd === process.cwd, typeof cwd()); diff --git a/tests/harness/corpus-costs/function-name-const-alias.mjs.json b/tests/harness/corpus-costs/function-name-const-alias.mjs.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/function-name-const-alias.mjs.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 }