Run buffer-crc32's and yazl's capability probes as Node does (#1549)

This commit is contained in:
Chris Tate
2026-10-20 18:02:43 -05:00
committed by GitHub
36 changed files with 724 additions and 25 deletions
@@ -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,
@@ -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();
@@ -444,6 +444,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
@@ -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;
@@ -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
@@ -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"),
@@ -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,
@@ -68,6 +68,7 @@ export interface EarlyExecutableNativeFeatures {
dynTier?: boolean;
streamDyn?: boolean;
eventsOnceSignal?: boolean;
dynBufferNum?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -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
@@ -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")
@@ -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 (
@@ -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 <expr>`: 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) {
@@ -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 {
@@ -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<ts.Node, Map<string, ts.BinaryExpression[]>>();
/** JavaScript function declarations their own module reassigns
* (rebindable-functions.ts). */
readonly rebindableFunctions = new Set<ts.FunctionDeclaration>();
/** 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;
@@ -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<ts.FunctionDeclaration> {
const found = new Set<ts.FunctionDeclaration>();
if (!isJsSourceFile(sf)) return found;
const byName = new Map<string, ts.FunctionDeclaration>();
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));
}
+1
View File
@@ -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,
@@ -769,6 +769,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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",
@@ -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
@@ -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<string> = 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<IrLibFn> = 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,
@@ -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,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6560-npm-js-capability-probes/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6565-rebindable-function-import-cycle/main.ts"
],
"diags": []
}
@@ -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.
+1
View File
@@ -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
+101
View File
@@ -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 <math.h>
#include <stdio.h>
#include <string.h>
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);
}
+18
View File
@@ -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,
+4
View File
@@ -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; ""
@@ -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());
@@ -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;
@@ -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 };
@@ -0,0 +1 @@
{ "name": "zip-helpers", "version": "1.0.0", "main": "index.js", "types": "index.d.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);
@@ -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)}`;
}
@@ -0,0 +1,2 @@
export declare function format(value: string): string;
export declare const report: () => string[];
@@ -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"),
];
@@ -0,0 +1 @@
{ "name": "cycle-format", "version": "1.0.0", "type": "module", "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } } }