mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Compile TLSSocket.isSessionReused, keyed short-circuit assignment and Object.assign into class instances (#1460)
This commit is contained in:
@@ -5238,6 +5238,8 @@ declare module "tls" {
|
||||
getProtocol(): string | null;
|
||||
/* OpenSSL's suite name, the IANA name and the suite's version */
|
||||
getCipher(): CipherNameAndProtocol;
|
||||
/* whether the handshake resumed a cached session */
|
||||
isSessionReused(): boolean;
|
||||
}
|
||||
export interface CipherNameAndProtocol {
|
||||
name: string;
|
||||
|
||||
@@ -214,6 +214,37 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
|
||||
!nullish && !and && element.kind === "f64"
|
||||
? arrayNumberRead(receiver, key, elementLoc)
|
||||
: arrayValueRead(lowerer, receiver, key, element, elementLoc);
|
||||
} else if (
|
||||
receiver.type.kind === "record" &&
|
||||
key.type.kind === "string" &&
|
||||
pureIndexShapeOf(lowerer, receiver.type.shapeId) !== null
|
||||
) {
|
||||
// `timings[phase] ??= v` over a pure index-signature record
|
||||
// (`Record<string, number>`): the keyed read answers the value or
|
||||
// undefined for a missing key, and the write inserts or replaces
|
||||
// the key, exactly the two halves `o[k]` and `o[k] = v` lower to.
|
||||
const shapeId = receiver.type.shapeId;
|
||||
const indexValue = pureIndexShapeOf(lowerer, shapeId)!;
|
||||
key = save(key, "%shortCircuitKey");
|
||||
const readType =
|
||||
indexValue.kind === "dyn" ? indexValue : lowerer.withUndefinedArmOf(indexValue);
|
||||
if (!readType) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
target,
|
||||
"short-circuit assignment through this indexed receiver",
|
||||
);
|
||||
}
|
||||
storage = indexValue;
|
||||
read = { kind: "recordKeyGet", obj: receiver, shapeId, key, type: readType, loc };
|
||||
write = (value) => ({
|
||||
kind: "recordKeySet",
|
||||
obj: receiver,
|
||||
shapeId,
|
||||
key,
|
||||
value: lowerer.coerceToExpected(value, indexValue),
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -265,3 +296,11 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
|
||||
}
|
||||
return finish({ kind: "nullish", left: read, right, type: read.type, loc });
|
||||
}
|
||||
|
||||
/** The value type of a pure index-signature record shape (no declared
|
||||
* fields, not a tuple), or null. */
|
||||
function pureIndexShapeOf(lowerer: Lowerer, shapeId: string): IrType | null {
|
||||
const shape = lowerer.shapes.get(shapeId);
|
||||
if (!shape || shape.tuple || shape.fields.length > 0 || !shape.indexValue) return null;
|
||||
return shape.indexValue;
|
||||
}
|
||||
|
||||
@@ -6769,6 +6769,99 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Whether `Object.assign(instance, record)` can copy every field of the
|
||||
* record shape into a field the class declares with the same type: a plain
|
||||
* record (no tuple, index signature or accessor slots) and a compiled
|
||||
* class whose chain has no builtin or runtime-provided storage and no
|
||||
* accessor for the written names. */
|
||||
function classAssignPlan(lowerer: Lowerer, className: string, srcShapeId: string): boolean {
|
||||
const info = lowerer.classes.get(className);
|
||||
const shape = lowerer.shapes.get(srcShapeId);
|
||||
if (!info || !shape || shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape))
|
||||
return false;
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
if (c.builtinError || c.builtinEmitter || c.builtinStream !== undefined || c.def.runtime)
|
||||
return false;
|
||||
}
|
||||
return shape.fields.every((f) => {
|
||||
const declared = info.fields.get(f.name);
|
||||
return (
|
||||
declared !== undefined &&
|
||||
(typeEquals(declared, f.type) || lowerer.widthLiftPlan(f.type, declared) !== null) &&
|
||||
!lowerer.findMethodOn(info, f.name) &&
|
||||
!lowerer.findMethodOn(info, `get:${f.name}`) &&
|
||||
!lowerer.findMethodOn(info, `set:${f.name}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Interned `%obj.assignClass.<n>(o, s)` — Object.assign's per-field
|
||||
* write of a record into a class instance (classAssignPlan's gate):
|
||||
* optional source fields write only when present, and the instance
|
||||
* returns. */
|
||||
function classAssignHelper(
|
||||
lowerer: Lowerer,
|
||||
className: string,
|
||||
srcShapeId: string,
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (!classAssignPlan(lowerer, className, srcShapeId)) return null;
|
||||
const key = `obj.assignClass:${className}:${srcShapeId}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const sShape = lowerer.shapes.get(srcShapeId)!;
|
||||
const oT: IrType = { kind: "object", className };
|
||||
const sT: IrType = { kind: "record", shapeId: srcShapeId };
|
||||
const oRef: IrExpr = { kind: "varRef", localId: "o.0", type: oT, loc };
|
||||
const sRef: IrExpr = { kind: "varRef", localId: "s.0", type: sT, loc };
|
||||
const body: IrStmt[] = [];
|
||||
const info = lowerer.classes.get(className)!;
|
||||
for (const f of sShape.fields) {
|
||||
const declared = info.fields.get(f.name)!;
|
||||
const read: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: srcShapeId,
|
||||
field: f.name,
|
||||
type: f.type,
|
||||
loc,
|
||||
};
|
||||
const lift = typeEquals(declared, f.type) ? null : lowerer.widthLiftPlan(f.type, declared);
|
||||
const set: IrStmt = {
|
||||
kind: "fieldSet",
|
||||
obj: oRef,
|
||||
className,
|
||||
field: f.name,
|
||||
value: lift === null ? read : lowerer.applyWidthLift(lift, read, declared, loc),
|
||||
loc,
|
||||
};
|
||||
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
|
||||
body.push(
|
||||
present.kind === "recordHas"
|
||||
? { kind: "if", cond: present, then: [set], else_: null, loc }
|
||||
: set,
|
||||
);
|
||||
}
|
||||
body.push({ kind: "return", value: oRef, loc });
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "o.0", name: "o", type: oT },
|
||||
{ localId: "s.0", name: "s", type: sT },
|
||||
],
|
||||
returnType: oT,
|
||||
locals: [
|
||||
{ id: "o.0", name: "o", type: oT, mutable: true },
|
||||
{ id: "s.0", name: "s", type: sT, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Interned `%obj.assign.<n>(t, s)` — Object.assign's per-field copy
|
||||
* over signature-free records (every source field lands on a same-named,
|
||||
* same-typed target field — the caller's gate): optional source fields
|
||||
@@ -7410,6 +7503,28 @@ function lowerObjectStaticCall(
|
||||
return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
|
||||
}
|
||||
}
|
||||
// A CLASS-INSTANCE target and a plain record source whose fields are
|
||||
// all fields the class declares with the same types
|
||||
// (`Object.assign(this, { retries: 3 })`): the per-field write into
|
||||
// the instance, returning it — the instance mutates in place.
|
||||
if (tProbe?.type.kind === "object" && sProbe?.type.kind === "record") {
|
||||
const helper = classAssignPlan(lowerer, tProbe.type.className, sProbe.type.shapeId);
|
||||
if (helper) {
|
||||
const loc = locOf(call);
|
||||
const target = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
const source = lowerer.lowerExpr(call.arguments.nodes[1]!);
|
||||
if (target.type.kind === "object" && source.type.kind === "record") {
|
||||
const name = classAssignHelper(
|
||||
lowerer,
|
||||
target.type.className,
|
||||
source.type.shapeId,
|
||||
loc,
|
||||
);
|
||||
if (name)
|
||||
return { kind: "call", callee: name, args: [target, source], type: target.type, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tProbe?.type.kind === "record" && sProbe?.type.kind === "record") {
|
||||
const tShape = lowerer.shapes.get(tProbe.type.shapeId);
|
||||
const sShape = lowerer.shapes.get(sProbe.type.shapeId);
|
||||
@@ -7500,6 +7615,25 @@ function lowerObjectStaticCall(
|
||||
const srcNode = spread ? argNode.expression : argNode;
|
||||
const rawSource = lowerer.lowerExpr(srcNode);
|
||||
fenceSymbolFieldCopy(lowerer, srcNode, rawSource.type, spread);
|
||||
// A class instance copied into a fresh object
|
||||
// (`Object.assign({}, instance)`): the result is a plain
|
||||
// object holding the instance's own fields, never an
|
||||
// instance, so a result the checker types as the class has
|
||||
// no representation (the checked cast would throw).
|
||||
if (
|
||||
rawSource.type.kind === "object" &&
|
||||
freshLiteralTarget &&
|
||||
!ts.isExpressionStatement(call.parent)
|
||||
) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT !== null && resultT.kind === "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
srcNode,
|
||||
`Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const src = lowerer.coerceToExpected(rawSource, DYN);
|
||||
if (src.type.kind !== "dyn") {
|
||||
ok = false;
|
||||
|
||||
@@ -2476,6 +2476,20 @@ function lowerNetSocketMethodCall(
|
||||
? value
|
||||
: lowerer.coerceInto(call, value, declared);
|
||||
}
|
||||
if (name === "isSessionReused" && args.length === 0) {
|
||||
// TLSSocket.isSessionReused(): the client never offers a cached
|
||||
// session (no session resumption, a documented TLS divergence), so
|
||||
// no handshake resumes one. A plain socket asserted to be a
|
||||
// TLSSocket answers false too, where Node has no such method.
|
||||
const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: receiver, loc }],
|
||||
result: boolLit(false, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "resetAndDestroy") {
|
||||
// An RST close; a connecting socket resets once it connects. Answers
|
||||
// the socket, Node's chaining.
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6416-tls-session-reused.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6417-object-assign-class-target.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6418-nullish-assign-index-record.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/object-assign-class-copy.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/tls-session-timings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// TLSSocket.isSessionReused() on both ends of a fresh TLS connection,
|
||||
// before and after the handshake: a first handshake never resumes a
|
||||
// session, so every answer is false.
|
||||
import * as tls from "node:tls";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const cert = readFileSync("tests/fixtures/server/certs/localhost.pem");
|
||||
const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem");
|
||||
const ca = readFileSync("tests/fixtures/server/certs/ca.pem");
|
||||
|
||||
let serverLine: string | null = null;
|
||||
let clientLine: string | null = null;
|
||||
function both(): void {
|
||||
if (serverLine === null || clientLine === null) return;
|
||||
console.log(serverLine);
|
||||
console.log(clientLine);
|
||||
}
|
||||
|
||||
const server = tls.createServer({ key, cert }, (socket) => {
|
||||
serverLine = `server reused ${socket.isSessionReused()}`;
|
||||
both();
|
||||
socket.end("hi");
|
||||
});
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = (server.address() as { port: number }).port;
|
||||
const client = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca }, () => {
|
||||
clientLine = `client reused ${client.isSessionReused()}`;
|
||||
both();
|
||||
});
|
||||
console.log("before handshake", client.isSessionReused());
|
||||
client.on("data", (d: Buffer) => console.log("data", d.toString()));
|
||||
client.on("close", () => server.close());
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// Object.assign with a class instance as the target and a plain record as
|
||||
// the source: the record's fields land on the instance's own fields (also
|
||||
// readonly ones, which only the checker guards), optional source fields
|
||||
// only when present, and the call answers the instance itself.
|
||||
class Settings {
|
||||
retries = 1;
|
||||
readonly name: string = "default";
|
||||
verbose = false;
|
||||
label?: string;
|
||||
describe(): string {
|
||||
return `${this.name}:${this.retries}:${this.verbose}:${this.label ?? "-"}`;
|
||||
}
|
||||
}
|
||||
|
||||
const s = new Settings();
|
||||
const same = Object.assign(s, { retries: 3, verbose: true });
|
||||
console.log(same === s, s.describe());
|
||||
|
||||
function patch(target: Settings, update: { label?: string }): Settings {
|
||||
return Object.assign(target, update);
|
||||
}
|
||||
console.log(patch(s, { label: "x" }).describe());
|
||||
console.log(patch(s, {}).describe());
|
||||
console.log(patch(s, { label: undefined }).describe(), Object.assign(s, { retries: 0 }).describe());
|
||||
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
const p = new Point(1, 2);
|
||||
Object.assign(p, { y: 5 });
|
||||
console.log(p.x, p.y, JSON.stringify(p), p instanceof Point);
|
||||
@@ -0,0 +1,29 @@
|
||||
// Short-circuit assignment through a string-keyed index-signature record
|
||||
// (`timings[phase] ??= value`): a missing key takes the right side, a
|
||||
// present one keeps its value, and `||=` and `&&=` test truthiness.
|
||||
class Telemetry {
|
||||
private readonly timings: Record<string, number> = {};
|
||||
private readonly labels: { [key: string]: string } = {};
|
||||
mark(phase: string, at: number): number {
|
||||
return (this.timings[phase] ??= at);
|
||||
}
|
||||
label(key: string, value: string): string {
|
||||
this.labels[key] ||= value;
|
||||
this.labels[key] &&= this.labels[key].toUpperCase();
|
||||
return this.labels[key];
|
||||
}
|
||||
dump(): string {
|
||||
return JSON.stringify(this.timings) + " " + JSON.stringify(this.labels);
|
||||
}
|
||||
}
|
||||
|
||||
const t = new Telemetry();
|
||||
console.log(t.mark("dns", 5), t.mark("tcp", 7), t.mark("dns", 9));
|
||||
console.log(t.label("a", "first"), t.label("a", "second"), t.label("", "empty"));
|
||||
console.log(t.dump());
|
||||
|
||||
const counts: Record<string, number> = { zero: 0 };
|
||||
const keys = ["zero", "one", "zero"];
|
||||
for (const k of keys) counts[k] ??= 10;
|
||||
for (const k of keys) counts[k] ||= 20;
|
||||
console.log(JSON.stringify(counts));
|
||||
@@ -0,0 +1,11 @@
|
||||
// Object.assign copying a class instance into a fresh object: the result
|
||||
// is a plain object holding the instance's own fields, never an instance,
|
||||
// so a result the checker types as the class is refused rather than
|
||||
// failing its checked cast at run time.
|
||||
class Config {
|
||||
b = 1;
|
||||
a = 2;
|
||||
}
|
||||
const config = new Config();
|
||||
const copy = Object.assign({}, config);
|
||||
console.log(JSON.stringify(copy), Object.keys(copy).join(","));
|
||||
@@ -0,0 +1,31 @@
|
||||
// Under @types/node: a connector-style TLS client reads
|
||||
// isSessionReused() through a TLSSocket assertion and keeps per-phase
|
||||
// timings in a string-keyed record with `??=`.
|
||||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const cert = readFileSync("tests/fixtures/server/certs/localhost.pem");
|
||||
const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem");
|
||||
const ca = readFileSync("tests/fixtures/server/certs/ca.pem");
|
||||
|
||||
const timings: Record<string, number> = {};
|
||||
function mark(phase: string, value: number): void {
|
||||
timings[phase] ??= value;
|
||||
}
|
||||
|
||||
const server = tls.createServer({ key, cert }, (socket) => socket.end("ok"));
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
const socket: net.Socket = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca }, () => {
|
||||
mark("secure", 1);
|
||||
mark("secure", 2);
|
||||
console.log("reused", (socket as tls.TLSSocket).isSessionReused());
|
||||
});
|
||||
socket.on("data", (d: Buffer) => console.log("data", d.toString()));
|
||||
socket.on("close", () => {
|
||||
mark("close", 3);
|
||||
console.log(JSON.stringify(timings));
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
object-assign-class-copy.ts:10:32 - error SC1090: Object.assign copying a 'Config' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need) is not supported yet
|
||||
|
||||
9 | const config = new Config();
|
||||
10 | const copy = Object.assign({}, config);
|
||||
| ^~~~~~
|
||||
11 | console.log(JSON.stringify(copy), Object.keys(copy).join(","));
|
||||
@@ -200,6 +200,27 @@ test("node-types: socket.address and resetAndDestroy under @types/node", async (
|
||||
expect(comparableStderr(native.stderr)).toBe(node.stderr);
|
||||
});
|
||||
|
||||
test("node-types: TLSSocket.isSessionReused and keyed ??= timings under @types/node", async () => {
|
||||
const outDir = outDirFor("tls-session-timings");
|
||||
const entry = join(nodeTypesDir, "tls-session-timings.ts");
|
||||
const result = await compile(entry, {
|
||||
outPath: join(
|
||||
outDir,
|
||||
process.platform === "win32" ? "tls-session-timings.exe" : "tls-session-timings",
|
||||
),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
|
||||
if (!result.ok) return;
|
||||
const [native, node] = await Promise.all([
|
||||
execFileAsync(result.binaryPath),
|
||||
execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
|
||||
]);
|
||||
expect(native.stdout).toBe(node.stdout);
|
||||
expect(comparableStderr(native.stderr)).toBe(node.stderr);
|
||||
});
|
||||
|
||||
test("node-types: net.SocketAddress and net.BlockList under @types/node", async () => {
|
||||
const outDir = outDirFor("net-blocklist");
|
||||
const entry = join(nodeTypesDir, "net-blocklist.ts");
|
||||
|
||||
Reference in New Issue
Block a user