mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Merge main
This commit is contained in:
@@ -208,6 +208,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/dns-lookup-errors.mdx</include>
|
||||
|
||||
### Socket data encoding
|
||||
|
||||
<include>../limitations/socket-data-encoding.mdx</include>
|
||||
|
||||
### UDP socket options
|
||||
|
||||
<include>../limitations/udp-socket-options.mdx</include>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
After `socket.setEncoding("utf8")`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares. A listener passed by reference, such as a named function whose parameter is typed `Buffer`, keeps its `Buffer` parameter, so a string chunk throws `TypeError: expected Uint8Array at $[0], got string`. Declare that parameter as `string` or `string | Buffer`, or pass an inline function.
|
||||
@@ -94,6 +94,11 @@ export interface GenericFnInfo {
|
||||
* checker types key an instantiation, exactly the generic machinery —
|
||||
* the untyped params ARE the type parameters (see implicitCallInstance). */
|
||||
implicitParams?: (ts.Symbol | null)[];
|
||||
/** Implicit-any monomorphization of destructured object parameters
|
||||
* (implicitPatternsOf): parallel to decl.parameters, the destructured
|
||||
* names where the slot binds the call site's object type. Computed on
|
||||
* first use. */
|
||||
implicitPatterns?: (ImplicitPattern | null)[];
|
||||
}
|
||||
|
||||
export interface GenericInstance {
|
||||
@@ -1125,6 +1130,114 @@ function broadPromiseParam(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** A destructured object parameter (`function f({ Base, name })`) whose
|
||||
* names bind the call site argument's property types. */
|
||||
export interface ImplicitPattern {
|
||||
elements: { property: string; symbol: ts.Symbol }[];
|
||||
}
|
||||
|
||||
/** The destructured object parameters of a JS function-like that bind like
|
||||
* implicit type parameters: every element a plain name (optionally renamed
|
||||
* from an identifier or string key) with no default, no rest element, and
|
||||
* no write in the body, under a broad inferred type. Parallel to
|
||||
* decl.parameters. */
|
||||
export function implicitPatternsOf(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
|
||||
): (ImplicitPattern | null)[] {
|
||||
return decl.parameters.map((param): ImplicitPattern | null => {
|
||||
if (!ts.isObjectBindingPattern(param.name) || !decl.body) return null;
|
||||
if (param.dotDotDotToken || param.questionToken || param.initializer) return null;
|
||||
const elements: ImplicitPattern["elements"] = [];
|
||||
for (const element of param.name.elements) {
|
||||
if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name))
|
||||
return null;
|
||||
const key = element.propertyName ?? element.name;
|
||||
if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(element.name);
|
||||
if (!symbol || paramWrittenInBody(lowerer, decl.body, symbol, element.name.text)) return null;
|
||||
// A JSDoc or contextual type names the properties: keep its ABI.
|
||||
if ((lowerer.typeOf(element.name).flags & ts.TypeFlags.Any) === 0) return null;
|
||||
elements.push({ property: key.text, symbol });
|
||||
}
|
||||
return elements.length > 0 ? { elements } : null;
|
||||
});
|
||||
}
|
||||
|
||||
/** The type a destructured parameter takes at one call site (the object's
|
||||
* own representation, a record or a checked value) and the checker type
|
||||
* each destructured name binds. An object literal argument types the names
|
||||
* from its property expressions, which see the enclosing instance's
|
||||
* bindings (`makeClass({ Base, name })` inside a specialized
|
||||
* `extend(Base, name)`); any other argument uses its checker type. Null
|
||||
* when the argument lacks a destructured property, or passes a class the
|
||||
* callee itself defines: each such class would instantiate the callee
|
||||
* again (a static `extend` calling `makeClass({ Base: this })`). */
|
||||
function implicitPatternBinding(
|
||||
lowerer: Lowerer,
|
||||
callee: ts.Node,
|
||||
pattern: ImplicitPattern,
|
||||
arg: ts.Expression,
|
||||
): { record: IrType; types: Map<ts.Symbol, ts.Type> } | null {
|
||||
let literal: ts.Expression = arg;
|
||||
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
|
||||
const types = new Map<ts.Symbol, ts.Type>();
|
||||
if (ts.isObjectLiteralExpression(literal)) {
|
||||
const values = new Map<string, ts.Expression>();
|
||||
/** Shorthand properties name the binding they read. */
|
||||
const shorthand = new Map<string, ts.Symbol>();
|
||||
for (const property of literal.properties) {
|
||||
if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) {
|
||||
values.set(property.name.text, property.name);
|
||||
const read = lowerer.checker.getShorthandAssignmentValueSymbol(property);
|
||||
if (read) shorthand.set(property.name.text, read);
|
||||
} else if (
|
||||
ts.isPropertyAssignment(property) &&
|
||||
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
|
||||
)
|
||||
values.set(property.name.text, property.initializer);
|
||||
else return null;
|
||||
}
|
||||
for (const element of pattern.elements) {
|
||||
const value = values.get(element.property);
|
||||
if (!value) return null;
|
||||
const read = shorthand.get(element.property);
|
||||
const type = (read && lowerer.implicitParamTypes?.get(read)) ?? lowerer.typeOf(value);
|
||||
types.set(element.symbol, lowerer.checker.getBaseTypeOfLiteralType(type));
|
||||
}
|
||||
// The object keeps the representation the call site lowers it to; the
|
||||
// destructured names read their static types back out of it.
|
||||
const lowered = tryLowerExpression(lowerer, literal);
|
||||
if (lowered?.type.kind !== "record" && lowered?.type.kind !== "dyn") return null;
|
||||
if (lowered.type.kind === "record") {
|
||||
for (const field of lowerer.shapes.get(lowered.type.shapeId)?.fields ?? [])
|
||||
if (field.type.kind === "classval" && classDefinedIn(lowerer, field.type.className, callee))
|
||||
return null;
|
||||
}
|
||||
return { record: lowered.type, types };
|
||||
}
|
||||
const argType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(arg));
|
||||
const record = lowerer.mapTypeOf(argType);
|
||||
if (record?.kind !== "record") return null;
|
||||
for (const element of pattern.elements) {
|
||||
const property = lowerer.checker.getPropertyOfType(argType, element.property);
|
||||
if (!property) return null;
|
||||
types.set(element.symbol, lowerer.checker.getTypeOfSymbol(property));
|
||||
}
|
||||
return { record, types };
|
||||
}
|
||||
|
||||
/** The class is declared inside `scope`. */
|
||||
function classDefinedIn(lowerer: Lowerer, className: string, scope: ts.Node): boolean {
|
||||
for (
|
||||
let n: ts.Node | undefined = lowerer.classes.get(className)?.decl ?? undefined;
|
||||
n;
|
||||
n = n.parent
|
||||
)
|
||||
if (n === scope) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The implicit-type-parameter slots of a JS function-like: parallel to
|
||||
* decl.parameters, the param SYMBOL where the slot is a bindable
|
||||
* implicit param (identifier-named, broad inferred/JSDoc type, not
|
||||
@@ -1169,6 +1282,10 @@ export function implicitAnyParamSymbolsOf(
|
||||
hasImplicitParams = true;
|
||||
return sym;
|
||||
});
|
||||
// Destructured object parameters monomorphize too (implicitPatternsOf);
|
||||
// their slots stay null here.
|
||||
if (!hasImplicitParams && isJsSourceFile(decl.getSourceFile()))
|
||||
hasImplicitParams = implicitPatternsOf(lowerer, decl).some((pattern) => pattern !== null);
|
||||
return hasImplicitParams ? out : null;
|
||||
}
|
||||
|
||||
@@ -1440,7 +1557,27 @@ export function implicitCallInstance(
|
||||
): GenericInstance {
|
||||
const shapes: ParamShape[] = [];
|
||||
const argTypes = new Map<ts.Symbol, ts.Type>();
|
||||
const patterns = (info.implicitPatterns ??= implicitPatternsOf(lowerer, info.decl));
|
||||
/** The destructured names' bound types, which the object's own
|
||||
* representation does not always carry. */
|
||||
const patternKey: string[] = [];
|
||||
info.decl.parameters.forEach((param, i) => {
|
||||
const pattern = patterns[i];
|
||||
const patternArg = call.arguments[i];
|
||||
if (pattern && patternArg && !ts.isSpreadElement(patternArg)) {
|
||||
// The argument's object type binds each destructured name, so a
|
||||
// class passed as `{ Base }` keeps its static class type.
|
||||
const bound = implicitPatternBinding(lowerer, info.decl, pattern, patternArg);
|
||||
if (bound) {
|
||||
for (const [symbol, type] of bound.types) {
|
||||
argTypes.set(symbol, type);
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
patternKey.push(mapped ? typeKey(mapped) : "?");
|
||||
}
|
||||
shapes.push({ type: bound.record, mode: "required" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const sym = info.implicitParams![i];
|
||||
if (!sym) {
|
||||
shapes.push(lowerer.paramShape(param));
|
||||
@@ -1578,7 +1715,7 @@ export function implicitCallInstance(
|
||||
}
|
||||
shapes.push({ type: bound, mode: "required" });
|
||||
});
|
||||
return internImplicitInstance(lowerer, call, info, shapes, argTypes);
|
||||
return internImplicitInstance(lowerer, call, info, shapes, argTypes, patternKey.join(","));
|
||||
}
|
||||
|
||||
/** The all-dyn DEFAULT instance — today's compiled body exactly: what a
|
||||
@@ -1611,8 +1748,9 @@ function internImplicitInstance(
|
||||
info: GenericFnInfo,
|
||||
shapes: ParamShape[],
|
||||
argTypes: Map<ts.Symbol, ts.Type>,
|
||||
patternKey = "",
|
||||
): GenericInstance {
|
||||
const key = shapes.map((s) => typeKey(s.type)).join(",");
|
||||
const key = shapes.map((s) => typeKey(s.type)).join(",") + (patternKey ? `|${patternKey}` : "");
|
||||
let inst = info.instances.get(key);
|
||||
if (inst?.implicitState === "failed")
|
||||
lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`);
|
||||
|
||||
@@ -2111,6 +2111,67 @@ function lowerNetServerMethodCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a setEncoding call can reach some net.Socket in this program:
|
||||
* `x.setEncoding(...)` or `x["setEncoding"](...)` where `x` is a socket
|
||||
* or an untyped value. Sockets that never get an encoding keep the Buffer
|
||||
* chunk path; with one, Node's 'data' chunks are strings. */
|
||||
function socketEncodingReachable(lowerer: Lowerer): boolean {
|
||||
if (lowerer.socketEncodingReachable !== null) return lowerer.socketEncodingReachable;
|
||||
let reachable = false;
|
||||
for (const file of lowerer.program.getSourceFiles()) {
|
||||
if (file.isDeclarationFile || !file.text.includes("setEncoding")) continue;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (reachable) return "skip";
|
||||
if (!ts.isCallExpression(node)) return undefined;
|
||||
const callee = node.expression;
|
||||
const receiver =
|
||||
ts.isPropertyAccessExpression(callee) && callee.name.text === "setEncoding"
|
||||
? callee.expression
|
||||
: ts.isElementAccessExpression(callee) &&
|
||||
ts.isStringLiteralLike(callee.argumentExpression) &&
|
||||
callee.argumentExpression.text === "setEncoding"
|
||||
? callee.expression
|
||||
: null;
|
||||
if (receiver === null) return undefined;
|
||||
const kind = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(receiver))?.kind;
|
||||
if (kind === undefined || kind === "netSocket" || kind === "dyn") reachable = true;
|
||||
return undefined;
|
||||
});
|
||||
if (reachable) break;
|
||||
}
|
||||
lowerer.socketEncodingReachable = reachable;
|
||||
return reachable;
|
||||
}
|
||||
|
||||
/** An inline 'data' listener whose chunk parameter is typed Buffer, for a
|
||||
* socket that may have an encoding set: the chunk binds as a checked
|
||||
* dynamic value, so the body sees what Node passes (a string after
|
||||
* setEncoding, a Buffer otherwise) whatever the annotation says. Null
|
||||
* for other listener shapes. */
|
||||
function lowerEncodedDataListener(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
let fn = node;
|
||||
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
||||
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return null;
|
||||
if (fn.parameters.length !== 1) return null;
|
||||
const param = fn.parameters[0]!;
|
||||
if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (symbol === undefined) return null;
|
||||
const previousBinding = lowerer.runtimeOptionalBindingTypes.get(symbol);
|
||||
const previousImplicit = lowerer.implicitParamTypes;
|
||||
const implicit = new Map(previousImplicit ?? []);
|
||||
implicit.set(symbol, lowerer.checker.getUnknownType());
|
||||
lowerer.runtimeOptionalBindingTypes.set(symbol, DYN);
|
||||
lowerer.implicitParamTypes = implicit;
|
||||
try {
|
||||
return lowerer.lowerExpr(fn);
|
||||
} finally {
|
||||
if (previousBinding === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol);
|
||||
else lowerer.runtimeOptionalBindingTypes.set(symbol, previousBinding);
|
||||
lowerer.implicitParamTypes = previousImplicit;
|
||||
}
|
||||
}
|
||||
|
||||
/** Method calls on net.Socket receivers: write/end/destroy/pipe and
|
||||
* on/once("data" | "end" | "close" | "error" | "connect"). Null for
|
||||
* other receivers. */
|
||||
@@ -2565,6 +2626,27 @@ function lowerNetSocketMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (param !== null && param.kind === "bytes" && socketEncodingReachable(lowerer)) {
|
||||
const encoded = lowerEncodedDataListener(lowerer, args[1]!);
|
||||
// A listener defined elsewhere keeps its Buffer parameter; it takes
|
||||
// the chunk through the checked boundary, so a string chunk fails
|
||||
// the check instead of arriving as a Buffer.
|
||||
const cb: IrExpr =
|
||||
encoded ??
|
||||
({
|
||||
kind: "dynCheck",
|
||||
value: lowerSocketDynListener(lowerer, args[1]!, "data listeners"),
|
||||
type: funcOf([DYN], VOID),
|
||||
loc,
|
||||
} as IrExpr);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "net.sockOnData",
|
||||
args: [receiver, cb, once],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const { cb } = lowerCallbackArg(
|
||||
lowerer,
|
||||
args[1]!,
|
||||
|
||||
@@ -341,6 +341,7 @@ import {
|
||||
} from "./call-signatures.js";
|
||||
import {
|
||||
implicitAnyParamSymbolsOf,
|
||||
implicitLocalFnNodeOf,
|
||||
type GenericFnInfo,
|
||||
type GenericInstance,
|
||||
implicitMonoFile,
|
||||
@@ -1525,6 +1526,28 @@ function panicSafe<T>(query: () => T, fallback: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
/** The function's class expressions lower once per implicit-any instance.
|
||||
* A function whose only implicit slots are destructured options and that
|
||||
* is bound to a variable the implicit-instance probe rejects runs as one
|
||||
* shared body, so its classes keep a single name. */
|
||||
function monomorphizedImplicit(
|
||||
lowerer: Lowerer,
|
||||
fn: ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration,
|
||||
): boolean {
|
||||
const slots = implicitAnyParamSymbolsOf(lowerer, fn);
|
||||
if (!slots) return false;
|
||||
if (slots.some((slot) => slot !== null)) return true;
|
||||
let binding: ts.Node | undefined = fn.parent;
|
||||
while (binding && ts.isParenthesizedExpression(binding)) binding = binding.parent;
|
||||
if (
|
||||
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
|
||||
binding &&
|
||||
ts.isVariableDeclaration(binding)
|
||||
)
|
||||
return implicitLocalFnNodeOf(lowerer, binding) !== null;
|
||||
return true;
|
||||
}
|
||||
|
||||
export class Lowerer {
|
||||
readonly frontendServices: FrontendServices | undefined;
|
||||
readonly checker: ts.TypeChecker;
|
||||
@@ -1769,6 +1792,10 @@ 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;
|
||||
/** 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). */
|
||||
socketEncodingReachable: boolean | null = null;
|
||||
/** IMPLICIT-ANY instances lowered EAGERLY at first demand (their return
|
||||
* types are inferred from the body — the call site needs them settled),
|
||||
* collected here for run()'s function list (the liftedFns discipline). */
|
||||
@@ -2787,7 +2814,7 @@ export class Lowerer {
|
||||
if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break;
|
||||
if (
|
||||
parent.typeParameters?.length ||
|
||||
(implicitMonoFile(parent.getSourceFile()) && implicitAnyParamSymbolsOf(this, parent))
|
||||
(implicitMonoFile(parent.getSourceFile()) && monomorphizedImplicit(this, parent))
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5972-net-set-encoding-listeners.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+11
-10
@@ -56,9 +56,10 @@ Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native
|
||||
artifacts for this host. Without --baseline, reports absolute times only. Node is
|
||||
timed in the same interleaved loop as the reference to beat (--no-node skips it).
|
||||
|
||||
--layouts=N (macOS/Linux) links every executable N times, shifting the program
|
||||
object by 0, 16, 32, ... bytes of padding, and spreads the timed runs evenly over
|
||||
those layouts, so verdicts cover code-placement luck instead of one placement.
|
||||
--layouts=N (macOS/Linux) links every executable N times, shifting the runtime
|
||||
and program code by 0, 16, 32, ... bytes of padding, and spreads the timed runs
|
||||
evenly over those layouts, so verdicts cover code-placement luck instead of one
|
||||
placement.
|
||||
Use it for small codegen changes (e.g. --layouts=4 --runs=16).
|
||||
|
||||
Workload names are listed in benchmarks/runtime/workloads.json; for example
|
||||
@@ -103,10 +104,11 @@ const work = mkdtempSync(
|
||||
);
|
||||
|
||||
/* ── code-layout perturbation ──────────────────────────────────────────── */
|
||||
// Layout k > 0 links a retained k*16-byte padding object directly before the
|
||||
// program object, through a SCRIPTC_LINKER wrapper that forwards to clang (the
|
||||
// default linker driver on macOS and Linux). Code after that point moves as if
|
||||
// earlier code had grown; any checkout's compiler can be perturbed this way.
|
||||
// Layout k > 0 links a retained k*16-byte padding object before every other
|
||||
// input, through a SCRIPTC_LINKER wrapper that forwards to clang (the default
|
||||
// linker driver on macOS and Linux). The runtime is linked before the program
|
||||
// object, so both move as if earlier code had grown; any checkout's compiler can
|
||||
// be perturbed this way.
|
||||
function layoutTooling() {
|
||||
if (layouts === 1) return null;
|
||||
const root = join(work, "layouts");
|
||||
@@ -137,12 +139,11 @@ const args = process.argv.slice(2);
|
||||
const pad = process.env.SCRIPTC_BENCH_LAYOUT_PAD;
|
||||
let out = args;
|
||||
if (pad && !args.some((a) => a.startsWith("-print-prog-name"))) {
|
||||
const at = args.findIndex((a) => basename(a).startsWith(".scriptc-native-program-object-"));
|
||||
if (at < 0) {
|
||||
if (!args.some((a) => basename(a).startsWith(".scriptc-native-program-object-"))) {
|
||||
process.stderr.write("bench layout wrapper: no program object on the link line\\n");
|
||||
process.exit(2);
|
||||
}
|
||||
out = ["-Wl,-u,${symbol}", ...args.slice(0, at), pad, ...args.slice(at)];
|
||||
out = ["-Wl,-u,${symbol}", pad, ...args];
|
||||
}
|
||||
const result = spawnSync("clang", out, { stdio: "inherit" });
|
||||
process.exit(result.status ?? 1);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// After socket.setEncoding("utf8"), every 'data' listener receives the
|
||||
// string Node passes, whether its chunk parameter is unannotated,
|
||||
// annotated Buffer, string or string | Buffer. A Buffer-only method on
|
||||
// the string throws Node's TypeError. A socket without an encoding in
|
||||
// the same program still receives Buffers.
|
||||
import * as net from "node:net";
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
socket.on("data", (chunk) => {
|
||||
console.log("server chunk:", typeof chunk, chunk.length, "bytes:", chunk[0]);
|
||||
socket.end("hello wörld");
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const { port } = server.address() as net.AddressInfo;
|
||||
const client = net.connect(port, "127.0.0.1", () => client.write("hi"));
|
||||
client.setEncoding("utf8");
|
||||
let text = "";
|
||||
client.on("data", (chunk) => {
|
||||
text += chunk;
|
||||
console.log("unannotated:", typeof chunk, chunk.length, chunk);
|
||||
console.log("unannotated upper:", chunk.toString().toUpperCase());
|
||||
});
|
||||
client.on("data", (chunk: Buffer) => {
|
||||
console.log("annotated Buffer:", typeof chunk, chunk);
|
||||
console.log("annotated concat:", "<" + chunk + ">");
|
||||
try {
|
||||
console.log(chunk.readUInt8(0));
|
||||
} catch (error) {
|
||||
console.log("readUInt8:", (error as Error).name, (error as Error).message);
|
||||
}
|
||||
});
|
||||
client.once("data", (chunk: string) => console.log("string:", chunk.split(" ")));
|
||||
client.on("data", (chunk: string | Buffer) =>
|
||||
console.log("union:", typeof chunk === "string" ? "string " + chunk : "buffer"),
|
||||
);
|
||||
client.on("close", () => {
|
||||
console.log("text:", text);
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// A package factory that receives its base class through a destructured
|
||||
// options object builds subclasses of program classes that construct and
|
||||
// dispatch like Node.
|
||||
import { Class, extend } from "class-options/classes.js";
|
||||
|
||||
class Animal extends Class("Animal") {}
|
||||
class Dog extends extend(Animal, "Dog") {}
|
||||
class Puppy extends extend(Dog, "Puppy") {}
|
||||
|
||||
const rex = new Dog("Rex");
|
||||
const ada = new Animal("Ada");
|
||||
const bit = new Puppy("Bit");
|
||||
console.log(rex instanceof Dog, rex instanceof Animal, ada instanceof Dog, bit instanceof Animal);
|
||||
console.log(rex.describe(), ada.describe(), bit.describe());
|
||||
console.log(rex.kind, rex.value, ada.kind, bit.label);
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// A class factory that takes its base and settings as one options object,
|
||||
// as Effect's makeClass does: `makeClass({ Base, name })` destructures them
|
||||
// and builds `class extends Base`.
|
||||
function makeClass({ Base, name, tag: label }) {
|
||||
return class extends Base {
|
||||
constructor(value) {
|
||||
super(value);
|
||||
this.kind = name;
|
||||
this.label = label;
|
||||
}
|
||||
describe() {
|
||||
return `${this.label}:${this.kind}:${this.value}`;
|
||||
}
|
||||
// Each class passes itself back as a base: a call site must not
|
||||
// specialize makeClass on the classes makeClass itself defines.
|
||||
static extend(name) {
|
||||
return makeClass({ Base: this, name, tag: label });
|
||||
}
|
||||
};
|
||||
}
|
||||
class Root {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
export function Class(name) {
|
||||
return makeClass({ Base: Root, name, tag: "root" });
|
||||
}
|
||||
export function extend(Base, name) {
|
||||
return makeClass({ Base, name, tag: "sub" });
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "class-options", "version": "1.0.0", "type": "module", "exports": { "./classes.js": "./classes.js" } }
|
||||
@@ -1850,6 +1850,9 @@
|
||||
"5886-inspect-dates.ts": 2559,
|
||||
"5887-inspect-typed-arrays.ts": 3448,
|
||||
"5888-inspect-sorted.ts": 2998,
|
||||
"5970-net-connect-host-name.ts": 2400,
|
||||
"5971-buffer-chunks-to-string.ts": 2700,
|
||||
"5972-net-set-encoding-listeners.ts": 2550,
|
||||
"600-closures-basic.ts": 2349,
|
||||
"601-closures-loops.ts": 1788,
|
||||
"602-closures-identity-recursion.ts": 2372,
|
||||
@@ -4483,6 +4486,9 @@
|
||||
"5886-inspect-dates.ts": 2736,
|
||||
"5887-inspect-typed-arrays.ts": 2997,
|
||||
"5888-inspect-sorted.ts": 2861,
|
||||
"5970-net-connect-host-name.ts": 2800,
|
||||
"5971-buffer-chunks-to-string.ts": 3100,
|
||||
"5972-net-set-encoding-listeners.ts": 3000,
|
||||
"600-closures-basic.ts": 2534,
|
||||
"601-closures-loops.ts": 2297,
|
||||
"602-closures-identity-recursion.ts": 2427,
|
||||
|
||||
@@ -618,6 +618,40 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
},
|
||||
);
|
||||
|
||||
test.each(["release", "dev"] as const)(
|
||||
"a factory that destructures its base class from an options object builds static subclasses (%s)",
|
||||
async (optimization) => {
|
||||
const entry = join(pilotRoot, "class-options-cli.ts");
|
||||
const outDir = join(cacheDir, `class-options-${optimization}-${sanitize ? "san" : "plain"}`);
|
||||
const result = await compile(entry, {
|
||||
backend: "llvm",
|
||||
npmStatic: ["class-options"],
|
||||
sanitize,
|
||||
optimization,
|
||||
outDir,
|
||||
outPath: join(outDir, "program"),
|
||||
emitIr: true,
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const [reference, native] = await Promise.all([
|
||||
runBinary(process.execPath, [entry]),
|
||||
runBinary(result.binaryPath, []),
|
||||
]);
|
||||
expect(reference.exitCode).toBe(0);
|
||||
expect(native.stdout).toEqual(reference.stdout);
|
||||
expect(comparableStderr(native.stderr)).toEqual(reference.stderr);
|
||||
expect(native.exitCode).toBe(reference.exitCode);
|
||||
// One makeClass instance per distinct base (Root, Animal, Dog) plus
|
||||
// the generic one a class's own `extend` passing itself back reuses.
|
||||
const ir = JSON.parse(readFileSync(result.irPath!, "utf8")) as {
|
||||
functions: { name: string }[];
|
||||
};
|
||||
const instances = ir.functions.filter((fn) => /^%[\w.]*makeClass%\d+$/.test(fn.name));
|
||||
expect(instances.length).toBeLessThanOrEqual(4);
|
||||
},
|
||||
);
|
||||
|
||||
test.for([
|
||||
["escape-string-regexp", "escape-cli.ts"],
|
||||
["slash", "slash-cli.ts"],
|
||||
|
||||
Reference in New Issue
Block a user