mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Destructure narrowed and joined union sources, await promise-armed unions, support writeFileSync(fd) (#2466)
This commit is contained in:
@@ -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. */
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fs-write-file-sync-fd.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-await-promise-arm.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-destructuring-join-narrow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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 }));
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user