Destructure narrowed and joined union sources, await promise-armed unions, support writeFileSync(fd) (#2466)

This commit is contained in:
Chris Tate
2026-10-28 08:57:38 -05:00
committed by GitHub
23 changed files with 563 additions and 55 deletions
+5
View File
@@ -42,6 +42,11 @@ declare module "node:fs" {
/* The bare-encoding spelling — the options record's encoding key alone. */
export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void;
export function writeFileSync(path: string | URL, data: string | Uint8Array): void;
/* The file-descriptor form: Node writes the whole data to an already
* open descriptor and leaves it open (closeSync owns its lifetime, and
* the write lands at the descriptor's current offset). */
export function writeFileSync(fd: number, data: string): void;
export function writeFileSync(fd: number, data: string, encoding: "utf8" | "utf-8"): void;
export function appendFileSync(path: string | URL, data: string | Uint8Array): void;
export function existsSync(path: string | URL): boolean;
export function mkdirSync(path: string | URL): void;
@@ -557,6 +557,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"fs.readFileSyncBuf": "scr_fs_read_file_bytes",
"fs.readFileSyncBytes": "scr_fs_read_file_bytes",
"fs.writeFileSyncBytes": "scr_fs_write_file_bytes",
"fs.writeFileSyncFd": "scr_fs_write_file_fd",
"fs.appendFileSyncBytes": "scr_fs_append_file_bytes",
"fs.readFdSyncBytes": "scr_fs_read_fd_bytes",
// Stats snapshots (scr_lib.c): statSync/lstatSync throw like the other
@@ -997,6 +997,42 @@ export function lowerFsWriteBytesCall(
return null;
}
// The writeFileSync(fd, data[, "utf8"]) form — Node accepts a file
// descriptor where it accepts a path (write to a descriptor openSync
// returned, the descriptor staying open: closeSync owns its lifetime).
// Routed by the ARGUMENT's static type, like readFileSync(fd).
export function lowerFsWriteDescriptorCall(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const supported = 'only string data is supported: writeFileSync(fd, data, "utf8")';
const dataNode = expr.arguments.nodes[1]!;
const dataIr = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
if (dataIr?.kind !== "string") {
lowerer.noLowering(
`writeFileSync(fd) of '${dataIr ? lowerer.fmt(dataIr) : lowerer.checker.typeToString(lowerer.typeOf(dataNode))}' data`,
dataNode,
supported,
);
}
if (expr.arguments.nodes.length > 3) {
lowerer.noLowering(
`writeFileSync(fd) with ${expr.arguments.nodes.length} arguments`,
expr,
supported,
);
}
const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64);
return {
kind: "libCall",
fn: "fs.writeFileSyncFd",
args: [fd, lowerer.lowerExprExpecting(dataNode, STRING)],
type: VOID,
loc,
};
}
export function lowerFsWriteOptionsCall(
lowerer: Lowerer,
expr: ts.CallExpression,
@@ -14,6 +14,7 @@ import {
lowerFsReadSyncCall,
lowerFsWriteSyncCall,
lowerFsReadDescriptorCall,
lowerFsWriteDescriptorCall,
lowerFsEncodedRead,
lowerFsPromisesEncodedRead,
lowerFsMkdirSyncCall,
@@ -331,6 +332,17 @@ export function lowerBuiltinModuleCall(
const lowered = lowerFsWriteBytesCall(lowerer, expr, bi, loc);
if (lowered) return lowered;
}
// writeFileSync(fd, data): Node accepts an open descriptor where it
// accepts a path. Routed by the FIRST ARGUMENT's static type, like
// readFileSync(fd) (before the path forms complete the data type).
if (
bi.module === "fs" &&
bi.member === "writeFileSync" &&
expr.arguments.nodes.length >= 2 &&
lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "f64"
) {
return lowerFsWriteDescriptorCall(lowerer, expr, loc);
}
if (
bi.module === "fs" &&
(bi.member === "writeFileSync" || bi.member === "appendFileSync") &&
@@ -3592,11 +3592,12 @@ export function lowerUnionProperty(
* message. The caller (the property/element dispatch) maybeNarrows. */
export function lowerUnionKeyedRead(
lowerer: Lowerer,
expr: ts.Expression,
expr: ts.Node,
unionId: string,
value: IrExpr,
key: IrExpr,
literalField: string | null,
missingFieldAsUndefined = false,
): IrExpr | null {
const def = lowerer.unions.get(unionId);
if (!def) return null;
@@ -3646,6 +3647,14 @@ export function lowerUnionKeyedRead(
}
if (!shape.indexValue) {
if (literalField === null && shape.fields.length > 0) continue;
// A record arm that does not declare the key at all answers
// UNDEFINED in JS. Destructuring asks for that answer (its own
// desugar reads a missing property as undefined, Node-exact); a
// dotted read keeps refusing until narrowing proves the arm.
if (literalField !== null && missingFieldAsUndefined) {
push(UNDEFINED_T);
continue;
}
return null;
}
if (!pushAnswer(shape.indexValue)) return null;
@@ -3690,6 +3699,12 @@ export function lowerUnionKeyedRead(
if (!surfaces(declared)) return null;
continue;
}
if (literalField !== null && !shape.indexValue && missingFieldAsUndefined) {
// The missing-field answer above must surface as the join's
// undefined arm (the backend's keyed-read miss path stores it).
if (!(type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0)) return null;
continue;
}
const ovfShape: IrRecordShape =
literalField !== null && shape.indexValue ? { ...shape, fields: [] } : shape;
if (!recordKeyResultOk(lowerer, ovfShape, type)) return null;
@@ -27,7 +27,11 @@ import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands
import { staticCondition } from "./static-typeof.js";
import { constituentTypes } from "../ts7/checker.js";
import type { Lowerer } from "./lowerer.js";
import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js";
import {
lowerMaybePromiseAwait,
maybePromiseUnionOf,
promiseArmedUnionOf,
} from "./maybe-promise.js";
import { checkedClassUnionAssertion } from "./class-assertions.js";
import {
BIGINT_T,
@@ -676,9 +680,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
loc,
};
}
if (inner.kind === "union" || inner.kind === "dyn") {
// A union inner would need an arm-wise re-tag into the result
if (inner.kind === "union") {
// A UNION inner (`Promise<A | B> | undefined`): the awaited
// result is the checker's own `Awaited` of the operand, which
// the maybe-promise lowering already produces — it awaits the
// narrowed promise arm and coerces its settled union, and the
// unit arms keep JS's one-microtask hop. Only the promise-or-
// unit shape reaches here, so the maybe-promise union is the
// same union with its only data arms being units.
const armed = promiseArmedUnionOf(lowerer, value.type);
if (armed) return lowerMaybePromiseAwait(lowerer, expr, value, armed, loc);
lowerer.unsupported(
"SC1090",
expr,
`awaiting '${lowerer.fmt(value.type)}' (the promise's inner type has no combined result union — await the promise arm after narrowing instead)`,
);
}
if (inner.kind === "dyn") {
lowerer.unsupported(
"SC1090",
expr,
@@ -10,8 +10,12 @@ import {
lowerIndexedUnionNumberRead,
lowerIndexedUnionRead,
} from "./indexed-unions.js";
import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
import {
declaredMemberOf,
isPromisePrototypeRead,
lowerUnionKeyedRead,
} from "./expressions/property-access.js";
import { hostedStorageView, hostsAll } from "./expressions/hosted-narrowing.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
import { lowerForAwaitDynStream, nodeStreamInterfaceSide } from "./dyn-streams.js";
import { lowerForAwaitValueStream } from "./builtins/hash-stream.js";
@@ -3538,6 +3542,34 @@ export function lowerBindingPattern(
.get(srcType.unionId)
?.arms.every((arm) => arm.kind === "record" || arm.kind === "object")
) {
const arms = lowerer.unions.get(srcType.unionId)!.arms;
if (!unionObjectPatternFits(lowerer, pattern, arms)) {
// NARROW FIRST: the checker may have refined the union at this
// very site (`if (u.kind === "a") { const { x } = u }`, an `in`
// guard) while the IR value keeps the stored, wider union. Bind
// the narrowed view once and destructure that.
const view = narrowedUnionPatternSource(
lowerer,
pattern,
present,
srcType.unionId,
locOf(pattern),
);
if (view !== null) {
const tmp = lowerer.declareHiddenLocal("%destrN", view.type);
out.push({ kind: "varDecl", localId: tmp.id, init: view, loc: locOf(pattern) });
lowerer.lowerBindingPattern(
pattern,
() => ({ kind: "varRef", localId: tmp.id, type: view.type, loc: locOf(pattern) }),
view.type,
isLet,
out,
dynSpell,
allowDynObject,
);
return;
}
}
lowerUnionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
return;
}
@@ -3950,11 +3982,235 @@ function lowerUrlObjectPattern(
}
}
/** The fresh record for `{ a, ...rest }` over a UNION source: per arm,
* the unconsumed fields copied out at destructure time into a fresh
* record of the checker's arm-wise rest type, selected by the runtime
* tag. Null-safe answer: an arm whose remaining fields do not spell an
* arm of the checker's rest type keeps the named fence. */
function unionObjectRestValue(
lowerer: Lowerer,
el: ts.BindingElement,
srcRef: () => IrExpr,
srcType: IrType & { kind: "union" },
arms: readonly IrType[],
): IrExpr {
const loc = locOf(el);
const restT = patternBindingType(lowerer, el.name!);
const fence: (why: string) => never = (why) => lowerer.unsupported("SC1031", el, why);
if (restT === null) {
fence(`rest bindings over union sources (${NARROW_FIRST})`);
}
// The consumed set is the pattern's literal keys: a computed sibling
// would make it a runtime fact.
const consumed = new Set<string>();
const pattern = el.parent;
if (ts.isObjectBindingPattern(pattern)) {
for (const sib of pattern.elements.nodes) {
if (sib === el || sib.dotDotDotToken) continue;
const key = sib.propertyName ?? sib.name;
if (key && ts.isComputedPropertyName(key)) {
fence("rest bindings beside computed keys (the consumed set is a runtime fact)");
}
if (key && (ts.isIdentifier(key) || ts.isStringLiteral(key) || ts.isNumericLiteral(key))) {
consumed.add(key.text);
}
}
}
const pieces: { tag: number; value: IrExpr }[] = [];
for (let tag = 0; tag < arms.length; tag++) {
const arm = arms[tag]!;
if (arm.kind !== "record") {
fence(`rest bindings over union sources (${NARROW_FIRST})`);
}
const shape = lowerer.shapes.get(arm.shapeId);
if (!shape || shape.indexValue) {
fence(
"rest bindings over index-signature shapes (the undeclared entries would need overflow packing)",
);
}
const rem = shape.fields.filter((f) => !consumed.has(f.name));
if (rem.some((f) => f.name.startsWith("%"))) {
fence("rest bindings over accessor shapes (the entries are closures, not data)");
}
const armRef: IrExpr = {
kind: "unionNarrow",
unionId: srcType.unionId,
tag,
value: srcRef(),
type: arm,
loc,
};
const packed: IrExpr = lowerer.keyOrder.copy(
{
kind: "recordLit",
fields: rem.map((f) => ({
name: f.name,
value: lowerer.presenceKeepingCopy(
armRef,
arm.shapeId,
f.name,
{
kind: "recordGet",
obj: armRef,
shapeId: arm.shapeId,
field: f.name,
type: f.type,
loc,
},
loc,
),
})),
type: {
kind: "record",
shapeId: lowerer.shapes.intern(rem.map((f) => ({ name: f.name, type: f.type }))),
},
loc,
},
arm.shapeId,
);
const coerced = lowerer.coerceInto(el, packed, restT);
if (!typeEquals(coerced.type, restT)) {
fence(
`rest bindings over union sources (the remaining fields of an arm do not spell an arm of '${lowerer.fmt(restT)}')`,
);
}
pieces.push({ tag, value: coerced });
}
let result: IrExpr | null = null;
for (let i = pieces.length - 1; i >= 0; i--) {
const piece = pieces[i]!;
result =
result === null
? piece.value
: {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: srcType.unionId,
tag: piece.tag,
negated: false,
value: srcRef(),
type: BOOL,
loc,
},
then: piece.value,
else_: result,
type: restT,
loc,
};
}
if (result === null) {
fence(`rest bindings over union sources (${NARROW_FIRST})`);
}
return result;
}
/** The one type every arm declares for `propName`, or null when an arm
* does not declare it (or types it differently, or not at all). */
function unionPatternCommonType(
lowerer: Lowerer,
arms: readonly IrType[],
propName: string,
): IrType | null {
let common: IrType | null = null;
for (const arm of arms) {
let ft: IrType | undefined;
if (arm.kind === "record") {
ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type;
} else if (arm.kind === "object") {
ft = lowerer.classes.get(arm.className)?.fields.get(propName);
}
if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) {
return null;
}
common = ft;
}
return common;
}
/** Whether the unionDisc fast path lowers this pattern over these arms
* unchanged: no rest element, and every element's key folds to one name
* every arm declares with one type. */
function unionObjectPatternFits(
lowerer: Lowerer,
pattern: ts.ObjectBindingPattern,
arms: readonly IrType[],
): boolean {
for (const el of pattern.elements.nodes) {
if (el.name === undefined) continue;
if (el.dotDotDotToken) return false;
const prop = el.propertyName ?? el.name;
const propName =
ts.isIdentifier(prop) ||
ts.isComputedPropertyName(prop) ||
ts.isStringLiteralLike(prop) ||
ts.isNumericLiteral(prop)
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
: null;
if (propName === null) return false;
if (unionPatternCommonType(lowerer, arms, propName) === null) return false;
}
return true;
}
/** The CHECKER-NARROWED view of a destructuring source: `if (u.kind ===
* "a") { const { x } = u }` or an `in` guard refines the type at the
* pattern's own site, while the IR value keeps the stored (wider) union
* and the un-narrowed lowering. Bind the value to the narrowed sub-union
* — a trusted arm extraction for one arm, a re-tag with the excluded
* arms as traps for a sub-union — so the pattern reads the arms the
* checker proved. Null when the checker did not narrow, when the
* refinement is an intersection of stored records (hostedStorageView
* keeps that layout), or when nothing was excluded. */
function narrowedUnionPatternSource(
lowerer: Lowerer,
pattern: ts.ObjectBindingPattern,
value: IrExpr,
fromId: string,
loc: SrcLoc,
): IrExpr | null {
const from = lowerer.unions.get(fromId);
if (!from) return null;
const narrowed = lowerer.irTypeOf(pattern);
if (narrowed.kind === "void" || isUnitType(narrowed)) return null;
const siteArms =
narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed];
if (!siteArms || siteArms.length === 0) return null;
const allowed = new Set<number>();
for (const arm of siteArms) {
const tag = lowerer.armTag(fromId, arm);
// Not a stored arm: a checker intersection that refines the stored
// records, which keeps the value's layout.
if (tag < 0) return hostedStorageView(lowerer, value, narrowed, loc);
allowed.add(tag);
}
if (allowed.size === 0 || allowed.size === from.arms.length) return null;
if (allowed.size === 1) {
const tag = [...allowed][0]!;
// The checker proved the tag; the extraction traps on a mismatch
// instead of reading another arm.
return { kind: "unionNarrow", unionId: fromId, tag, value, type: from.arms[tag]!, loc };
}
const arms = from.arms.filter((_, tag) => allowed.has(tag));
const subId = lowerer.unions.transform(from, arms);
const helper = lowerer.narrowedRetagHelper(pattern, fromId, subId, loc);
if (!helper) return null;
return {
kind: "call",
callee: helper,
args: [value],
type: { kind: "union", unionId: subId },
loc,
};
}
/** An object pattern over a union of records and class instances
* (`function f({ kind, path }: Add | Remove)`): each element reads a data
* field every arm declares with one type, through the runtime tag, as
* `value.field` does. Rest elements and fields the arms type differently
* stay fenced. */
* field through the runtime tag, as `value.field` does — the shared
* `unionDisc` when every arm declares it with one type, else the
* `unionKeyGet` join (an arm that does not declare the key answers
* undefined, as JS reads it). A rest element packs each arm's unconsumed
* fields into a fresh record. Class arms still need same-typed fields. */
function lowerUnionObjectPattern(
lowerer: Lowerer,
pattern: ts.ObjectBindingPattern,
@@ -3967,7 +4223,17 @@ function lowerUnionObjectPattern(
for (const el of pattern.elements.nodes) {
if (el.name === undefined) continue;
if (el.dotDotDotToken) {
lowerer.unsupported("SC1031", el, `rest bindings over union sources (${NARROW_FIRST})`);
// `{ a, ...rest }` over a union: each arm packs its own unconsumed
// fields into a FRESH record (JS's CopyDataProperties makes a fresh
// object too), and the checker's rest type is the union of those
// per-arm packs. The tag decides which pack runs.
lowerer.bindPatternTarget(
el.name,
unionObjectRestValue(lowerer, el, srcRef, srcType, arms),
isLet,
out,
);
continue;
}
const prop = el.propertyName ?? el.name;
const propName =
@@ -3984,35 +4250,43 @@ function lowerUnionObjectPattern(
"destructuring with computed keys that do not fold to one property name",
);
}
let common: IrType | null = null;
for (const arm of arms) {
let ft: IrType | undefined;
if (arm.kind === "record") {
ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type;
} else if (arm.kind === "object") {
ft = lowerer.classes.get(arm.className)?.fields.get(propName);
}
if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) {
common = null;
break;
}
common = ft;
}
if (common === null) {
lowerer.unsupported(
"SC1031",
const common = unionPatternCommonType(lowerer, arms, propName);
let value: IrExpr;
if (common !== null) {
value = {
kind: "unionDisc",
unionId: srcType.unionId,
field: propName,
value: srcRef(),
type: common,
loc: locOf(el),
};
} else {
// The unionDisc generalization (`unionKeyGet`): each arm answers
// its own declared type and an arm that does not declare the
// key answers undefined — exactly JS's desugar, which reads a
// missing property as undefined.
const loc = locOf(el);
const joined = lowerUnionKeyedRead(
lowerer,
el,
`destructuring '${propName}' from a union whose arms type it differently (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`,
srcType.unionId,
srcRef(),
{ kind: "strLit", value: propName, type: STRING, loc },
propName,
true,
);
if (joined === null) {
lowerer.unsupported(
"SC1031",
el,
`destructuring '${propName}' from a union whose arms answer it with types that have no combined representation (every arm's '${
propName
}' must join into one union — ${NARROW_FIRST})`,
);
}
value = joined;
}
let value: IrExpr = {
kind: "unionDisc",
unionId: srcType.unionId,
field: propName,
value: srcRef(),
type: common,
loc: locOf(el),
};
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
lowerer.bindPatternTarget(el.name, value, isLet, out);
}
@@ -2197,7 +2197,9 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
rename: ["fs.checkPath"],
readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"],
// The bytes-data form and the { mode } options form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
// The bytes-data form, the { mode } options form, and the
// file-descriptor data form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync", "fs.writeFileSyncFd"],
appendFileSync: ["fs.appendFileSyncBytes"],
// The fs stream members (bytesRead/bytesWritten, pending, path, close).
createReadStream: [
@@ -920,6 +920,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"emitter.setDefaultMaxChk",
"fs.readFileSyncBytes",
"fs.writeFileSyncBytes",
"fs.writeFileSyncFd",
"fs.appendFileSyncBytes",
"zlib.inflateSync",
"zlib.inflateRawSync",
@@ -5289,6 +5289,11 @@ function buildLibFnSignatures() {
"fs.mkdtempSync": { argTypes: [STRING], result: STRING },
"fs.accessSync": { argTypes: [STRING, F64], result: VOID },
"fs.readFdSync": { argTypes: [F64, STRING], result: STRING },
/** The writeFileSync(fd, data) form: a write(2) loop over the whole
* string at the descriptor's current offset, leaving the descriptor
* open (closeSync owns it). Errors carry the fd operation's errno
* shape ("EBADF: bad file descriptor, write"). */
"fs.writeFileSyncFd": { argTypes: [F64, STRING], result: VOID },
"fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 },
/** Validated isatty(3) probe. Stream property lowering maps false to
* undefined; tty.isatty exposes the boolean result directly. */
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/fs-write-file-sync-fd.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/union-await-promise-arm.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/union-destructuring-join-narrow.ts"
],
"diags": []
}
+19
View File
@@ -4242,6 +4242,25 @@ double scr_fs_write_str_sync(double fd, ScrStr *data, double position,
return wrote;
}
/* writeFileSync(fd, data): Node writes the WHOLE data to the descriptor
* and leaves it open (closeSync owns its lifetime), at the descriptor's
* current offset — libuv's uv_fs_write loop, so a pipe's short writes
* resume until the data is gone. Errors keep the fd operation's errno
* shape ("EBADF: bad file descriptor, write"). */
void scr_fs_write_file_fd(double fd, ScrStr *data) {
char *owned;
const char *bytes = scr_str_utf8(data, &owned);
size_t at = 0;
while (at < data->len) {
double wrote = scr_fs_write_bytes(fd, bytes + at, data->len - at, -1);
/* A throw leaves 0 (or the short count a partial write reported);
* a zero-length write on a nonempty slice would spin, so stop. */
if (wrote <= 0) break;
at += (size_t)wrote;
}
scr_str_utf8_end(owned);
}
/* fs.readSync(fd, buffer, offset, length[, position]) — the buffer form.
* Node validates offset/length against the buffer before reading and
* throws ERR_OUT_OF_RANGE; here the checks clamp to the same contract and
+4
View File
@@ -289,6 +289,10 @@ ScrStr *scr_fs_read_file(ScrStr *path);
ScrStr *scr_fs_realpath(ScrStr *path);
ScrStr *scr_fs_realpath_promise(ScrStr *path);
void scr_fs_write_file(ScrStr *path, ScrStr *data);
/* writeFileSync(fd, data): a write(2) loop over the whole string at the
* descriptor's current offset; the descriptor stays open (closeSync owns
* it). Errors carry the fd operation's errno shape. */
void scr_fs_write_file_fd(double fd, ScrStr *data);
void scr_fs_append_file(ScrStr *path, ScrStr *data);
bool scr_fs_exists(ScrStr *path);
void scr_fs_mkdir(ScrStr *path);
+14
View File
@@ -0,0 +1,14 @@
// writeFileSync(fd, data): Node accepts an open file descriptor where it
// accepts a path, writes the whole data from the descriptor's current
// offset, and leaves the descriptor open.
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const scratch = path.join(os.tmpdir(), `scriptc-write-fd-${process.pid}.txt`);
const fd = fs.openSync(scratch, "w");
fs.writeFileSync(fd, "hello ");
fs.writeFileSync(fd, "world");
fs.closeSync(fd);
console.log(fs.readFileSync(scratch, "utf8"));
fs.rmSync(scratch);
+24
View File
@@ -0,0 +1,24 @@
// Awaiting a union whose promise arm settles a union
// (`Promise<A | B> | undefined`): the promise arm is awaited, and a
// non-promise arm answers its own value after one microtask turn.
function maybe(flag: boolean): Promise<string | null> | undefined {
if (!flag) return undefined;
return Promise.resolve(flag ? "value" : null);
}
async function main(): Promise<void> {
const absent = await maybe(false);
console.log(String(absent));
const settled = await maybe(true);
console.log(String(settled));
const nil = await maybe(true);
console.log(nil === null);
// Awaiting a non-promise arm takes exactly one microtask turn.
const order: string[] = [];
Promise.resolve().then(() => order.push("microtask"));
const hopped = await maybe(false);
order.push(`after:${String(hopped)}`);
console.log(order.join(","));
}
main();
@@ -0,0 +1,73 @@
// Object patterns over a union whose arms type a field differently (or do
// not declare it), narrowed sources, and rest elements over a union.
interface Wide {
kind: "wide";
value: number;
extra?: string;
}
interface Narrow {
kind: "narrow";
value: string;
extra?: undefined;
}
type Item = Wide | Narrow;
// The joined read: `number | string`, `string | undefined`.
function read(item: Item): void {
const { value, extra } = item;
console.log(item.kind, String(value), String(extra));
}
read({ kind: "wide", value: 7, extra: "e" });
read({ kind: "narrow", value: "v" });
interface Delta {
type: "delta";
partial: { text: string };
other: number;
}
interface Final {
type: "final";
partial: { text: number };
tail: boolean;
}
interface Bare {
type: "bare";
other: number;
}
type Wire = Delta | Final | Bare;
// A rest element over a union packs each arm's own remaining fields.
function rest(event: Delta | Final): string {
const { partial: _partial, ...kept } = event;
return JSON.stringify(kept);
}
console.log(rest({ type: "delta", partial: { text: "t" }, other: 2 }));
console.log(rest({ type: "final", partial: { text: 3 }, tail: true }));
// A discriminant test and an `in` guard narrow the source; the pattern
// reads the arms the checker proved.
function narrowed(event: Wire): string {
if (event.type === "delta") {
const { partial, other } = event;
return `tag:${partial.text}:${other}`;
}
if ("partial" in event) {
const { partial } = event;
return `in:${String(partial.text)}`;
}
return "none";
}
console.log(narrowed({ type: "delta", partial: { text: "t" }, other: 2 }));
console.log(narrowed({ type: "final", partial: { text: 3 }, tail: true }));
console.log(narrowed({ type: "bare", other: 4 }));
// An `in` guard alone narrows to a SUB-union: both remaining arms keep
// their own field types.
function inOnly(w: Wire): string {
if (!("partial" in w)) return "none";
const { partial } = w;
return String(partial.text);
}
console.log(inOnly({ type: "delta", partial: { text: "t" }, other: 2 }));
console.log(inOnly({ type: "final", partial: { text: 3 }, tail: true }));
console.log(inOnly({ type: "bare", other: 4 }));
+1 -1
View File
@@ -42,7 +42,7 @@ interface E2 {
label: string;
}
function fromUnion(u: E1 | E2): string {
const { b } = u; // union source whose arms type the field differently: narrow first
const { b } = u; // arms type the field differently: the joined read compiles
return String(b);
}
console.log(fromUnion({ kind: "one", b: "s", count: 1 }));
@@ -19,22 +19,6 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a
| ^~~~~~
32 | console.log(onlyIn);
destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a union whose arms type it differently (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet
44 | function fromUnion(u: E1 | E2): string {
45 | const { b } = u; // union source whose arms type the field differently: narrow first
| ^
46 | return String(b);
destructuring-fences.ts:46:17 - error SC2004: uses of 'b' inherit the blocker on its declaration
45 | const { b } = u; // union source whose arms type the field differently: narrow first
46 | return String(b);
| ^
47 | }
hint: the declaration of 'b' did not compile — fix the diagnostic reported there and these sites clear with it
destructuring-fences.ts:51:7 - error SC1031: object destructuring of non-record values (the source is array-typed) are not supported yet
50 | const xs = [1, 2, 3];
@@ -0,0 +1 @@
{ "plain": 3750, "san": 5900 }
@@ -0,0 +1 @@
{ "plain": 3150, "san": 4400 }
@@ -0,0 +1 @@
{ "plain": 2900, "san": 2900 }