Support zlib stream classes as const aliases and instanceof targets

`const C = zlib.Gzip` stopped at SC2020, and `instanceof zlib.Gzip`
(any zlib class) stopped at SC1090. A const binding of a zlib class is
now a compile-time alias with no runtime slot: `new C()`, `typeof C`,
`C.name` and `C === zlib.Gzip` lower at the use. `instanceof` against a
zlib class asks the runtime for the stream's mode when the operand is a
Transform, tests for a Transform first when it is typed as one of
Transform's bases, and answers false for other static types, as in Node.
Other uses of an alias as a value report SC1090 at the use.
This commit is contained in:
Chris Tate
2026-10-18 09:52:17 -05:00
parent 2fa4b19b45
commit b4ef932b72
16 changed files with 303 additions and 6 deletions
@@ -848,6 +848,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fetch.abortAny": "scr_fetch_abort_any",
"fetch.webIs": "scr_fetch_web_is",
"fetch.streamIs": "scr_fetch_stream_is",
"zlib.streamIs": "scr_zlib_stream_is",
"fetch.streamNew": "scr_fetch_stream_new",
"fetch.streamFrom": "scr_fetch_stream_from",
"fetch.readerRead": "scr_fetch_reader_read",
@@ -1,11 +1,12 @@
import { lowerBuiltinByteInput, errorFirstBytesCallback } from "./arguments.js";
import { dynUndefinedExpr, numLit } from "../../../ir/build.js";
import { dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { isNodeTypesPath } from "../../dts-paths.js";
import {
BOOL,
BYTES_U8,
DYN,
F64,
@@ -39,7 +40,8 @@ const ZLIB_CALLBACK_FNS: Readonly<Record<string, IrLibFn | undefined>> = {
unzip: "zlib.unzipCb",
};
const ZLIB_STREAM_T: IrType = { kind: "object", className: "%Transform" };
const ZLIB_STREAM_CLASS = "%Transform";
const ZLIB_STREAM_T: IrType = { kind: "object", className: ZLIB_STREAM_CLASS };
/** A zlib data argument, lowered once: Buffer/Uint8Array bytes, a string
* (UTF-8 encoded here), or a checked-dynamic value (the JS lane's untyped
@@ -130,6 +132,8 @@ export function zlibStreamModeOf(
symbol: ts.Symbol | null | undefined,
): number | null {
if (!symbol) return null;
const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0);
if (aliasMode !== undefined) return aliasMode;
const target =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const mode = ZLIB_STREAM_MODES.get(target.name);
@@ -494,3 +498,145 @@ export function lowerZlibModuleCall(
}
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
}
/** The zlib stream class an expression names (`zlib.Gzip`, an imported
* `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */
export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null {
let e = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e));
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name));
}
return null;
}
/** The class name of a zlib stream mode (Node's constructor name). */
export function zlibClassName(mode: number): string {
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
return "Zlib";
}
/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a
* binding), null when the symbol is a const binding of something else,
* undefined when it is not a const binding. Resolved from the declaration,
* so uses lowered before the declaration statement see the same answer. */
function zlibClassAliasModeOf(
lowerer: Lowerer,
symbol: ts.Symbol,
depth: number,
): number | null | undefined {
const cached = lowerer.zlibClassAliases.get(symbol);
if (cached !== undefined) return cached;
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined;
const decl = lowerer.checker.valueDeclarationOf(symbol);
if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
return undefined;
}
// const only: a var or let binding could be reassigned
const list = decl.parent;
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
return undefined;
}
let init = decl.initializer;
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === undefined || depth > 16) return null;
let mode: number | null = null;
if (ts.isIdentifier(init)) {
const next = lowerer.checker.getSymbolAtLocation(init);
if (next !== undefined) {
const nested = zlibClassAliasModeOf(lowerer, next, depth + 1);
mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested;
}
} else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) {
mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name));
}
if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode);
return mode;
}
/** True for `const C = zlib.Gzip` (or an alias of an alias): a
* compile-time zlib class alias that owns no runtime slot, so the
* statement lowering skips it. */
export function registerZlibClassAlias(
lowerer: Lowerer,
nameNode: ts.Node,
init: ts.Expression | undefined,
): boolean {
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null;
}
/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */
export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null {
return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null;
}
/** `value instanceof zlib.Gzip` (any spelling of the class, aliases
* included). zlib streams are runtime Transforms: a Transform operand asks
* the runtime for the stream's mode, and an operand typed as one of
* Transform's bases (Duplex, Readable, EventEmitter) is first tested for a
* Transform. Any other static type answers false after the operand
* evaluates. */
export function lowerZlibInstanceOf(
lowerer: Lowerer,
expr: ts.BinaryExpression,
mode: number,
loc: SrcLoc,
): IrExpr {
const value = lowerer.lowerExpr(expr.left);
const type = value.type;
const holdsStream = (className: string): boolean =>
className === ZLIB_STREAM_CLASS || lowerer.isSubclassOf(ZLIB_STREAM_CLASS, className);
const arms = type.kind === "union" ? lowerer.unions.get(type.unionId)?.arms : undefined;
if (
type.kind === "dyn" ||
arms?.some((arm) => arm.kind === "object" && holdsStream(arm.className))
) {
lowerer.unsupported(
"SC1090",
expr,
`'instanceof zlib.${zlibClassName(mode)}' on an untyped or union value (give the operand a stream type first)`,
);
}
if (type.kind === "object" && holdsStream(type.className)) {
const isMode = (stream: IrExpr): IrExpr => ({
kind: "libCall",
fn: "zlib.streamIs",
args: [stream, numLit(mode, loc)],
type: BOOL,
loc,
});
if (type.className === ZLIB_STREAM_CLASS) return isMode(value);
const local = lowerer.declareHiddenLocal("%zlibTest", type);
const ref = varRef(local.id, type, loc);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
result: {
kind: "logical",
op: "&&",
left: {
kind: "instanceOf",
value: ref,
className: ZLIB_STREAM_CLASS,
type: BOOL,
loc,
},
right: isMode({ kind: "downcast", value: ref, type: ZLIB_STREAM_T, loc }),
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: value, loc }],
result: { kind: "boolLit", value: false, type: BOOL, loc },
type: BOOL,
loc,
};
}
@@ -10,7 +10,7 @@ import { InternalCompilerError } from "../../errors.js";
* hierarchy registration. */
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
import * as ts from "../ts7/adapter.js";
import { lowerZlibStreamNew, zlibStreamModeOf } from "./builtins/compression.js";
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
import type { FnCtx, Lowerer } from "./lowerer.js";
import {
BOOL,
@@ -8664,7 +8664,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// `new zlib.Gzip(options?)` — the zlib stream classes.
const zlibModeNs = zlibStreamModeOf(lowerer, resolved);
if (zlibModeNs !== null) {
const what = `new zlib.${expr.expression.name.text}`;
const what = `new zlib.${zlibClassName(zlibModeNs)}`;
return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibModeNs, what, expr, loc);
}
// `new N.C(...)` / `new a.Point(...)` — construction through a
@@ -8922,7 +8922,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (streamInfo) return lowerStreamNew(lowerer, expr, streamInfo);
const zlibMode = zlibStreamModeOf(lowerer, symbol);
if (zlibMode !== null) {
const what = `new zlib.${symbol!.name}`;
const what = `new zlib.${zlibClassName(zlibMode)}`;
return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibMode, what, expr, loc);
}
// `new URL(input)`: the WHATWG URL class (stdlib/@types provenance —
@@ -31,7 +31,14 @@ import { literalUnionArm } from "../union-discriminants.js";
* (FieldTarget). */
import { isFsStreamMember, lowerFsStreamProperty } from "./builtins/fs-streams.js";
import * as ts from "../ts7/adapter.js";
import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "./builtins/compression.js";
import {
isZlibStreamMember,
lowerZlibInstanceOf,
lowerZlibStreamBytesWritten,
zlibClassAliasOf,
zlibClassModeOfExpr,
zlibClassName,
} from "./builtins/compression.js";
import { constituentTypes } from "../ts7/checker.js";
import { dirname } from "node:path";
import * as posix from "node:path/posix";
@@ -608,6 +615,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (isNodeModuleValue(lowerer, expr.expression)) {
return { kind: "strLit", value: "object", type: STRING, loc };
}
// `typeof zlib.Gzip`, `typeof C` over a zlib class alias.
if (zlibClassModeOfExpr(lowerer, expr.expression) !== null) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
if (ts.isPropertyAccessExpression(expr.expression)) {
const member = expr.expression;
const prototype = member.expression;
@@ -864,6 +875,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
`a builtin function alias as an escaping value (call '${expr.text}' directly)`,
);
}
// `const C = zlib.Gzip` owns no runtime slot: construction,
// instanceof, typeof and .name lower at their use sites.
if (sym && zlibClassAliasOf(lowerer, sym) !== null) {
lowerer.unsupported(
"SC1090",
expr,
`a zlib class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
);
}
}
// Union-typed bindings read through tsc's control-flow narrowing:
// when the checker types this USE as a single arm, maybeNarrow
@@ -1799,6 +1819,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const table = lowerInternTableProperty(lowerer, expr);
if (table) return table;
}
// `zlib.Gzip.name`, `C.name` over a zlib class alias.
if (expr.name.text === "name" && !expr.questionDotToken) {
const mode = zlibClassModeOfExpr(lowerer, expr.expression);
if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc };
}
// Expando statics may still be absent even when JS inference sees
// the eventual assignment's scalar type. Preserve the checked value.
const staticField = lowerStaticFieldRead(lowerer, expr);
@@ -10017,6 +10042,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
lowerUnionTypeofTest(lowerer, expr, loc);
if (test) return test;
}
// `C === zlib.Gzip` between two zlib class references: the classes
// are compile-time values, so the answer folds.
if (
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
) {
const leftClass = zlibClassModeOfExpr(lowerer, expr.left);
const rightClass = leftClass === null ? null : zlibClassModeOfExpr(lowerer, expr.right);
if (leftClass !== null && rightClass !== null) {
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
return { kind: "boolLit", value: (leftClass === rightClass) !== negated, type: BOOL, loc };
}
}
const strictEquality =
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
@@ -12062,6 +12100,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
if (sockTest !== null) return sockTest;
const zlibMode = zlibClassModeOfExpr(lowerer, expr.right);
if (zlibMode !== null) return lowerZlibInstanceOf(lowerer, expr, zlibMode, loc);
if (lowerer.isStdlibGlobal(expr.right, "Set") || lowerer.isStdlibGlobal(expr.right, "Map")) {
const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set";
@@ -80,6 +80,7 @@ import {
createRequireProgramModuleDecl,
} from "./builtins/module-bindings.js";
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { registerZlibClassAlias } from "./builtins/compression.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import {
bindingContextualGenericFnNodeOf,
@@ -1700,6 +1701,7 @@ export function collectGlobals(
// resolution and value reads reuse the source's interned closure.
if (isConst && registerOverloadedCallableAlias(lowerer, decl)) continue;
if (isConst && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) continue;
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) continue;
// `const f = (x) => ...` at file scope of an npm-static JS file
// whose params are implicit-any: the implicit-monomorphization
// twin of the generic binding above — no global exists; calls and
@@ -147,6 +147,7 @@ import {
} from "./builtins/module-bindings.js";
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
import { registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { registerZlibClassAlias } from "./builtins/compression.js";
import { lowerEnumDeclaration } from "./lower-enums.js";
import {
abstractPropertyDeclOf,
@@ -4263,6 +4264,7 @@ export function lowerVarDecl(
// reads reuse its interned closure identity.
if (!isLet && registerOverloadedCallableAlias(lowerer, decl)) return null;
if (!isLet && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) return null;
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) return null;
// `const knownBy = (cmd) => ...` — an IMPLICIT-ANY function-value
// binding in npm-static JS (function-scope bindings register here, in
@@ -1802,6 +1802,11 @@ export class Lowerer {
* typeParamBindings — the checker has no `T` to substitute, so the
* binding rides the node-type accessor instead of mapType. */
implicitParamTypes: Map<ts.Symbol, ts.Type> | null = null;
/** Immutable bindings that name a zlib stream class (`const C =
* zlib.Gzip`), keyed by the binding's symbol: the class's
* node_zlib_mode. No runtime slot exists: new, typeof, .name and ===
* resolve at compile time, and instanceof checks the stream's mode. */
readonly zlibClassAliases = new Map<ts.Symbol, number>();
/** Whether some net.Socket in this program may get an encoding: a
* setEncoding call whose receiver is a socket or an untyped value.
* Computed once on first use (null until then). */
@@ -2771,6 +2771,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* receivers are borrowed. */
"zlib.streamNew": { argTypes: [F64, DYN], result: VOID },
"zlib.streamBytesWritten": { argTypes: [null], result: F64 },
/** `stream instanceof zlib.Gzip`: the borrowed stream and the class's
* node_zlib_mode; true when the stream is a zlib stream of that mode. */
"zlib.streamIs": { argTypes: [null, F64], result: BOOL },
"zlib.streamFlush": { argTypes: [null, DYN], result: VOID },
"zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID },
"zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID },
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5975-zlib-class-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/zlib-class-alias-escape.ts"
],
"diags": []
}
+3
View File
@@ -90,6 +90,9 @@ ScrPromise *scr_zlib_codec_promise_dyn(ScrDyn *data, double mode, ScrDyn *opts);
* with Node's ladder. The callbacks MOVE in and may be NULL. */
ScrStream *scr_zlib_stream_new(double mode, ScrDyn *opts);
double scr_zlib_stream_bytes_written(ScrStream *s);
/* `s instanceof zlib.Gzip` (and the other classes): true when s is a zlib
* stream built with that mode. s is borrowed. */
bool scr_zlib_stream_is(ScrStream *s, double mode);
void scr_zlib_stream_flush(ScrStream *s, ScrDyn *kind, ScrClosure *cb);
void scr_zlib_stream_params(ScrStream *s, ScrDyn *level, ScrDyn *strategy, ScrClosure *cb);
void scr_zlib_stream_close(ScrStream *s, ScrClosure *cb);
+5
View File
@@ -496,6 +496,11 @@ static ScrZlibStream *scr_zs_from_stream(ScrStream *s) {
return zs;
}
bool scr_zlib_stream_is(ScrStream *s, double mode) {
const ScrZlibStream *zs = scr_stream_native(s, &scr_zs_release_v);
return zs != NULL && zs->mode == (int)mode;
}
double scr_zlib_stream_bytes_written(ScrStream *s) {
ScrZlibStream *zs = scr_zs_from_stream(s);
return zs ? zs->bytes_written : 0;
+52
View File
@@ -0,0 +1,52 @@
// The zlib stream classes as values: const aliases construct, instanceof
// tests each class against stream, EventEmitter and plain operands, and
// typeof, .name and === answer as in Node.
import * as zlib from "node:zlib";
import { Gunzip, Unzip } from "node:zlib";
import { Readable, Transform } from "node:stream";
import { EventEmitter } from "node:events";
function makeLate() {
return new Late();
}
const Gz = zlib.Gzip;
const Gun = Gunzip;
const Again = Gz;
const Late = zlib.Inflate;
const gz = new Gz();
console.log("new alias", gz instanceof zlib.Gzip, gz instanceof Gz, gz instanceof Again);
console.log("other classes", gz instanceof Gun, gz instanceof Gunzip, gz instanceof zlib.Deflate);
console.log("factory", zlib.createGzip() instanceof Gz, zlib.createGunzip() instanceof Gz);
console.log("hoisted use", makeLate() instanceof zlib.Inflate, makeLate() instanceof Gun);
const asTransform: Transform = new zlib.DeflateRaw();
console.log("transform-typed", asTransform instanceof zlib.DeflateRaw, asTransform instanceof Gz);
const plain = new Transform({
transform(chunk, _encoding, callback) {
callback(null, chunk);
},
});
console.log("plain transform", plain instanceof Gz, plain instanceof Transform);
const readable = new Readable({ read() {} });
console.log("readable", readable instanceof zlib.Unzip);
const emitter: EventEmitter = zlib.createDeflate();
console.log("emitter-typed", emitter instanceof zlib.Deflate, emitter instanceof zlib.Inflate);
const bare = new EventEmitter();
console.log("bare emitter", bare instanceof zlib.Deflate);
const record = { kind: "gzip" };
console.log("object", record instanceof Gz);
console.log("typeof", typeof Gz, typeof zlib.Inflate, typeof Unzip);
console.log("name", Gz.name, Again.name, Gun.name, zlib.DeflateRaw.name, Unzip.name);
console.log("identity", Gz === zlib.Gzip, Again === Gz, Gz === Gun, Gz !== zlib.Gunzip);
const chunks: Buffer[] = [];
const roundTrip = new Again();
roundTrip.on("data", (chunk: Buffer) => chunks.push(chunk));
roundTrip.on("end", () => {
console.log("round trip", zlib.gunzipSync(Buffer.concat(chunks)).toString());
});
roundTrip.end("class values");
@@ -0,0 +1,11 @@
// A zlib class alias owns no runtime value: construction, instanceof,
// typeof and .name compile, other uses report at the use. instanceof on
// an untyped operand reports as well.
import * as zlib from "node:zlib";
const Gz = zlib.Gzip;
const classes = [Gz];
console.log(classes.length);
const parsed: unknown = JSON.parse("{}");
console.log(parsed instanceof Gz);
@@ -0,0 +1,13 @@
zlib-class-alias-escape.ts:7:18 - error SC1090: a zlib class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet
6 | const Gz = zlib.Gzip;
7 | const classes = [Gz];
| ^~
8 | console.log(classes.length);
zlib-class-alias-escape.ts:11:13 - error SC1090: 'instanceof zlib.Gzip' on an untyped or union value (give the operand a stream type first) is not supported yet
10 | const parsed: unknown = JSON.parse("{}");
11 | console.log(parsed instanceof Gz);
| ^~~~~~~~~~~~~~~~~~~~
12 |
+2
View File
@@ -1916,6 +1916,7 @@
"5971-buffer-chunks-to-string.ts": 2700,
"5972-net-set-encoding-listeners.ts": 2550,
"5974-typeof-module-exports.cjs": 2000,
"5975-zlib-class-values.ts": 2800,
"5990-worker-uncloneable.mjs": 3283,
"5991-worker-async-dispose.mjs": 5699,
"5992-worker-thread-name-lifetime.mjs": 2687,
@@ -4620,6 +4621,7 @@
"5971-buffer-chunks-to-string.ts": 3100,
"5972-net-set-encoding-listeners.ts": 3000,
"5974-typeof-module-exports.cjs": 2500,
"5975-zlib-class-values.ts": 4200,
"5990-worker-uncloneable.mjs": 22432,
"5991-worker-async-dispose.mjs": 25768,
"5992-worker-thread-name-lifetime.mjs": 28932,