Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/content/limitations/builtin-function-values.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.

`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments.
Class constructors passed to `util.inherits` or `util.deprecate` are refused.
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused.
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/executable-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/library-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
"dyn.builtinClass": "scr_dyn_builtin_class",
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/native-link-info-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface NativeLinkFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/backend/native/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ export interface CcOptions {
* line (escape-only programs ride the always-linked component encoder
* and never flip this). */
qs?: boolean;
/** The program uses EventEmitter or a stream class as an untyped
* constructor value (a constructor function's base): compiles
* scr_dyn_builtin_class.c, which also references the inspection,
* dyn-invoke and symbol units. */
dynBuiltinClass?: boolean;
/** The program uses native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler/src/backend/native/executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
"scr_util_callable.c",
),
unit(
(u) =>
u.opts.dynBuiltinClass &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol &&
(u.opts.emitter || u.opts.workers),
"scr_dyn_builtin_class.c",
),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
// The zlib stream classes reference both the zlib engine and the
// stream unit.
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/runtime-pack-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/runtime-pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/backend/warm-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export async function warmNativeCaches(
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/executable/executable-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/executable/executable-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface EarlyExecutableNativeFeatures {
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
Expand Down Expand Up @@ -188,6 +189,7 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
Expand Down
149 changes: 149 additions & 0 deletions packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { numLit } from "../../../ir/build.js";
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
import * as ts from "../../ts7/adapter.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";

/* EventEmitter and the stream classes as untyped constructor values, the
* bases of JavaScript constructor functions:
*
* function Bus() { EventEmitter.call(this); }
* util.inherits(Bus, EventEmitter);
*
* A compiled `class Bus extends EventEmitter` gets a native emitter prefix,
* but `new Bus()` on a constructor function builds an untyped object, so
* the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a
* function whose prototype carries Node's EventEmitter methods over the
* object's own `_events` table. The stream classes have prototype chains
* for util.inherits and instanceof; constructing over them refuses where
* it runs. These values appear only where a constructor function uses its
* base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`,
* `Base.prototype` and `instanceof Base` on an untyped value. */

/** scr_dyn_builtin_class's kind for a runtime class, or null. */
export function builtinClassKind(className: string): number | null {
switch (className) {
case RUNTIME_EMITTER_CLASS:
return 0;
case "%Readable":
return 2;
case "%Writable":
return 3;
case "%Duplex":
return 4;
case "%Transform":
return 5;
case "%PassThrough":
return 6;
default:
return null;
}
}

/** The classes by builtin module member. JavaScript sources bind
* `require("events").EventEmitter` as a module member rather than a class
* value, and `require("stream")` is the legacy Stream constructor. */
const MODULE_CLASS_KINDS: Readonly<Record<string, Readonly<Record<string, number>>>> = {
events: { EventEmitter: 0, default: 0 },
stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 },
};

/** The kind of a builtin module member that is one of those classes. */
export function builtinMemberClassKind(module: string, member: string): number | null {
return MODULE_CLASS_KINDS[module]?.[member] ?? null;
}

function unparenthesized(node: ts.Expression): ts.Expression {
let current = node;
while (ts.isParenthesizedExpression(current)) current = current.expression;
return current;
}

/** The kind of a node that names one of those classes: a class value, a
* builtin module member, or the events and stream modules themselves. */
export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
const expr = unparenthesized(node);
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (mapped?.kind === "classval") return builtinClassKind(mapped.className);
const member = ts.isIdentifier(expr)
? lowerer.builtinImportOf(expr)
: ts.isPropertyAccessExpression(expr)
? lowerer.builtinMemberOf(expr)
: null;
if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null;
const module = lowerer.builtinNamespaceModuleOf(expr);
return module === "events" ? 0 : module === "stream" ? 1 : null;
}

/** Whether evaluating the node can be skipped: a binding or a member of
* one, or of a builtin require. */
function referenceOnly(node: ts.Expression): boolean {
const expr = unparenthesized(node);
if (ts.isIdentifier(expr)) return true;
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
const base = unparenthesized(expr.expression);
return (
ts.isIdentifier(base) ||
(ts.isCallExpression(base) &&
ts.isIdentifier(base.expression) &&
base.expression.text === "require" &&
base.arguments.nodes.length === 1 &&
ts.isStringLiteral(base.arguments.nodes[0]!))
);
}
return false;
}

/** The untyped constructor named by a node, or null when it names none of
* those classes. */
export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
const kind = builtinClassKindOf(lowerer, node);
if (kind === null) return null;
const loc = locOf(node);
const value = builtinClassValue(kind, loc);
if (referenceOnly(node)) return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }],
result: value,
type: DYN,
loc,
};
}

export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
}

/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
* values: Function.prototype.call/apply over the untyped constructor. */
export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
const method = callee.name.text;
if (method !== "call" && method !== "apply") return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const receiver = builtinClassDynValueOf(lowerer, callee.expression);
if (receiver === null) return null;
const loc = locOf(call);
return {
kind: "dynInvoke",
recv: receiver,
method,
calleeName: callee.getText(),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
}

/** `value instanceof Base` for an untyped value. */
export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr {
return {
kind: "libCall",
fn: "dyn.builtinInstanceOf",
args: [value, numLit(kind, loc)],
type: BOOL,
loc,
};
}
14 changes: 14 additions & 0 deletions packages/compiler/src/frontend/lowering/class-prototypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js";
import * as ts from "../ts7/adapter.js";
import {
BOOL,
Expand Down Expand Up @@ -450,6 +451,19 @@ export function lowerClassPrototypeData(
exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
// EventEmitter.prototype and the stream prototypes: the untyped
// constructor's prototype object (builtin-class-dyn.ts).
const builtinKind = builtinClassKind(info.def.name);
if (builtinKind !== null) {
const loc = locOf(expr);
return {
kind: "dynKeyGet",
value: builtinClassValue(builtinKind, loc),
key: { kind: "strLit", value: "prototype", type: STRING, loc },
type: DYN,
loc,
};
}
const parent = expr.parent;
const descriptorKey =
ts.isCallExpression(parent) &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
import * as posix from "node:path/posix";
import { type Lowerer, PoisonError } from "../lowerer.js";
Expand Down Expand Up @@ -538,6 +539,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
// EventEmitter and the stream classes are the runtime's untyped
// constructors, so constructor functions can inherit from them.
const classKind = builtinMemberClassKind(bi.module, bi.member);
if (classKind !== null) return builtinClassValue(classKind, loc);
// A function member is a function value that fences when called.
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
Expand Down
25 changes: 25 additions & 0 deletions packages/compiler/src/frontend/lowering/expressions/relational.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
Expand Down Expand Up @@ -471,6 +472,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return checkedClassInstanceOf(lowerer, value, constructor, loc);
}
// `this instanceof Glob` in a JavaScript constructor function: the
// function's prototype against the value's prototype chain.
if (
constructor?.type.kind === "func" &&
isJsSourceFile(expr.getSourceFile()) &&
lowerer.dynConvertible(constructor.type)
) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "dyn.functionInstanceOf",
args: [value, { kind: "dynFrom", value: constructor, type: DYN, loc }],
type: BOOL,
loc,
};
}
lowerer.unsupported(
"SC1090",
expr.right,
Expand Down Expand Up @@ -579,6 +596,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
loc,
};
}
// `u instanceof EventEmitter` (or a stream class): an untyped object
// built by a constructor function inherits the untyped constructor's
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
// no untyped form, so the prototype chain answers for every value.
const builtinKind = builtinClassKind(target.def.name);
if (left.type.kind === "dyn" && builtinKind !== null) {
return builtinClassInstanceOf(left, builtinKind, loc);
}
if (left.type.kind === "dyn") {
lowerer.unsupported(
"SC1090",
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-calls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
Expand Down Expand Up @@ -264,6 +265,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (object) return object;
}

// `EventEmitter.call(this)` in a constructor function: the untyped
// EventEmitter constructor initializes the receiver.
const builtinClassCall = lowerBuiltinClassCall(lowerer, expr);
if (builtinClassCall) return builtinClassCall;

// require() of a JSON document: the shared parsed object.
const json = lowerJsonRequireCall(lowerer, expr);
if (json) return json;
Expand Down
Loading
Loading