From be9c18f0b3a16602f85bbaf8fbcaa9751c490f1d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 13:50:06 -0700 Subject: [PATCH] Bind builtin static functions and read Buffer.alloc, fs.realpathSync and String methods as values - bind/call/apply on builtin function values held as checked-dynamic functions (String.fromCharCode, JSON.stringify and the forms below). - Function values for Buffer.byteLength (Node's runtime encoding ladder), Buffer.compare, Buffer.alloc (size check, then fill), JSON.parse, fs.realpathSync and fs.realpathSync.native (Node's options handling). - A JavaScript string method read as a value answers String.prototype's function (smol-toml's `!!(''.isWellFormed)` probe). The runtime entries live in a new unit, scr_builtin_values.c, linked only by programs that read these values. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 2 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../src/executable/executable-cache.ts | 1 + .../src/frontend/lowering/builtins/json.ts | 52 +++ .../lowering/expressions/property-access.ts | 189 ++++++++++- .../src/frontend/lowering/lower-calls.ts | 9 +- .../compiler/src/frontend/lowering/lowerer.ts | 50 +++ packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/runtime-features.ts | 3 + .../compiler/src/native/sanitizer.test.ts | 1 + .../main.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_builtin_values.c | 141 ++++++++ packages/runtime/src/scr_bytes.h | 3 + .../6570-npm-builtin-function-bind/main.ts | 25 ++ .../node_modules/bound-builtins/index.d.ts | 2 + .../node_modules/bound-builtins/index.js | 42 +++ .../node_modules/bound-builtins/package.json | 1 + .../node_modules/fs.realpath/LICENSE | 43 +++ .../node_modules/fs.realpath/index.d.ts | 6 + .../node_modules/fs.realpath/index.js | 66 ++++ .../node_modules/fs.realpath/old.js | 303 ++++++++++++++++++ .../node_modules/fs.realpath/package.json | 1 + .../truncate-utf8-bytes/index.d.ts | 2 + .../node_modules/truncate-utf8-bytes/index.js | 5 + .../truncate-utf8-bytes/lib/truncate.js | 42 +++ .../truncate-utf8-bytes/package.json | 8 + .../node_modules/value-forms/index.d.ts | 1 + .../node_modules/value-forms/index.js | 31 ++ .../node_modules/value-forms/package.json | 1 + tests/harness/__snapshots__/json-dyn.ts.txt | 9 +- 37 files changed, 1058 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json create mode 100644 packages/runtime/src/scr_builtin_values.c create mode 100644 tests/corpus/6570-npm-builtin-function-bind/main.ts create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js create mode 100644 tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 0fcf86ada..e8a5dcdf3 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.builtinValues ? { builtinValues: true } : {}), events: features.processEvents, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8cd40240f..da5b1991e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -374,6 +374,7 @@ export const LIB_FN_SYMS: Record = { "fsp.globStrings": "scr_fsp_glob_strings", "fsp.globError": "scr_fsp_glob_error", "fs.realpathSync": "scr_fs_realpath", + "fs.realpathValue": "scr_fs_realpath_value", "fs.realpathNativeSync": "scr_fs_realpath_promise", "fs.unlinkSync": "scr_fs_unlink", "fs.chmodSync": "scr_fs_chmod", @@ -437,6 +438,7 @@ export const LIB_FN_SYMS: Record = { "buffer.concatDyn": "scr_buffer_concat_dyn", "buffer.concatLenDyn": "scr_buffer_concat_len_dyn", "buffer.alloc": "scr_buffer_alloc", + "buffer.allocValue": "scr_buffer_alloc_value", "buffer.allocUnsafe": "scr_buffer_alloc_unsafe", "buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow", "buffer.poolFromStr": "scr_buffer_from_str", @@ -447,6 +449,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", + "buffer.byteLenValue": "scr_buffer_byte_length_value", "buffer.byteLenStr": "scr_bytes_byte_length_str", "buffer.isEncoding": "scr_bytes_is_encoding", // The checked-dynamic compare/equals validators (scr_bytes_io.c): diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 84f448e93..fedc26165 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -49,6 +49,9 @@ export interface NativeLinkFeatures { /** events.once with options: scr_events_once_signal.c. Absent (not * false) for programs without it. */ eventsOnceSignal?: boolean; + /** Builtin functions read as values (Buffer.byteLength, Buffer.alloc, + * fs.realpathSync) link scr_builtin_values.c; absent (false) otherwise. */ + builtinValues?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index a9c93d3df..0097600e5 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -119,6 +119,8 @@ export interface CcOptions { /** events.once with options (events.onceSignal): compiles * scr_events_once_signal.c. */ eventsOnceSignal?: boolean; + /** Builtin functions read as values: compiles scr_builtin_values.c. */ + builtinValues?: boolean; /** The program constructs EventTarget/Event/CustomEvent values or * calls events.addAbortListener (the event.* libCalls), or uses the * native fetch AbortSignal built on the same engine: compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 949f03839..218ac0042 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -179,6 +179,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"), unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"), unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"), + // Builtin functions read as values in JavaScript dependencies. + unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"), unit((u) => u.opts.dc, "scr_dc.c"), unit((u) => u.opts.dynTier, "scr_js_value.c"), unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 5c9d417c5..a6fe528cf 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -86,6 +86,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { dynTier: boolean; streamDyn: boolean; eventsOnceSignal: boolean; + builtinValues: boolean; warningReplace: boolean; stdioExtended: boolean; nativeFetch: boolean; @@ -279,6 +280,7 @@ export function effectiveRuntimeFeatures( dynTier: features.dynTier === true, streamDyn: features.streamDyn === true, eventsOnceSignal: features.eventsOnceSignal === true, + builtinValues: features.builtinValues === true, warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, nativeFetch, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 064b1f867..c5c9bde2b 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -68,6 +68,7 @@ export interface EarlyExecutableNativeFeatures { dynTier?: boolean; streamDyn?: boolean; eventsOnceSignal?: boolean; + builtinValues?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 54bcae4b5..7eb8aa9b2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -650,5 +650,57 @@ export function lowerJsonProperty( loc, }; } + if (member === "parse") { + // JSON.parse as a value: ToString of the text, then the reviver form + // (a reviver that is not a function is ignored, as in Node). + const loc = locOf(expr); + const name = "%builtin.JSON.parse"; + if (!lowerer.builtinCallableValueFns.has(name)) { + lowerer.builtinCallableValueFns.set(name, name); + const params = ["text", "reviver"].map((name) => ({ localId: name, name, type: DYN })); + lowerer.liftedFns.push({ + name, + params, + returnType: DYN, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })), + loc, + body: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "json.parseReviver", + args: [ + { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [varRef("text", DYN, loc)], + type: STRING, + loc, + }, + varRef("reviver", DYN, loc), + ], + type: DYN, + loc, + }, + loc, + }, + ], + }); + } + return { + kind: "dynFrom", + value: { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [DYN, DYN], ret: DYN }, + loc, + }, + fnName: "parse", + type: DYN, + loc, + }; + } lowerer.unsupported("SC1090", expr, `JSON 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 36ea0e8c2..c37ab5bcc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -27,6 +27,7 @@ import { import { BOOL, BYTES_ELEMENT_NAME, + BYTES_U8, DYN, F64, type IrExpr, @@ -156,7 +157,7 @@ import { narrowStoredClassValue, structuralReadType } from "../class-unions.js"; import { InternalCompilerError } from "../../../errors.js"; import { lowerUnionFieldRead } from "./union-fields.js"; import { errorPropertyRead } from "../error-methods.js"; -import { isSafeToRepeat } from "./evaluation-safety.js"; +import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js"; import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js"; import { keyedFieldRead } from "./keyed-field-targets.js"; @@ -1624,6 +1625,38 @@ export function lowerIntrinsicProperty( if (netAddressStatic) return netAddressStatic; if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") return lowerCheckedPredicateValue(lowerer, "Buffer.isBuffer", "buffer", locOf(expr)); + // Buffer statics as function values (`Buffer.byteLength.bind(Buffer)` in + // truncate-utf8-bytes): the checked forms take any argument as Node does. + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "byteLength") + return lowerer.lowerNativeCallableValue( + { + fn: "buffer.byteLenValue", + params: [DYN, DYN], + result: F64, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + "Buffer.byteLength", + locOf(expr), + ); + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "alloc") + return bufferAllocValue(lowerer, locOf(expr)); + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "compare") + return lowerer.lowerNativeCallableValue( + { + fn: "buffer.compareChk", + params: [DYN, DYN], + result: F64, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + ], + }, + "Buffer.compare", + locOf(expr), + ); if (lowerer.stdlibGlobalMember(expr, "Date") === "now") return lowerer.lowerNativeCallableValue( { @@ -2205,11 +2238,165 @@ export function lowerIntrinsicProperty( own(STDLIB_SURFACE.string, name) !== undefined : ARRAY_METHODS.has(name) ) { + // A JavaScript string method read as a value (smol-toml's + // `!!(''.isWellFormed)` feature probe): String.prototype's method, the + // runtime's interned function that runs it on its receiver. + if (kind === "string" && isJsSourceFile(expr.getSourceFile()) && !expr.questionDotToken) { + const receiver = lowerer.lowerExpr(expr.expression); + const method: IrExpr = { + kind: "libCall", + fn: "dyn.builtinMethod", + args: [ + { kind: "strLit", value: "String", type: STRING, loc: locOf(expr) }, + { kind: "strLit", value: name, type: STRING, loc: locOf(expr.name) }, + ], + type: DYN, + loc: locOf(expr), + }; + return isSafeToDiscard(receiver) + ? method + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: receiver, loc: locOf(expr.expression) }], + result: method, + type: DYN, + loc: locOf(expr), + }; + } lowerer.unsupported("SC1090", expr, `${kind} methods as values (call '${name}' directly)`); } return null; } +/** The fill step of Buffer.alloc's value form: a number, or a string with + * no encoding (utf8), fills the fresh buffer statically; any other fill + * takes the checked-dynamic `fill` dispatch. */ +function bufferAllocFill(loc: SrcLoc): IrStmt { + const buf = varRef("buf", BYTES_U8, loc); + const fill = varRef("fill", DYN, loc); + const encoding = varRef("encoding", DYN, loc); + const test = (kind: "number" | "string" | "undefined", value: IrExpr): IrExpr => ({ + kind: "dynTest", + test: kind, + value, + type: BOOL, + loc, + }); + const fillStr: IrExpr = { + kind: "bytesIntrinsic", + method: "fillStr", + receiver: buf, + args: [ + { kind: "dynCheck", value: fill, type: STRING, loc }, + { kind: "strLit", value: "utf8", type: STRING, loc }, + ], + type: BYTES_U8, + loc, + }; + const fillNum: IrExpr = { + kind: "bytesIntrinsic", + method: "fillNum", + receiver: buf, + args: [{ kind: "dynCheck", value: fill, type: F64, loc }], + type: BYTES_U8, + loc, + }; + const other: IrExpr = { + kind: "dynInvoke", + recv: { kind: "dynFrom", value: buf, type: DYN, loc }, + method: "fill", + calleeName: "buf.fill", + args: [fill, encoding], + type: DYN, + loc, + }; + return { + kind: "if", + cond: test("number", fill), + then: [{ kind: "exprStmt", expr: fillNum, loc }], + else_: [ + { + kind: "if", + cond: { + kind: "logical", + op: "&&", + left: test("string", fill), + right: test("undefined", encoding), + type: BOOL, + loc, + }, + then: [{ kind: "exprStmt", expr: fillStr, loc }], + else_: [{ kind: "exprStmt", expr: other, loc }], + loc, + }, + ], + loc, + }; +} + +/** Buffer.alloc as a function value (`var alloc = Buffer.alloc` in + * tar-stream): the checked allocation, then `fill(fill, encoding)` on the + * fresh buffer when a fill is given, which is how Node's alloc fills. */ +function bufferAllocValue(lowerer: Lowerer, loc: SrcLoc): IrExpr { + const name = "%builtin.Buffer.alloc"; + if (!lowerer.builtinCallableValueFns.has(name)) { + lowerer.builtinCallableValueFns.set(name, name); + const params = ["size", "fill", "encoding"].map((id) => ({ localId: id, name: id, type: DYN })); + lowerer.liftedFns.push({ + name, + params, + returnType: BYTES_U8, + locals: [ + ...params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })), + { id: "buf", name: "buf", type: BYTES_U8, mutable: false }, + ], + loc, + body: [ + { + kind: "varDecl", + localId: "buf", + init: { + kind: "libCall", + fn: "buffer.allocValue", + args: [varRef("size", DYN, loc)], + type: BYTES_U8, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "dynTest", + test: "undefined", + negated: true, + value: varRef("fill", DYN, loc), + type: BOOL, + loc, + }, + then: [bufferAllocFill(loc)], + else_: null, + loc, + }, + { kind: "return", value: varRef("buf", BYTES_U8, loc), loc }, + ], + }); + } + return { + kind: "dynFrom", + value: { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [DYN, DYN, DYN], ret: BYTES_U8 }, + loc, + }, + fnName: "alloc", + type: DYN, + loc, + }; +} + /** A `T | undefined` class field read that the checker narrowed to `T` * where the field was assigned a value that may be missing (an unchecked * indexed read). TypeScript's assignment narrowing is not a runtime fact diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b1b4086cd..2da3d2595 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -9338,15 +9338,18 @@ function lowerObjLitGenericMethodCall( lowerer.checker.getCallSignatures(recvT).length > 0 ) { const value = tryLowerExpression(lowerer, access.expression); + // Builtin function values held as checked-dynamic functions + // (String.fromCharCode, JSON.stringify) bind the same way. if ( - value?.type.kind === "func" && - lowerer.dynConvertible(value.type) && + value && + (value.type.kind === "dyn" || + (value.type.kind === "func" && lowerer.dynConvertible(value.type))) && !call.arguments.nodes.some(ts.isSpreadElement) ) { const loc = locOf(call); return { kind: "dynInvoke", - recv: { kind: "dynFrom", value, type: DYN, loc }, + recv: value.type.kind === "dyn" ? value : { kind: "dynFrom", value, type: DYN, loc }, method: name, calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e2236f592..3d4dc039f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -12945,6 +12945,8 @@ export class Lowerer { loc, }; } + if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native")) + return fsRealpathValue(this, bi.member === "realpathSync.native", loc); const fn = builtinModuleFnOf(this, bi.module, bi.member); return fn ? this.lowerNativeCallableValue(fn, `${bi.module}.${bi.member}`, loc) : null; } @@ -13657,3 +13659,51 @@ function droppedMembers(lowerer: Lowerer, from: IrType, to: IrType, seen: Set ({ localId: id, name: id, type: DYN })); + lowerer.liftedFns.push({ + name, + params, + returnType: DYN, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })), + loc, + body: [ + { + kind: "return", + value: { + kind: "libCall", + fn: "fs.realpathValue", + args: [ + { kind: "varRef", localId: "path", type: DYN, loc }, + { kind: "varRef", localId: "options", type: DYN, loc }, + { kind: "boolLit", value: native, type: BOOL, loc }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + }); + } + return { + kind: "dynFrom", + value: { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [DYN, DYN], ret: DYN }, + loc, + }, + fnName: native ? "native" : "realpathSync", + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0f144ca2e..914c39fd6 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -547,6 +547,7 @@ async function compileExecutableNative( ...(features.dynTier ? { dynTier: true } : {}), ...(features.streamDyn ? { streamDyn: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.builtinValues ? { builtinValues: true } : {}), events: features.events, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d4e96a35a..e391b9439 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -85,6 +85,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "bytes.constructor", "bytes.instanceOf", "buffer.byteLenDyn", + "buffer.byteLenValue", + "buffer.allocValue", + "fs.realpathValue", "bytes.construct", "arrayBuffer.new", "sharedArrayBuffer.new", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1ffae7536..289d6ea6c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -687,6 +687,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** realpath(3) with Node's error shape (syscall "lstat" in the message, * Node's own spelling for realpathSync failures). +1 fresh string. */ "fs.realpathSync": { argTypes: [STRING], result: STRING }, + /** fs.realpathSync (or its .native when the flag is true) as a function + * value: path and options arrive unchecked; the result is a string or a + * Buffer by the options' encoding. */ + "fs.realpathValue": { argTypes: [DYN, DYN, BOOL], result: DYN }, /** Native realpath with syscall "realpath" on failure. +1 fresh string. */ "fs.realpathNativeSync": { argTypes: [STRING], result: STRING }, /** The os.userInfo() field trio (uv_os_get_passwd's slices): pw_name, @@ -2868,6 +2872,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * allocUnsafeSlow never pool. The size forms validate like Node * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, + /** Buffer.alloc as a function value: the size arrives unchecked. */ + "buffer.allocValue": { argTypes: [DYN], result: BYTES_U8 }, "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, @@ -2881,6 +2887,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * fromStr's — and Buffer.isEncoding(name) over a runtime string * (case-insensitive against Node's alias set). Pure; never throw. */ "buffer.byteLenDyn": { argTypes: [DYN, STRING], result: F64 }, + /** Buffer.byteLength as a function value: the encoding arrives at run time. */ + "buffer.byteLenValue": { argTypes: [DYN, DYN], result: F64 }, "buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 }, "buffer.isEncoding": { argTypes: [STRING], result: BOOL }, /** Buffer.concat(list, totalLength): the concatenation truncated or diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 808b20fe4..1ead7f6a7 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -569,6 +569,8 @@ const RUNTIME_FEATURE_RULES = { /** Checked-dynamic stream slots test values and pipe into JavaScript * stream objects (scr_stream_dyn.c). */ streamDyn: rule({ fns: ["stream.dynNative", "stream.pipeDyn", "stream.unpipeDyn"] }), + /** Builtin functions read as values (scr_builtin_values.c). */ + builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ emitter: rule({ @@ -752,6 +754,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stdioExtended: false, stream: false, streamDyn: false, + builtinValues: false, symbol: false, tls: false, tlsCa: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 7607034b2..86693609a 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -79,6 +79,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dynTier: false, streamDyn: false, eventsOnceSignal: false, + builtinValues: false, warningReplace: false, stdioExtended: false, regex: true, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json new file mode 100644 index 000000000..3db56c8ab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-npm-builtin-function-bind/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6570-npm-builtin-function-bind/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 258df8dec..97d206be2 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -61,6 +61,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_inspect.c", "inspect"], ["scr_console_native.c", "inspect"], ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")], + // Builtin functions read as values in JavaScript dependencies. + ["scr_builtin_values.c", "builtinValues"], ["scr_dc.c", "dc"], // The dynamic tier's jsval values (two-tier design I3): linked only into // programs whose IR has dynamic-tier operations. diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c new file mode 100644 index 000000000..7d9855e0e --- /dev/null +++ b/packages/runtime/src/scr_builtin_values.c @@ -0,0 +1,141 @@ +/* Builtin functions called as function values in JavaScript dependencies + * (`Buffer.byteLength.bind(Buffer)`, `var alloc = Buffer.alloc`, a stored + * `fs.realpathSync`): the arguments arrive unchecked, so these entries apply + * Node's argument handling before the ordinary operation. A separate unit, + * linked only by programs that read these values. */ +#include "scr_runtime.h" + +#include +#include + +/* Buffer.byteLength called as a function value (`Buffer.byteLength.bind( + * Buffer)` in truncate-utf8-bytes), with the encoding known only at run + * time. Node's ladder: a non-string source answers its byte length or + * throws before the encoding is read; an empty string answers 0; a falsy + * encoding, or one getEncodingOps does not know after ToString (any ASCII + * case), counts UTF-8. */ +double scr_buffer_byte_length_value(ScrDyn *value, ScrDyn *encoding) { + static const struct { const char *from; const char *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, + {"base64", "base64"}, {"base64url", "base64url"}, + {"latin1", "latin1"}, {"binary", "latin1"}, {"ascii", "ascii"}, + {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + const char *name = "utf8"; + if (value->kind == SCR_DYN_STR && value->v.str->len > 0 && scr_dyn_truthy(encoding)) { + ScrStr *raw = scr_dyn_string_coerce_js(encoding); + if (!raw) return 0; + for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) { + size_t n = strlen(map[i].from); + bool same = raw->len == n; + for (size_t k = 0; same && k < n; k++) { + char c = raw->data[k]; + if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A')); + same = c == map[i].from[k]; + } + if (same) { + name = map[i].to; + break; + } + } + scr_str_release(raw); + } + ScrStr *canonical = scr_str_new(name, strlen(name)); + double length = scr_buffer_byte_length_dyn(value, canonical); + scr_str_release(canonical); + return length; +} + +/* Buffer.alloc called as a function value (`var alloc = Buffer.alloc` in + * tar-stream): the size must be a number, ERR_INVALID_ARG_TYPE otherwise, + * then Node's allocation and range checks. The fill argument rides the + * compiled caller. +1, or NULL with the exception pending. */ +ScrBytes *scr_buffer_alloc_value(const ScrDyn *size) { + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_buffer_alloc(size->v.num); +} + +/* fs.realpathSync and fs.realpathSync.native called as function values + * (fs.realpath keeps `var origRealpathSync = fs.realpathSync`). Node's + * argument handling: options that are null, undefined or a function mean + * none, a string is the encoding, an object supplies `encoding`, anything + * else throws ERR_INVALID_ARG_TYPE; a path that is not a string converts + * with ToString (a Buffer decodes as UTF-8). The result is a string for + * utf8, a Buffer for "buffer", and the UTF-8 bytes decoded in any other + * encoding, whose name must be one Node knows. +1 or NULL. */ +ScrDyn *scr_fs_realpath_value(const ScrDyn *path, const ScrDyn *options, bool native) { + const ScrDyn *encoding = NULL; + if (options->kind == SCR_DYN_STR) { + encoding = options; + } else if (options->kind == SCR_DYN_OBJ) { + encoding = scr_dyn_obj_get(options, "encoding", 8); + } else if (options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL && + options->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("options", "of type string or an instance of Object", options); + return NULL; + } + static const struct { const char *from; const char *to; } names[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, {"base64", "base64"}, + {"base64url", "base64url"}, {"latin1", "latin1"}, {"binary", "latin1"}, + {"ascii", "ascii"}, {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, {"buffer", "buffer"}, + }; + const char *canonical = "utf8"; + if (encoding && encoding->kind != SCR_DYN_UNDEF && encoding->kind != SCR_DYN_NULL && + !(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 0)) { + const char *found = NULL; + if (encoding->kind == SCR_DYN_STR) { + for (size_t i = 0; i < sizeof names / sizeof names[0] && !found; i++) { + size_t n = strlen(names[i].from); + if (encoding->v.str->len != n) continue; + bool same = true; + for (size_t k = 0; k < n && same; k++) { + char c = encoding->v.str->data[k]; + if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A')); + same = c == names[i].from[k]; + } + if (same) found = names[i].to; + } + } + if (!found) { + ScrStr *shown = scr_dyn_string_coerce_js(encoding); + if (!shown) return NULL; + char msg[256]; + int n = snprintf(msg, sizeof msg, "The argument 'encoding' is invalid encoding. Received '%.*s'", + (int)(shown->len > 160 ? 160 : shown->len), shown->data); + scr_str_release(shown); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_ARG_VALUE"); + return NULL; + } + canonical = found; + } + ScrStr *p = path->kind == SCR_DYN_STR ? scr_str_retain(path->v.str) : scr_dyn_string_coerce_js(path); + if (!p) return NULL; + ScrStr *resolved = native ? scr_fs_realpath_promise(p) : scr_fs_realpath(p); + scr_str_release(p); + if (!resolved) return NULL; + ScrDyn *out; + if (!strcmp(canonical, "utf8")) { + out = scr_dyn_new_str(resolved); + } else { + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_buffer_from_str(resolved, utf8); + scr_str_release(utf8); + if (!strcmp(canonical, "buffer")) { + out = scr_dyn_new_buffer(bytes); + } else { + ScrStr *enc = scr_str_new(canonical, strlen(canonical)); + ScrStr *text = scr_bytes_to_str(bytes, enc); + scr_str_release(enc); + out = scr_dyn_new_str(text); + scr_str_release(text); + } + scr_bytes_release(bytes); + } + scr_str_release(resolved); + return out; +} diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index b7b913d7f..5625c8dcb 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -394,6 +394,8 @@ ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list * and each element, then the typed concat. NULL with the exception pending. */ ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); +ScrBytes *scr_buffer_alloc_value(const struct ScrDyn *size); +struct ScrDyn *scr_fs_realpath_value(const struct ScrDyn *path, const struct ScrDyn *options, bool native); ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ @@ -408,6 +410,7 @@ ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc); * Borrow; never throw. */ double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc); double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *enc); +double scr_buffer_byte_length_value(ScrDyn *value, ScrDyn *encoding); bool scr_bytes_is_encoding(const ScrStr *s); /* equals / compare / indexOf-lastIndexOf / fill / copy / swap / write on diff --git a/tests/corpus/6570-npm-builtin-function-bind/main.ts b/tests/corpus/6570-npm-builtin-function-bind/main.ts new file mode 100644 index 000000000..5acea77ed --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/main.ts @@ -0,0 +1,25 @@ +// Function.prototype.bind, call and apply on builtin static functions in +// JavaScript dependencies. The vendored truncate-utf8-bytes 1.0.2 (WTFPL) +// binds Buffer.byteLength at load time and calls the bound function per +// character; bound-builtins binds Buffer, Math, String and JSON statics. +// fs.realpath 1.0.0 (ISC) and value-forms read builtin functions as values: +// fs.realpathSync stored and compared, Buffer.alloc aliased, and a String +// method read for a feature probe. +import truncate from 'truncate-utf8-bytes'; +import { report, badLength } from 'bound-builtins'; +import realpath from 'fs.realpath'; +import { report as valueForms } from 'value-forms'; + +console.log(truncate('hello world', 5), truncate('héllo', 2), truncate('héllo', 3)); +console.log(JSON.stringify(truncate('a😀b', 3)), JSON.stringify(truncate('a😀b', 5)), truncate('short', 99)); +try { + (truncate as unknown as (value: unknown, n: number) => string)(42, 1); +} catch (error) { + console.log((error as Error).message); +} +for (const value of report()) console.log(value); +console.log(badLength()); + +console.log(realpath.realpathSync('/'), realpath.sync('/')); +realpath('/', (error, resolved) => console.log('async', error, resolved)); +for (const value of valueForms()) console.log(value); diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts new file mode 100644 index 000000000..9810c8bfc --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.d.ts @@ -0,0 +1,2 @@ +export declare function report(): unknown[]; +export declare function badLength(): string; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js new file mode 100644 index 000000000..7cc668957 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/index.js @@ -0,0 +1,42 @@ +// Function.prototype.bind, call and apply on builtin static functions in +// JavaScript: Buffer, Math, String and JSON. +'use strict'; +var byteLength = Buffer.byteLength.bind(Buffer); +var max = Math.max.bind(null, 10); +var fromCodes = String.fromCharCode.bind(String, 104); +var stringify = JSON.stringify.bind(JSON); +var parse = JSON.parse.bind(JSON); +var compare = Buffer.compare.bind(Buffer); + +exports.report = function report() { + return [ + byteLength('héllo'), + byteLength('héllo', 'latin1'), + byteLength('abcd', 'hex'), + byteLength('héllo', 'not-an-encoding'), + byteLength(''), + byteLength(Buffer.from([1, 2, 3])), + max(3, 42), + max(), + Math.min.call(null, 4, 2), + Math.abs.apply(null, [-7]), + fromCodes(105, 33), + String.fromCharCode.apply(null, [111, 107]), + stringify({ a: [1, 2] }), + stringify({ b: 1 }, null, 2), + compare(Buffer.from('a'), Buffer.from('b')), + parse('{"n":1}').n, + JSON.stringify(parse('[1,2]', function (key, value) { + return typeof value === 'number' ? value * 10 : value; + })), + ]; +}; + +exports.badLength = function badLength() { + try { + byteLength(42); + return 'no error'; + } catch (error) { + return error.code + ' ' + error.name; + } +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json new file mode 100644 index 000000000..dd7b59369 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/bound-builtins/package.json @@ -0,0 +1 @@ +{ "name": "bound-builtins", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE new file mode 100644 index 000000000..5bd884c25 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/LICENSE @@ -0,0 +1,43 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts new file mode 100644 index 000000000..308c19e6c --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.d.ts @@ -0,0 +1,6 @@ +declare function realpath(p: string, cb: (error: Error | null, resolved?: string) => void): void; +declare namespace realpath { + function realpathSync(p: string, cache?: object): string; + function sync(p: string, cache?: object): string; +} +export = realpath; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js new file mode 100644 index 000000000..b09c7c7e6 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/index.js @@ -0,0 +1,66 @@ +module.exports = realpath +realpath.realpath = realpath +realpath.sync = realpathSync +realpath.realpathSync = realpathSync +realpath.monkeypatch = monkeypatch +realpath.unmonkeypatch = unmonkeypatch + +var fs = require('fs') +var origRealpath = fs.realpath +var origRealpathSync = fs.realpathSync + +var version = process.version +var ok = /^v[0-5]\./.test(version) +var old = require('./old.js') + +function newError (er) { + return er && er.syscall === 'realpath' && ( + er.code === 'ELOOP' || + er.code === 'ENOMEM' || + er.code === 'ENAMETOOLONG' + ) +} + +function realpath (p, cache, cb) { + if (ok) { + return origRealpath(p, cache, cb) + } + + if (typeof cache === 'function') { + cb = cache + cache = null + } + origRealpath(p, cache, function (er, result) { + if (newError(er)) { + old.realpath(p, cache, cb) + } else { + cb(er, result) + } + }) +} + +function realpathSync (p, cache) { + if (ok) { + return origRealpathSync(p, cache) + } + + try { + return origRealpathSync(p, cache) + } catch (er) { + if (newError(er)) { + return old.realpathSync(p, cache) + } else { + throw er + } + } +} + +function monkeypatch () { + fs.realpath = realpath + fs.realpathSync = realpathSync +} + +function unmonkeypatch () { + fs.realpath = origRealpath + fs.realpathSync = origRealpathSync +} diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js new file mode 100644 index 000000000..b40305e73 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/old.js @@ -0,0 +1,303 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var pathModule = require('path'); +var isWindows = process.platform === 'win32'; +var fs = require('fs'); + +// JavaScript implementation of realpath, ported from node pre-v6 + +var DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG); + +function rethrow() { + // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and + // is fairly slow to generate. + var callback; + if (DEBUG) { + var backtrace = new Error; + callback = debugCallback; + } else + callback = missingCallback; + + return callback; + + function debugCallback(err) { + if (err) { + backtrace.message = err.message; + err = backtrace; + missingCallback(err); + } + } + + function missingCallback(err) { + if (err) { + if (process.throwDeprecation) + throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs + else if (!process.noDeprecation) { + var msg = 'fs: missing callback ' + (err.stack || err.message); + if (process.traceDeprecation) + console.trace(msg); + else + console.error(msg); + } + } + } +} + +function maybeCallback(cb) { + return typeof cb === 'function' ? cb : rethrow(); +} + +var normalize = pathModule.normalize; + +// Regexp that finds the next partion of a (partial) path +// result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] +if (isWindows) { + var nextPartRe = /(.*?)(?:[\/\\]+|$)/g; +} else { + var nextPartRe = /(.*?)(?:[\/]+|$)/g; +} + +// Regex to find the device root, including trailing slash. E.g. 'c:\\'. +if (isWindows) { + var splitRootRe = /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/; +} else { + var splitRootRe = /^[\/]*/; +} + +exports.realpathSync = function realpathSync(p, cache) { + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return cache[p]; + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstatSync(base); + knownHard[base] = true; + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + // NB: p.length changes. + while (pos < p.length) { + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + continue; + } + + var resolvedLink; + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // some known symbolic link. no need to stat again. + resolvedLink = cache[base]; + } else { + var stat = fs.lstatSync(base); + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + continue; + } + + // read the link if it wasn't read before + // dev/ino always return 0 on windows, so skip the check. + var linkTarget = null; + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + linkTarget = seenLinks[id]; + } + } + if (linkTarget === null) { + fs.statSync(base); + linkTarget = fs.readlinkSync(base); + } + resolvedLink = pathModule.resolve(previous, linkTarget); + // track this, if given a cache. + if (cache) cache[base] = resolvedLink; + if (!isWindows) seenLinks[id] = linkTarget; + } + + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } + + if (cache) cache[original] = p; + + return p; +}; + + +exports.realpath = function realpath(p, cache, cb) { + if (typeof cb !== 'function') { + cb = maybeCallback(cache); + cache = null; + } + + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return process.nextTick(cb.bind(null, null, cache[p])); + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstat(base, function(err) { + if (err) return cb(err); + knownHard[base] = true; + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + function LOOP() { + // stop if scanned past end of path + if (pos >= p.length) { + if (cache) cache[original] = p; + return cb(null, p); + } + + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + return process.nextTick(LOOP); + } + + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // known symbolic link. no need to stat again. + return gotResolvedLink(cache[base]); + } + + return fs.lstat(base, gotStat); + } + + function gotStat(err, stat) { + if (err) return cb(err); + + // if not a symlink, skip to the next path part + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + return process.nextTick(LOOP); + } + + // stat & read the link if not read before + // call gotTarget as soon as the link target is known + // dev/ino always return 0 on windows, so skip the check. + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + return gotTarget(null, seenLinks[id], base); + } + } + fs.stat(base, function(err) { + if (err) return cb(err); + + fs.readlink(base, function(err, target) { + if (!isWindows) seenLinks[id] = target; + gotTarget(err, target); + }); + }); + } + + function gotTarget(err, target, base) { + if (err) return cb(err); + + var resolvedLink = pathModule.resolve(previous, target); + if (cache) cache[base] = resolvedLink; + gotResolvedLink(resolvedLink); + } + + function gotResolvedLink(resolvedLink) { + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json new file mode 100644 index 000000000..9adf9cdf3 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/fs.realpath/package.json @@ -0,0 +1 @@ +{ "name": "fs.realpath", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "license": "ISC" } diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts new file mode 100644 index 000000000..2d051a0b8 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.d.ts @@ -0,0 +1,2 @@ +declare function truncate(string: string, byteLength: number): string; +export = truncate; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js new file mode 100644 index 000000000..39e899c37 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/index.js @@ -0,0 +1,5 @@ +'use strict'; + +var truncate = require("./lib/truncate"); +var getLength = Buffer.byteLength.bind(Buffer); +module.exports = truncate.bind(null, getLength); diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js new file mode 100644 index 000000000..7837a4e11 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/lib/truncate.js @@ -0,0 +1,42 @@ +'use strict'; + +function isHighSurrogate(codePoint) { + return codePoint >= 0xd800 && codePoint <= 0xdbff; +} + +function isLowSurrogate(codePoint) { + return codePoint >= 0xdc00 && codePoint <= 0xdfff; +} + +// Truncate string by size in bytes +module.exports = function truncate(getLength, string, byteLength) { + if (typeof string !== "string") { + throw new Error("Input must be string"); + } + + var charLength = string.length; + var curByteLength = 0; + var codePoint; + var segment; + + for (var i = 0; i < charLength; i += 1) { + codePoint = string.charCodeAt(i); + segment = string[i]; + + if (isHighSurrogate(codePoint) && isLowSurrogate(string.charCodeAt(i + 1))) { + i += 1; + segment += string[i]; + } + + curByteLength += getLength(segment); + + if (curByteLength === byteLength) { + return string.slice(0, i + 1); + } + else if (curByteLength > byteLength) { + return string.slice(0, i - segment.length + 1); + } + } + + return string; +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json new file mode 100644 index 000000000..05cd0181a --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/truncate-utf8-bytes/package.json @@ -0,0 +1,8 @@ +{ + "name": "truncate-utf8-bytes", + "version": "1.0.2", + "description": "Truncate string to given length in bytes", + "main": "index.js", + "types": "index.d.ts", + "license": "WTFPL" +} diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts new file mode 100644 index 000000000..ca583e5b3 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.d.ts @@ -0,0 +1 @@ +export declare function report(): unknown[]; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js new file mode 100644 index 000000000..d78a5de03 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/index.js @@ -0,0 +1,31 @@ +// Builtin functions read as values: tar-stream's `var alloc = Buffer.alloc`, +// fs.realpathSync stored and compared, and smol-toml's `''.isWellFormed` +// feature probe. +'use strict'; +var fs = require('fs'); +var alloc = Buffer.alloc; +var origRealpathSync = fs.realpathSync; +var HAS_WELLFORMED = !!''.isWellFormed; + +exports.report = function report() { + var header = alloc(8); + var filled = alloc(5, 'ab'); + var numbered = alloc(3, 7); + var out = []; + out.push(header.length, header[0], filled.toString(), numbered[2], typeof alloc, alloc.length); + try { + alloc('8'); + } catch (error) { + out.push(error.code); + } + out.push(origRealpathSync('/'), origRealpathSync === fs.realpathSync, typeof fs.realpathSync.native); + out.push(Buffer.isBuffer(origRealpathSync('/', 'buffer')), origRealpathSync('/', { encoding: 'hex' })); + out.push(fs.realpathSync.native('/')); + try { + origRealpathSync('/definitely/missing/path'); + } catch (error) { + out.push(error.code); + } + out.push(HAS_WELLFORMED, 'ok'.isWellFormed(), typeof ''.toUpperCase); + return out; +}; diff --git a/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json new file mode 100644 index 000000000..6f96b3124 --- /dev/null +++ b/tests/corpus/6570-npm-builtin-function-bind/node_modules/value-forms/package.json @@ -0,0 +1 @@ +{ "name": "value-forms", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/harness/__snapshots__/json-dyn.ts.txt b/tests/harness/__snapshots__/json-dyn.ts.txt index 4a09378fb..c0a9c7fe9 100644 --- a/tests/harness/__snapshots__/json-dyn.ts.txt +++ b/tests/harness/__snapshots__/json-dyn.ts.txt @@ -12,11 +12,4 @@ json-dyn.ts:46:7 - error SC2007: values of type '{ (text: string, reviver?: ((th | ^~~~~~~~ 47 | // The exact `string | undefined` root now lowers: strings serialize while - hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow - -json-dyn.ts:46:18 - error SC1090: JSON methods as values (call 'parse' directly) are not supported yet - - 45 | const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585) - 46 | const parseRef = JSON.parse; - | ^~~~~~~~~~ - 47 | // The exact `string | undefined` root now lowers: strings serialize while \ No newline at end of file + hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow \ No newline at end of file