diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 0fcf86ada..8db629857 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.dynBufferNum ? { dynBufferNum: true } : {}), events: features.processEvents, emitter: features.emitter, symbol: features.symbol, diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index bc4f1128a..575d8c195 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -28,6 +28,7 @@ import { elemAccess, vAdapters } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { emitUnionWiden } from "./expr-records.js"; +import { DYN_BUFFER_NUM_METHODS } from "../../ir/runtime-features.js"; export function emitDynamicExpr( host: LlvmEmitterContext, @@ -226,9 +227,12 @@ export function emitDynamicExpr( }); argsPtr = arr; } - host.declare( - `declare ptr @scr_dyn_invoke_prepared(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`, - ); + // Fixed-width Buffer method names take scr_bytes_dyn.c's entry, which + // runs them on Buffer receivers and dispatches any other receiver. + const invoke = DYN_BUFFER_NUM_METHODS.has(e.method) + ? "scr_dyn_invoke_buffer_num" + : "scr_dyn_invoke_prepared"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -237,7 +241,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @scr_dyn_invoke_prepared(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8cd40240f..72969fa4f 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -444,6 +444,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromBytes": "scr_buffer_from_bytes", "buffer.poolFromArray": "scr_buffer_from_arr", "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", + "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 84f448e93..5aa5307a0 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; + /** Fixed-width Buffer methods called on checked-dynamic values link + * scr_bytes_dyn.c; absent (false) for programs without them. */ + dynBufferNum?: 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..47cb91d96 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -119,6 +119,9 @@ export interface CcOptions { /** events.once with options (events.onceSignal): compiles * scr_events_once_signal.c. */ eventsOnceSignal?: boolean; + /** Fixed-width Buffer methods called on checked-dynamic values: + * compiles scr_bytes_dyn.c. */ + dynBufferNum?: 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..e135b9ee2 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"), + // Fixed-width Buffer methods called on checked-dynamic values. + unit((u) => u.opts.dynBufferNum === true && u.opts.dynInvoke, "scr_bytes_dyn.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..75cfaa577 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; + dynBufferNum: 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, + dynBufferNum: features.dynBufferNum === 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..45655c06f 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; + dynBufferNum?: boolean; events: boolean; emitter: boolean; symbol: boolean; diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index f8e49a516..49292c260 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -24,6 +24,7 @@ import { typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import type { Lowerer } from "../lowerer.js"; import { newFnCtx, own } from "../lowerer.js"; import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js"; @@ -1306,6 +1307,185 @@ function knownBufferProducer( return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac"; } +/** Buffer.from over a checked-dynamic source and encoding, both already in + * repeatable locals: Node's fromString takes utf8 for an absent, non-string + * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; + * other sources ignore the encoding. */ +function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { + const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; + const isString = (x: IrExpr): IrExpr => ({ + kind: "dynTest", + test: "string", + value: x, + type: BOOL, + loc, + }); + const name: IrExpr = { + kind: "ternary", + cond: { + kind: "logical", + op: "&&", + left: isString(encoding), + right: { + kind: "strEq", + negated: true, + left: { kind: "dynCheck", value: encoding, type: STRING, loc }, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: { kind: "dynCheck", value: encoding, type: STRING, loc }, + else_: utf8, + type: STRING, + loc, + }; + return { + kind: "ternary", + cond: isString(source), + then: { + kind: "libCall", + fn: "buffer.poolFromStrChecked", + args: [{ kind: "dynCheck", value: source, type: STRING, loc }, name], + type: BYTES_U8, + loc, + }, + else_: { + kind: "libCall", + fn: "buffer.poolFromDynRef", + args: [source, utf8], + type: BYTES_U8, + loc, + }, + type: BYTES_U8, + loc, + }; +} + +/** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in + * source order, and builds the result from their references. */ +function withDynBufferArguments( + lowerer: Lowerer, + source: IrExpr, + encNode: ts.Expression | undefined, + loc: SrcLoc, + build: (source: IrExpr, encoding: IrExpr) => IrExpr, +): IrExpr { + const v = lowerer.declareHiddenLocal("%bufferValue", DYN); + const e = lowerer.declareHiddenLocal("%bufferEncoding", DYN); + const result = build(varRef(v.id, DYN, loc), varRef(e.id, DYN, loc)); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: v.id, init: lowerer.coerceToExpected(source, DYN), loc }, + { + kind: "varDecl", + localId: e.id, + init: + encNode === undefined + ? dynUndefinedExpr(loc) + : lowerer.coerceToExpected(lowerer.lowerExpr(encNode), DYN), + loc, + }, + ], + result, + type: result.type, + loc, + }; +} + +/** Buffer.from(value, encoding) over a checked-dynamic value whose encoding + * is not a literal: yazl forwards its `encoding` parameter, often undefined. + * Null when the encoding is absent or a literal. */ +function dynBufferFromRuntimeEncoding( + lowerer: Lowerer, + value: IrExpr, + encNode: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr | null { + if (encNode === undefined || lowerer.typeOf(encNode).isStringLiteralType()) return null; + return withDynBufferArguments(lowerer, value, encNode, loc, (source, encoding) => + dynBufferFromParts(source, encoding, loc), + ); +} + +/** The deprecated `new Buffer(arg[, encoding])` in a dependency's + * JavaScript, through the global or an alias of it (buffer-crc32 and yazl + * keep it as the fallback for runtimes without Buffer.alloc/Buffer.from). + * Node never emits DEP0005 for calls from node_modules, so the construction + * is exactly Node's: a number allocates zero-filled bytes (and throws + * ERR_INVALID_ARG_TYPE beside a string second argument), anything else + * converts as Buffer.from does. Null elsewhere: other callers owe the + * deprecation warning. */ +export function lowerNodeModulesBufferConstruct( + lowerer: Lowerer, + expr: ts.NewExpression, +): IrExpr | null { + const sf = expr.getSourceFile(); + if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null; + const args = expr.arguments?.nodes ?? []; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null; + const loc = locOf(expr); + const value = withDynBufferArguments( + lowerer, + lowerer.lowerExpr(args[0]!), + args[1], + loc, + (source, encoding) => ({ + kind: "ternary", + cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, + then: { + kind: "ternary", + cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc }, + then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, + else_: { + kind: "libCall", + fn: "buffer.alloc", + args: [{ kind: "dynCheck", value: source, type: F64, loc }], + type: BYTES_U8, + loc, + }, + type: BYTES_U8, + loc, + }, + else_: dynBufferFromParts(source, encoding, loc), + type: BYTES_U8, + loc, + }), + ); + return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }; +} + +/** Buffer.from(array or bytes, extra): Node copies an array-like source and + * ignores the encoding/offset argument (yazl passes its `encoding` through + * unchanged), but JavaScript still evaluates it after the source. */ +function withIgnoredBufferArgument( + lowerer: Lowerer, + extraNode: ts.Expression | undefined, + source: IrExpr, + build: (source: IrExpr) => IrExpr, +): IrExpr { + if (extraNode === undefined) return build(source); + const extra = lowerer.lowerExpr(extraNode); + if (isSafeToDiscard(extra)) return build(source); + const loc = source.loc; + const tmp = lowerer.declareHiddenLocal("%bufferSource", source.type); + const result = build(varRef(tmp.id, source.type, loc)); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: tmp.id, init: source, loc }, + { kind: "exprStmt", expr: extra, loc }, + ], + result, + type: result.type, + loc, + }; +} + export function lowerBufferStaticCall( lowerer: Lowerer, call: ts.CallExpression, @@ -1378,17 +1558,23 @@ function lowerBufferStaticValue( ) { // A number[] literal (contextually typed by the lib's readonly // number[] slot — build it element-wise, the Set-seed pattern). - if (args.length === 1) { - const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); - const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; - return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc }; - } + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); + const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; + return withIgnoredBufferArgument(lowerer, args[1], seed, (array) => ({ + kind: "libCall", + fn: "buffer.poolFromArray", + args: [array], + type: BYTES_U8, + loc, + })); } else { const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); const value = lowerer.lowerExpr(argNode); // JS callback storage can be native DYN even when the checker // reports any (or a narrowed Buffer). Dispatch on its actual ABI. if (value.type.kind === "dyn") { + const runtimeEncoding = dynBufferFromRuntimeEncoding(lowerer, value, args[1], loc); + if (runtimeEncoding) return runtimeEncoding; const encName = args[1] ? bufEncoding(lowerer, "Buffer.from", args[1]) : "utf8"; const enc: IrExpr = { kind: "strLit", value: encName, type: STRING, loc }; return { @@ -1432,26 +1618,25 @@ function lowerBufferStaticValue( // { toText: true } ? string : Uint8Array` result) whose lowered // value is a u8 view copies like the typed form. if ( - args.length === 1 && - ((srcIr?.kind === "bytes" && srcIr.elem === "u8") || - (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8")) + (srcIr?.kind === "bytes" && srcIr.elem === "u8") || + (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8") ) { - return { + return withIgnoredBufferArgument(lowerer, args[1], value, (bytes) => ({ kind: "libCall", fn: "buffer.poolFromBytes", - args: [value], + args: [bytes], type: BYTES_U8, loc, - }; + })); } - if (args.length === 1 && srcIr?.kind === "array" && srcIr.elem.kind === "f64") { - return { + if (srcIr?.kind === "array" && srcIr.elem.kind === "f64") { + return withIgnoredBufferArgument(lowerer, args[1], value, (array) => ({ kind: "libCall", fn: "buffer.poolFromArray", - args: [value], + args: [array], type: BYTES_U8, loc, - }; + })); } // A union of strings, u8 views and undefined/null (a JS-inferred // npm result such as pako's deflate): the checked form dispatches diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 49e7ad9fc..4be6cf088 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -100,7 +100,7 @@ import { isAssertClassNew } from "./assert-instances.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { knownBufEncoding } from "./containers/bytes.js"; +import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; @@ -9204,6 +9204,11 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); + // A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts). + { + const buffer = lowerNodeModulesBufferConstruct(lowerer, expr); + if (buffer) return buffer; + } // new Function(...): Node's EvalError under // --disallow-code-generation-from-strings (code-generation.ts). if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function") diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index bec8528c4..0ddfa0ac4 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -2,6 +2,7 @@ import { declareTypedArguments } from "./typed-arguments.js"; import { varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; +import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -59,6 +60,8 @@ function declareCallReceiver( * same collection lists top-level declarations ride. */ export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean { const symbol = lowerer.resolveValueSymbol(ident); + // A reassigned JavaScript declaration is read through its module global. + if (isRebindableFunctionSymbol(lowerer, symbol ?? undefined)) return false; if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true; const decl = symbol ? symbol.declarations[0] : undefined; return ( diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3a786aa4e..fab075524 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js"; * seeds), npm/JSON import collection, per-file %init functions, %main, and * the module artifacts (globals, embedded npm tables) the IR module carries. */ import * as ts from "../ts7/adapter.js"; +import { collectRebindableFunctions, isRebindableFunction } from "./rebindable-functions.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; import { dirname as dirnamePath, resolve as resolvePath } from "node:path"; @@ -246,8 +247,12 @@ export function splitFiles(lowerer: Lowerer): FileParts[] { const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; return files.map((sf) => { const fp: FileParts = { sf, fnDecls: [], classDecls: [], topStmts: [] }; + const rebindable = collectRebindableFunctions(lowerer, sf); for (const stmt of sf.statements.nodes) { - if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); + // A reassigned JavaScript declaration lives in a module global that + // the init body assigns first (rebindable-functions.ts). + if (ts.isFunctionDeclaration(stmt) && rebindable.has(stmt)) fp.topStmts.push(stmt); + else if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); else if (ts.isClassDeclaration(stmt)) fp.classDecls.push(stmt); // Namespaces: ambient/type-only ones are zero-runtime and skip; // instantiated bodies FLATTEN into this file's parts (functions/ @@ -1274,6 +1279,21 @@ export function collectGlobals( // collected earlier in this pass must resolve them (lower-expando.ts). collectExpandoMembers(lowerer, sf); for (const stmt of topStmts) { + if (ts.isFunctionDeclaration(stmt) && stmt.name && isRebindableFunction(lowerer, stmt)) { + const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${stmt.name.text}`, + name: stmt.name.text, + type: DYN, + mutable: true, + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsByDeclNode.set(stmt, g); + lowerer.globalsList.push(g); + } + continue; + } // `export default `: the module's `default` binding is a const // module global (registered under the checker's default-export // symbol — the symbol a default IMPORT's alias chain terminates at, @@ -2836,6 +2856,23 @@ export function lowerFileInit( loc: loc0, }); } + // Reassigned function declarations are initialized when the module + // starts, as declarations are hoisted: before its dependencies run, so + // a module of an import cycle can call one while it initializes. Their + // statements then lower to nothing (rebindable-functions.ts). + for (const stmt of stmts) { + if (!ts.isFunctionDeclaration(stmt) || !stmt.name || !isRebindableFunction(lowerer, stmt)) + continue; + const g = lowerer.globalsByDeclNode.get(stmt); + if (!g) continue; + header.push({ + kind: "assign", + localId: g.id, + value: lowerer.coerceToExpected(lowerer.lowerLambda(stmt), DYN), + loc: locOf(stmt), + }); + ctx.hoistedFnDecls.add(stmt); + } for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) { const npm = lowerer.npmInitActions.get(sf)?.get(stmt); if (npm !== undefined && npm.length > 0) { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 69f6cc99b..168d578f1 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -5917,11 +5917,44 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar writes = indexed; lowerer.jsBindingWritesByOwner.set(owner, writes); } - return (writes.get(decl.name.text) ?? []).some( - (node) => - lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol && - (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + const candidates = (writes.get(decl.name.text) ?? []).filter( + (node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol, ); + if ( + candidates.some( + (node) => + (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + ) + ) + return true; + // A plain JavaScript write of a value the declared type cannot hold (the + // checker does not report it): buffer-crc32 replaces its number[] table + // with `new Int32Array(table)`. Only a different representation counts; + // writes that coerce into the declared type keep it. + // A write whose checker type is the declaration's own maps to the same + // representation; the declared type is mapped only for the others. + const declaredType = lowerer.typeOf(decl.name); + const differing = candidates.filter( + (node) => + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + lowerer.typeOf(node.right) !== declaredType, + ); + if (differing.length === 0) return false; + const declared = lowerer.mapTypeOf(declaredType); + if (declared === null || declared.kind === "dyn" || declared.kind === "union") return false; + return differing.some((node) => { + const written = lowerer.mapTypeOf(lowerer.typeOf(node.right)); + return ( + written !== null && + written.kind !== "dyn" && + written.kind !== "union" && + !isUnitType(written) && + lowerer.dynConvertible(written) && + lowerer.dynConvertible(declared) && + !typeEquals(written, declared) && + (written.kind !== declared.kind || written.kind === "bytes") + ); + }); } function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 8708a0845..259847076 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -37,6 +37,7 @@ import { isOptionalProcessVersionsRead, } from "./builtins/process.js"; import { isSettledIteratorValueRead } from "./lower-generators.js"; +import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { concreteCollectionNarrow } from "./collection-narrowing.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js"; @@ -1851,6 +1852,9 @@ export class Lowerer { /** Syntactic writes indexed once per JS scope. Symbol and RHS type * checks remain contextual to each binding probe. */ readonly jsBindingWritesByOwner = new Map>(); + /** JavaScript function declarations their own module reassigns + * (rebindable-functions.ts). */ + readonly rebindableFunctions = new Set(); /** TRAP bindings: declarations whose initializer provably throws before * producing a value (its chain roots at an ambient-undefined name — * ambientUndefVarRootOf). Module init unwinds at the declaration, so no @@ -11687,6 +11691,9 @@ export class Lowerer { isSelfReference(ident: ts.Identifier): boolean { const ctx = this.ctx; if (!ctx.selfSymbol) return false; + // A reassigned JavaScript declaration names its module global, which + // may hold another function by the time the body runs. + if (isRebindableFunctionSymbol(this, ctx.selfSymbol)) return false; // The self binding can be shadowed by a scoped local of the same symbol? // No — a shadow is a different symbol; symbol identity is exact. return this.checker.getSymbolAtLocation(ident) === ctx.selfSymbol; diff --git a/packages/compiler/src/frontend/lowering/rebindable-functions.ts b/packages/compiler/src/frontend/lowering/rebindable-functions.ts new file mode 100644 index 000000000..da41833a6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/rebindable-functions.ts @@ -0,0 +1,64 @@ +/* JavaScript function declarations that their own module reassigns. + * + * Capability probes in published packages replace a function declaration + * with the implementation that works on the first call (yazl's + * `function bufferFrom(...) { bufferFrom = modern; ... }`). A top-level + * declaration normally lowers to a static function, which cannot change. + * A reassigned one instead becomes a mutable module global holding the + * function as a checked-dynamic value: the module initializer assigns it + * before its first statement (the declaration's hoisting), and reads, + * calls and writes of the name all go through the global. TypeScript + * rejects such writes, so only JavaScript sources take this path. */ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Top-level function declarations of `sf` that an assignment anywhere in + * the file writes, recorded for the queries below. */ +export function collectRebindableFunctions( + lowerer: Lowerer, + sf: ts.SourceFile, +): Set { + const found = new Set(); + if (!isJsSourceFile(sf)) return found; + const byName = new Map(); + for (const stmt of sf.statements.nodes) { + if ( + ts.isFunctionDeclaration(stmt) && + stmt.name && + stmt.body && + stmt.typeParameters === undefined + ) + byName.set(stmt.name.text, stmt); + } + if (byName.size === 0) return found; + ts.walkPreorder(sf, (node) => { + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(node.left) + ) + return undefined; + const decl = byName.get(node.left.text); + if (!decl || found.has(decl)) return undefined; + const symbol = lowerer.checker.getSymbolAtLocation(node.left); + if (symbol && symbol.declarations.includes(decl)) found.add(decl); + return undefined; + }); + for (const decl of found) lowerer.rebindableFunctions.add(decl); + return found; +} + +export function isRebindableFunction(lowerer: Lowerer, decl: ts.Node | undefined): boolean { + return ( + decl !== undefined && ts.isFunctionDeclaration(decl) && lowerer.rebindableFunctions.has(decl) + ); +} + +/** Whether the symbol names a reassigned function declaration. */ +export function isRebindableFunctionSymbol( + lowerer: Lowerer, + symbol: ts.Symbol | undefined, +): boolean { + return symbol !== undefined && symbol.declarations.some((d) => isRebindableFunction(lowerer, d)); +} diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0f144ca2e..981d14bd3 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.dynBufferNum ? { dynBufferNum: 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..00ff5bce1 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -769,6 +769,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromBytes", "buffer.poolFromArray", "buffer.poolFromDyn", + "buffer.poolFromDynRef", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index cd3db784d..299e6e0ac 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2879,6 +2879,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + /** poolFromDyn that also copies an array crossing by reference. */ + "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index fd7c98cf6..bb165af26 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,6 +9,27 @@ import { } from "./ir.js"; import { everyModuleNode } from "./traverse.js"; +/** The fixed-width Buffer read/write method names (both "UInt" and "Uint" + * spellings) a checked-dynamic method call routes to scr_bytes_dyn.c. */ +export const DYN_BUFFER_NUM_METHODS: ReadonlySet = new Set( + ["read", "write"].flatMap((rw) => [ + ...["UInt", "Uint"].flatMap((u) => + ["8", "16LE", "16BE", "32LE", "32BE"].map((w) => `${rw}${u}${w}`), + ), + ...[ + "Int8", + "Int16LE", + "Int16BE", + "Int32LE", + "Int32BE", + "FloatLE", + "FloatBE", + "DoubleLE", + "DoubleBE", + ].map((n) => `${rw}${n}`), + ]), +); + const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ // DEP0040 rides the dyn-async unit's warning machinery. "punycode.warn", @@ -573,6 +594,11 @@ const RUNTIME_FEATURE_RULES = { streamDyn: rule({ fns: ["stream.dynNative", "stream.dynNativePipe", "stream.pipeDyn", "stream.unpipeDyn"], }), + /** Fixed-width Buffer read/write methods called on checked-dynamic + * values (scr_bytes_dyn.c). */ + dynBufferNum: rule({ + expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_NUM_METHODS.has(node.method), + }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ emitter: rule({ @@ -756,6 +782,7 @@ function noRuntimeFeatures(): RuntimeFeatures { stdioExtended: false, stream: false, streamDyn: false, + dynBufferNum: 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..d32d8564c 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, + dynBufferNum: false, warningReplace: false, stdioExtended: false, regex: true, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json new file mode 100644 index 000000000..62b900d3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6560-npm-js-capability-probes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json new file mode 100644 index 000000000..f0d46c0bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6565-rebindable-function-import-cycle/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..fc56fbe91 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")], + // Fixed-width Buffer methods called on checked-dynamic values. + ["scr_bytes_dyn.c", all("dynBufferNum", "dynInvoke")], ["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_bytes.h b/packages/runtime/src/scr_bytes.h index b7b913d7f..c0f6ed120 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -389,6 +389,7 @@ ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc); ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c new file mode 100644 index 000000000..d4b29574b --- /dev/null +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -0,0 +1,101 @@ +/* The fixed-width read/write methods of a Buffer held as a checked-dynamic + * value: a JavaScript dependency's `buf.writeUInt32LE(v, 0)` on a buffer it + * got back untyped (yazl). The compiler routes only the dyn method calls + * that name one of these methods here, so programs without them link + * nothing new; any other receiver takes the ordinary dispatch. */ +#include "scr_runtime.h" + +#include +#include +#include + +static const struct { + const char *name; + ScrBytesNumKind kind; + bool le; +} scr_bytes_dyn_names[] = { + { "UInt8", SCR_BN_U8, false }, { "Uint8", SCR_BN_U8, false }, + { "Int8", SCR_BN_I8, false }, { "UInt16LE", SCR_BN_U16, true }, + { "Uint16LE", SCR_BN_U16, true }, { "UInt16BE", SCR_BN_U16, false }, + { "Uint16BE", SCR_BN_U16, false }, { "Int16LE", SCR_BN_I16, true }, + { "Int16BE", SCR_BN_I16, false }, { "UInt32LE", SCR_BN_U32, true }, + { "Uint32LE", SCR_BN_U32, true }, { "UInt32BE", SCR_BN_U32, false }, + { "Uint32BE", SCR_BN_U32, false }, { "Int32LE", SCR_BN_I32, true }, + { "Int32BE", SCR_BN_I32, false }, { "FloatLE", SCR_BN_F32, true }, + { "FloatBE", SCR_BN_F32, false }, { "DoubleLE", SCR_BN_F64, true }, + { "DoubleBE", SCR_BN_F64, false }, +}; + +static size_t scr_bytes_dyn_width(ScrBytesNumKind kind) { + switch (kind) { + case SCR_BN_U8: + case SCR_BN_I8: + return 1; + case SCR_BN_U16: + case SCR_BN_I16: + return 2; + case SCR_BN_F64: + return 8; + default: + return 4; + } +} + +/* Node's checkInt range test and message, before the offset is looked at. */ +static bool scr_bytes_dyn_value_in_range(double value, size_t width, bool sign) { + double max = sign ? exp2((double)(8 * width - 1)) - 1 : exp2((double)(8 * width)) - 1; + double min = sign ? -exp2((double)(8 * width - 1)) : 0; + if (!(value > max || value < min)) return true; + char recv[48]; + scr_num_received(value, recv); + char minb[32], maxb[32]; + scr_f64_to_str(min, minb); + scr_f64_to_str(max, maxb); + char msg[160]; + int mlen = snprintf(msg, sizeof msg, + "The value of \"value\" is out of range. It must be >= %s and <= %s. Received %s", + minb, maxb, recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); + return false; +} + +/* Node's order: a write converts the value with ToNumber and checks its + * range, then the offset must be a number (absent means 0), then the + * bounds. +1, or NULL with the exception pending. */ +static ScrDyn *scr_bytes_dyn_num_method(ScrBytes *b, bool read, size_t at, ScrDyn *const *args, + size_t argc) { + ScrBytesNumKind kind = scr_bytes_dyn_names[at].kind; + double value = 0; + if (!read) { + if (!scr_dyn_number_coerce_js(argc > 0 ? args[0] : scr_dyn_undefined(), &value)) return NULL; + if (kind != SCR_BN_F32 && kind != SCR_BN_F64) { + bool sign = kind == SCR_BN_I8 || kind == SCR_BN_I16 || kind == SCR_BN_I32; + if (!scr_bytes_dyn_value_in_range(value, scr_bytes_dyn_width(kind), sign)) return NULL; + } + } + size_t offset_at = read ? 0 : 1; + double offset = 0; + if (argc > offset_at && args[offset_at]->kind != SCR_DYN_UNDEF) { + if (args[offset_at]->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("offset", "of type number", args[offset_at]); + return NULL; + } + offset = args[offset_at]->v.num; + } + bool le = scr_bytes_dyn_names[at].le; + double out = read ? scr_bytes_read_num(b, offset, kind, le) : scr_bytes_write_num(b, value, offset, kind, le); + return scr_exc_pending() ? NULL : scr_dyn_new_num(out); +} + +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + if (recv->kind == SCR_DYN_BYTES && recv->buffer && !recv->v.bytes->is_data_view) { + bool read = !strncmp(method, "read", 4); + const char *rest = method + (read ? 4 : 5); + for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) { + if (!strcmp(rest, scr_bytes_dyn_names[at].name)) + return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); + } + } + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 4d7ef5f67..c852388e9 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1350,6 +1350,24 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding return scr_buffer_pool_adopt(out); } +/* scr_buffer_from_dyn_pooled for values that can be a typed array crossing + * an untyped slot by reference (a number[] passed to an untyped parameter, + * as yazl's Buffer.from(something, encoding) receives it): the array copies + * like the array it is. Used by the runtime-encoding Buffer.from forms. */ +ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding) { + if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(value) != NULL) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { + scr_dyn_release(materialized); + return NULL; + } + ScrBytes *out = scr_buffer_from_dyn_pooled(materialized, encoding); + scr_dyn_release(materialized); + return out; + } + return scr_buffer_from_dyn_pooled(value, encoding); +} + /* ── the checked-dynamic Buffer compare/equals validators ────────────── * Node's argument ladders for buf.equals / buf.compare / Buffer.compare * over dyn-boxed arguments (the invalid-input probes: string needles, diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 401bf2b39..143c8e649 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1134,6 +1134,10 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for the fixed-width Buffer read/write method names + * (scr_bytes_dyn.c): a Buffer receiver runs the method, any other takes the + * ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" diff --git a/tests/corpus/6560-npm-js-capability-probes/main.ts b/tests/corpus/6560-npm-js-capability-probes/main.ts new file mode 100644 index 000000000..e9eacee8e --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/main.ts @@ -0,0 +1,14 @@ +// JavaScript dependency forms from bundled zip packages, which run as Node +// runs them: +// - a var table replaced by an Int32Array view of itself (buffer-crc32); +// - function declarations that redefine themselves on the first call, and +// Buffer.from over an array with an undefined encoding (yazl); +// - the deprecated `new Buffer(...)` fallback both keep, and the Buffer +// methods called on the untyped buffers they return. +import { crc32, localHeader, signature, legacy } from 'zip-helpers'; + +console.log(crc32('hello'), crc32(Buffer.from([1, 2, 3])), crc32('world', 17), crc32(4)); +console.log(Array.from(localHeader('a.txt', 9)).join(',')); +console.log(Array.from(localHeader('b', 1)).join(',')); +console.log(Array.from(signature()).join(',')); +console.log(legacy()); diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts new file mode 100644 index 000000000..0b51fe366 --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts @@ -0,0 +1,4 @@ +export declare function crc32(input: Uint8Array | string | number, previous?: number): number; +export declare function localHeader(name: string, size: number): Uint8Array; +export declare function signature(): Uint8Array; +export declare function legacy(): string; diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js new file mode 100644 index 000000000..15fc0a0c4 --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js @@ -0,0 +1,125 @@ +// Small parts of buffer-crc32 (MIT) and yazl (MIT), in their published +// shapes: a var table replaced by an Int32Array view of itself, and +// function declarations that replace themselves with the implementation +// that works on the first call. +'use strict'; +var Buffer2 = require('buffer').Buffer; +var CRC_TABLE = [ + 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, + 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, + 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, + 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, + 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, + 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, + 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, + 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, + 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, + 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, + 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, + 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, + 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, + 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, + 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, + 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, + 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, + 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, + 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, + 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, + 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, + 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, + 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, + 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, + 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, + 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, + 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, + 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, + 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, + 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, + 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, + 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117 +]; +typeof Int32Array < 'u' && (CRC_TABLE = new Int32Array(CRC_TABLE)); + +function ensureBuffer(input) { + if (Buffer2.isBuffer(input)) return input; + var hasNewBufferAPI = typeof Buffer2.alloc == 'function' && typeof Buffer2.from == 'function'; + if (typeof input == 'number') return hasNewBufferAPI ? Buffer2.alloc(input) : new Buffer2(input); + if (typeof input == 'string') return hasNewBufferAPI ? Buffer2.from(input) : new Buffer2(input); + throw new Error('input must be buffer, number, or string, received ' + typeof input); +} + +function crc32(buf, previous) { + buf = ensureBuffer(buf); + for (var crc = ~~previous ^ -1, n = 0; n < buf.length; n++) crc = CRC_TABLE[(crc ^ buf[n]) & 255] ^ (crc >>> 8); + return (crc ^ -1) >>> 0; +} + +function bufferFrom(something, encoding) { + bufferFrom = modern; + try { + return bufferFrom(something, encoding); + } catch { + return (bufferFrom = legacy), bufferFrom(something, encoding); + } + function modern(something2, encoding2) { + return Buffer.from(something2, encoding2); + } + function legacy(something2, encoding2) { + return new Buffer(something2, encoding2); + } +} + +function bufferAlloc(size) { + bufferAlloc = modern; + try { + return bufferAlloc(size); + } catch { + return (bufferAlloc = legacy), bufferAlloc(size); + } + function modern(size2) { + return Buffer.alloc(size2); + } + function legacy(size2) { + return new Buffer(size2); + } +} + +function localHeader(name, size) { + var nameBuffer = bufferFrom(name, 'utf-8'); + var fixed = bufferAlloc(8); + fixed.writeUInt32LE(0x04034b50, 0); + fixed.writeUInt16LE(nameBuffer.length, 4); + fixed.writeUInt16LE(size, 6); + return Buffer.concat([fixed, nameBuffer]); +} + +var SIGNATURE = bufferFrom([0x50, 0x4b, 0x05, 0x06], undefined); + +function legacy() { + var sized = new Buffer2(3); + var text = new Buffer2('hi', 'utf8'); + var array = new Buffer2([1, 2, 255]); + var out = [sized.length, sized[0], text.toString(), array.readUInt16BE(1), array.readInt8(2)]; + try { + new Buffer2(2, 'utf8'); + } catch (error) { + out.push(error.code); + } + try { + array.writeUInt8(300, 0); + } catch (error) { + out.push(error.code); + } + try { + array.readUInt8('0'); + } catch (error) { + out.push(error.code); + } + return out.join(' '); +} + +function signature() { + return SIGNATURE; +} + +module.exports = { crc32: crc32, localHeader: localHeader, signature: signature, legacy: legacy }; diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json new file mode 100644 index 000000000..2192addcf --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json @@ -0,0 +1 @@ +{ "name": "zip-helpers", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6565-rebindable-function-import-cycle/main.ts b/tests/corpus/6565-rebindable-function-import-cycle/main.ts new file mode 100644 index 000000000..ff505d978 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/main.ts @@ -0,0 +1,5 @@ +// A package whose import cycle calls a reassigned function declaration +// while the cycle initializes. +import { report } from "cycle-format"; + +for (const line of report()) console.log(line); diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js new file mode 100644 index 000000000..730e7b34e --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js @@ -0,0 +1,8 @@ +import { format } from "./index.js"; + +// Runs before index.js's body: format is the hoisted declaration. +export const early = format("a"); + +export function describe(value) { + return `${typeof format} ${format(value)}`; +} diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts new file mode 100644 index 000000000..55a683bb7 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts @@ -0,0 +1,2 @@ +export declare function format(value: string): string; +export declare const report: () => string[]; diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js new file mode 100644 index 000000000..c96fadcad --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js @@ -0,0 +1,18 @@ +// A reassigned function declaration of a module in an import cycle: the +// dependency calls it while the cycle initializes, before this module's +// body runs, and gets the hoisted declaration; later calls see the +// replacement. +import { early, describe } from "./dep.js"; + +export function format(value) { + format = function formatUpper(text) { + return `[${String(text).toUpperCase()}]`; + }; + return `<${value}>`; +} + +export const report = () => [ + "early: " + early, + "now: " + format("b") + " " + format("c"), + "dep: " + describe("d"), +]; diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json new file mode 100644 index 000000000..becff6bc6 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json @@ -0,0 +1 @@ +{ "name": "cycle-format", "version": "1.0.0", "type": "module", "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } } }