Compile TLSSocket.isSessionReused, keyed short-circuit assignment and Object.assign into class instances (#1460)

This commit is contained in:
Chris Tate
2026-10-19 23:10:10 -05:00
committed by GitHub
16 changed files with 386 additions and 0 deletions
+2
View File
@@ -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.
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6416-tls-session-reused.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6417-object-assign-class-target.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6418-nullish-assign-index-record.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/object-assign-class-copy.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/tls-session-timings.ts"
],
"diags": []
}
+33
View File
@@ -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(","));
+31
View File
@@ -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(","));
+21
View File
@@ -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");