From b4ef932b722010ebf6f55044fd173a8524922d49 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 09:52:17 -0500 Subject: [PATCH] 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. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../frontend/lowering/builtins/compression.ts | 150 +++++++++++++++++- .../src/frontend/lowering/lower-classes.ts | 6 +- .../src/frontend/lowering/lower-exprs.ts | 42 ++++- .../src/frontend/lowering/lower-modules.ts | 2 + .../src/frontend/lowering/lower-stmts.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 5 + .../compiler/src/ir/builtin-signatures.ts | 3 + .../corpus/5975-zlib-class-values.ts.json | 6 + .../zlib-class-alias-escape.ts.json | 6 + packages/runtime/src/scr_zlib.h | 3 + packages/runtime/src/scr_zlib_stream.c | 5 + tests/corpus/5975-zlib-class-values.ts | 52 ++++++ tests/diagnostics/zlib-class-alias-escape.ts | 11 ++ .../zlib-class-alias-escape.ts.txt | 13 ++ tests/harness/corpus-costs.json | 2 + 16 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5975-zlib-class-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/zlib-class-alias-escape.ts.json create mode 100644 tests/corpus/5975-zlib-class-values.ts create mode 100644 tests/diagnostics/zlib-class-alias-escape.ts create mode 100644 tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index f69c96e6e..02bf37192 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -848,6 +848,7 @@ export const LIB_FN_SYMS: Record = { "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", diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 9c47711c1..c9553c2f9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -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> = { 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, + }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 7a98096b7..b9bfc7e99 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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 — diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index c8874fe34..33d602228 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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"; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 81ded6a58..c5802b07e 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 2cc51e789..5d742ee46 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b4520d3be..2b5ae2e92 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 | 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(); /** 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). */ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index ca2749591..b34bcefdd 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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 }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5975-zlib-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5975-zlib-class-values.ts.json new file mode 100644 index 000000000..561bd1e92 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5975-zlib-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5975-zlib-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/zlib-class-alias-escape.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/zlib-class-alias-escape.ts.json new file mode 100644 index 000000000..cab5c0ba0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/zlib-class-alias-escape.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/zlib-class-alias-escape.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_zlib.h b/packages/runtime/src/scr_zlib.h index 3ac07d844..124e157ab 100644 --- a/packages/runtime/src/scr_zlib.h +++ b/packages/runtime/src/scr_zlib.h @@ -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); diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 55dcd6302..14d8747e8 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -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; diff --git a/tests/corpus/5975-zlib-class-values.ts b/tests/corpus/5975-zlib-class-values.ts new file mode 100644 index 000000000..f46d7d5d4 --- /dev/null +++ b/tests/corpus/5975-zlib-class-values.ts @@ -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"); diff --git a/tests/diagnostics/zlib-class-alias-escape.ts b/tests/diagnostics/zlib-class-alias-escape.ts new file mode 100644 index 000000000..8c8e17452 --- /dev/null +++ b/tests/diagnostics/zlib-class-alias-escape.ts @@ -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); diff --git a/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt b/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt new file mode 100644 index 000000000..ea9d76089 --- /dev/null +++ b/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt @@ -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 | \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 4acd0928d..d5f63bfbc 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -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,