Files
scriptc/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
T
2026-10-29 04:24:50 -05:00

2115 lines
79 KiB
TypeScript

import { keyedHelperName } from "../stable-names.js";
import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, ladderFenceExpr } from "../lowerer.js";
import { lowerFsPath } from "./fs-paths.js";
import { isJsSourceFile, locOf } from "../../program.js";
import {
builtinModuleFnOf,
FS_READDIR_DOCUMENTED_OPTIONS,
FS_WATCH_DOCUMENTED_OPTIONS,
FS_WATCH_FILE_DOCUMENTED_OPTIONS,
fenceOrDropOptionKey,
} from "../surfaces.js";
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
import {
BIGINT_STATS_T,
BOOL,
BYTES_U8,
DIR_T,
DYN,
F64,
FILEHANDLE_T,
FSWATCHER_T,
type IrExpr,
type IrLibFn,
type IrStmt,
type IrType,
STATS_T,
STRING,
type SrcLoc,
VOID,
arrayOf,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
import { optionMember, lowerBuiltinOptionalDefault } from "./arguments.js";
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
type FsSyncBufferWindowSource =
| { kind: "options"; node: ts.Expression | undefined }
| { kind: "offset"; node: ts.Expression };
/** Normalize fs.readSync/fs.writeSync's shorthand and inline-options buffer
* forms into the existing fixed-width descriptor IR. fd and buffer bind
* first, option values bind in object-literal order, and only then do the
* default window expressions read the bound buffer length. */
export function lowerFsSyncBufferWindow(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
fn: "fs.readSync" | "fs.writeSync",
source: FsSyncBufferWindowSource,
supported: string,
): IrExpr {
const fdValue = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64);
const bufferValue = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8);
const fdLocal = lowerer.declareHiddenLocal("%fsSyncFd", F64);
const bufferLocal = lowerer.declareHiddenLocal("%fsSyncBuffer", BYTES_U8);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: fdLocal.id, init: fdValue, loc: fdValue.loc },
{ kind: "varDecl", localId: bufferLocal.id, init: bufferValue, loc: bufferValue.loc },
];
const fd = (): IrExpr => varRef(fdLocal.id, fdLocal.type, loc);
const buffer = (): IrExpr => varRef(bufferLocal.id, bufferLocal.type, loc);
const number = (value: number): IrExpr => numLit(value, loc);
const stageNumber = (name: string, node: ts.Expression): IrExpr => {
const mapped = lowerer.mapTypeOf(lowerer.typeOf(node));
if (mapped?.kind !== "f64") {
lowerer.noLowering(
`${fn} with a '${mapped ? lowerer.fmt(mapped) : lowerer.checker.typeToString(lowerer.typeOf(node))}' ${name}`,
node,
supported,
);
}
const value = lowerer.lowerExprExpecting(node, F64);
const local = lowerer.declareHiddenLocal(`%fsSync${name}`, F64);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
return varRef(local.id, local.type, loc);
};
const isNull = (node: ts.Expression): boolean => {
let value = node;
while (ts.isParenthesizedExpression(value)) value = value.expression;
return value.kind === ts.SyntaxKind.NullKeyword;
};
let offset: IrExpr | null = null;
let length: IrExpr | null = null;
let position: IrExpr | null = null;
if (source.kind === "offset") {
offset = stageNumber("offset", source.node);
} else if (source.node !== undefined && !isNull(source.node)) {
if (!ts.isObjectLiteralExpression(source.node)) {
lowerer.noLowering(`${fn} with a non-literal options argument`, source.node, supported);
}
for (const property of (source.node as ts.ObjectLiteralExpression).properties.nodes) {
const member = optionMember(property);
if (member === null) {
lowerer.noLowering(`${fn} with a non-plain options member`, property, supported);
}
if (member.name === "offset") {
offset = stageNumber("offset", member.value);
} else if (member.name === "length") {
length = stageNumber("length", member.value);
} else if (member.name === "position") {
position = isNull(member.value) ? null : stageNumber("position", member.value);
} else {
const effect = lowerer.lowerExpr(member.value);
stmts.push({ kind: "exprStmt", expr: effect, loc: effect.loc });
}
}
}
const normalizedOffset = offset ?? number(0);
const normalizedLength =
length ??
({
kind: "bin",
op: "-",
left: {
kind: "bytesIntrinsic",
method: "length",
receiver: buffer(),
args: [],
type: F64,
loc,
},
right: normalizedOffset,
type: F64,
loc,
} satisfies IrExpr);
const result: IrExpr = {
kind: "libCall",
fn,
args: [fd(), buffer(), normalizedOffset, normalizedLength, position ?? number(-1)],
type: F64,
loc,
};
return { kind: "seqExpr", stmts, result, type: F64, loc };
}
/** One builtin-module function call → its libCall. Completes the call to
* the table's exact shape: variadicPack functions (path.join/resolve)
* accept any arity — or ONE spread of a string[] — and pack the
* arguments into a single array-literal argument; `defaults` complete
* omitted trailing arguments. fs.readFileSync keeps its historical
* per-site checks (the encoding must be the literal "utf8"). */
/** fs._toUnixTimestamp — the (underscore-stable) seconds coercion the
* utimes family runs on its time arguments: finite numbers pass
* (negatives answer now/1000, Node's shape), numeric STRINGS coerce
* through ToNumber's loose-equality gate, everything else throws
* Node's exact ERR_INVALID_ARG_TYPE. The argument crosses as a dyn
* value so the runtime renders the Received tail. Null when this is
* not that call (the table fence stays for other shapes). */
export function lowerFsTimestampValue(
lowerer: Lowerer,
node: ts.Expression | undefined,
loc: SrcLoc,
): IrExpr {
if (!node) return dynUndefinedExpr(loc);
const box = (value: IrExpr): IrExpr => {
if (value.type.kind === "date") {
const milliseconds: IrExpr = {
kind: "libCall",
fn: "date.getTime",
args: [value],
type: F64,
loc,
};
return {
kind: "libCall",
fn: "date.nativeNew",
args: [
{
kind: "dynArrLit",
elems: [{ kind: "dynFrom", value: milliseconds, type: DYN, loc }],
type: DYN,
loc,
},
],
type: DYN,
loc,
};
}
return lowerer.coerceInto(node, value, DYN);
};
return box(lowerer.lowerExpr(node));
}
export function lowerFsToUnixTimestampCall(
lowerer: Lowerer,
expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr | null {
if (bi.module !== "fs" || bi.member !== "_toUnixTimestamp") return null;
if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!))
return null;
return {
kind: "libCall",
fn: "fs.toUnixTimestamp",
args: [lowerFsTimestampValue(lowerer, expr.arguments.nodes[0], loc)],
type: F64,
loc,
};
}
/** True when opendirSync's arguments fit lowerFsOpendirCall: a string
* path and no options, or an object literal whose members are a numeric
* bufferSize and a utf8 encoding. */
function opendirLowers(lowerer: Lowerer, args: readonly ts.Expression[]): boolean {
if (args.length < 1 || args.length > 2) return false;
if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind !== "string") return false;
const options = args[1];
if (!options || (ts.isIdentifier(options) && options.text === "undefined")) return true;
if (!ts.isObjectLiteralExpression(options)) return false;
return options.properties.nodes.every((property) => {
const option = optionMember(property);
if (!option) return false;
if (option.name === "bufferSize")
return lowerer.mapTypeOf(lowerer.typeOf(option.value))?.kind === "f64";
if (option.name !== "encoding") return false;
const encoding = lowerer.typeOf(option.value);
return (
encoding.isStringLiteralType() && (encoding.value === "utf8" || encoding.value === "utf-8")
);
});
}
/** The fs validation-ladder spoke (checked-dynamic lane, JS sources
* only — TypeScript keeps its compile fences): implemented-namespace
* calls whose misuse Node rejects with typed errors lower to fs.*Chk
* libCalls that replicate the validation ladder over dyn values and
* throw Node's exact ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE /
* ERR_OUT_OF_RANGE — the honest tail (the real operation where one
* exists, the compiler-rendered SC2020 fence otherwise) runs only
* after every validation passes, exactly Node's order. Null when this
* is not a claimed member/shape (the table or fence path stands). */
export function lowerFsLadderCall(
lowerer: Lowerer,
expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr | null {
if (
bi.module === "fs" &&
bi.member !== "rename" &&
builtinModuleFnOf(lowerer, bi.module, bi.member)?.fn === "fs.callbackCall"
) {
return lowerer.lowerBuiltinModuleCall(
expr,
bi,
{ fn: "fs.callbackCall", params: [], result: DYN },
loc,
);
}
if (bi.module !== "fs" && bi.module !== "fs/promises") return null;
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
const args = expr.arguments.nodes;
if (args.some(ts.isSpreadElement)) return null;
// Every ladder argument crosses as a dyn value; an argument that
// cannot leaves the historical fence in place.
const dynArg = (node: ts.Expression | undefined): IrExpr | null => {
if (!node) return dynUndefinedExpr(loc);
const raw = lowerer.lowerExpr(node);
if (raw.type.kind === "dyn") return raw;
if (raw.kind === "unitLit" || lowerer.dynConvertible(raw.type)) {
return { kind: "dynFrom", value: raw, type: DYN, loc };
}
return null;
};
const dynArgs = (nodes: (ts.Expression | undefined)[]): IrExpr[] | null => {
const out: IrExpr[] = [];
for (const n of nodes) {
const v = dynArg(n);
if (v === null) return null;
out.push(v);
}
return out;
};
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? DYN;
const chk = (fn: IrLibFn, chkArgs: IrExpr[], type: IrType): IrExpr => ({
kind: "libCall",
fn,
args: chkArgs,
type,
loc,
});
if (bi.module === "fs/promises") {
if (bi.member !== "lchmod" || args.length !== 2) return null;
const a = dynArgs([args[0], args[1]]);
return a && chk("fsp.lchmodChk", a, { kind: "promise", inner: VOID });
}
switch (bi.member) {
case "exists": {
// The REAL deprecated-API shape: the callback validates
// synchronously (Node's one throwing arm), invalid paths answer
// false THROUGH it, and the answer is asynchronous.
// A provably-non-file `new URL('<literal>')` path (the suite's
// https://foo probe): Node answers false through the callback
// synchronously — the checked-dynamic tree has no URL kind, so the path slot
// carries the unvalidatable token instead (construction of a
// parseable literal is effect-free; file: URLs keep the fence —
// they would need the real path conversion).
let pathNode: ts.Expression | undefined = args[0];
let pathExpr: IrExpr | null = null;
if (
pathNode &&
ts.isNewExpression(pathNode) &&
ts.isIdentifier(pathNode.expression) &&
pathNode.expression.text === "URL" &&
lowerer.mapTypeOf(lowerer.typeOf(pathNode))?.kind === "url" &&
pathNode.arguments?.nodes.length === 1 &&
ts.isStringLiteral(pathNode.arguments.nodes[0]!)
) {
let parsed: URL | null = null;
try {
parsed = new URL(pathNode.arguments.nodes[0]!.text);
} catch {
parsed = null;
}
if (parsed === null || parsed.protocol === "file:") return null;
pathExpr = dynUndefinedExpr(loc);
pathNode = undefined;
}
pathExpr ??= dynArg(pathNode);
const cbExpr = dynArg(args[1]);
if (pathExpr === null || cbExpr === null) return null;
return chk("fs.existsChk", [pathExpr, cbExpr], DYN);
}
case "mkdtemp": {
// mkdtemp(prefix[, options], callback) — the callback is the
// LAST argument (makeCallback runs first, then the prefix).
const a = dynArgs([args[0], args.length >= 2 ? args[args.length - 1] : undefined]);
return (
a && chk("fs.mkdtempChk", [...a, ladderFenceExpr(lowerer, "fs.mkdtemp", expr)], resultT)
);
}
case "mkdtempSync": {
// The table serves the plain string 1-arg form; the ladder takes
// every other shape — prefix/encoding validation, then the REAL
// mkdtemp when the options leave utf8 semantics.
if (args.length === 1 && lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string")
return null;
if (args.length > 2) return null;
const a = dynArgs([args[0], args[1]]);
return (
a &&
chk(
"fs.mkdtempSyncChk",
[...a, ladderFenceExpr(lowerer, "fs.mkdtempSync with these options", expr)],
STRING,
)
);
}
case "readFile": {
// readFile(path[, options], callback): callback, assertEncoding,
// path — then the async read fences.
const a = dynArgs([
args[0],
args.length >= 3 ? args[1] : undefined,
args.length >= 2 ? args[args.length - 1] : undefined,
]);
return (
a && chk("fs.readFileChk", [...a, ladderFenceExpr(lowerer, "fs.readFile", expr)], resultT)
);
}
case "opendirSync": {
// A string path with no options or a literal { bufferSize, encoding:
// "utf8" } takes the real lowering; anything else keeps Node's
// validation ladder and its fence.
if (opendirLowers(lowerer, args)) return null;
const a = dynArgs([args[0], args[1]]);
return (
a && chk("fs.opendirChk", [...a, ladderFenceExpr(lowerer, "fs.opendirSync", expr)], resultT)
);
}
case "watchFile": {
// watchFile(filename[, options], listener): the path first, the
// listener's function contract second; real watching fences.
const a = dynArgs([args[0], args.length >= 2 ? args[args.length - 1] : undefined]);
return (
a && chk("fs.watchFileChk", [...a, ladderFenceExpr(lowerer, "fs.watchFile", expr)], resultT)
);
}
case "lchmod": {
// lchmod(path, mode, callback): callback, path, mode — macOS
// shapes (non-APPLE answers Node's not-a-function TypeError).
const a = dynArgs([args[0], args[1], args[2]]);
return a && chk("fs.lchmodChk", [...a, ladderFenceExpr(lowerer, "fs.lchmod", expr)], resultT);
}
case "lchmodSync": {
if (args.length > 2) return null;
const a = dynArgs([args[0], args[1]]);
return a && chk("fs.lchmodSyncChk", a, DYN);
}
case "read": {
// read(fd, buffer, offset, length, position, callback) — the
// positional form's full ladder; options-object forms keep the
// fence (their misuse arms are not in the target set).
if (args.length < 4) return null;
const a = dynArgs([
args[0],
args[1],
args[2],
args[3],
args.length >= 6 ? args[4] : undefined,
]);
return a && chk("fs.readChk", [...a, ladderFenceExpr(lowerer, "fs.read", expr)], resultT);
}
case "createReadStream":
case "createWriteStream": {
// The runtime streams (scr_fs_stream.c) run Node's checks; a third
// argument keeps the ladder's fence.
const a = dynArgs([args[0], args[1]]);
if (a && args.length <= 2) {
const readable = bi.member === "createReadStream";
return {
kind: "libCall",
fn: readable ? "fs.createReadStream" : "fs.createWriteStream",
args: a,
type: { kind: "object", className: readable ? "%Readable" : "%Writable" },
loc,
};
}
return (
a &&
chk("fs.streamOptsChk", [...a, ladderFenceExpr(lowerer, `fs.${bi.member}`, expr)], resultT)
);
}
default:
return null;
}
}
/** A valid string/byte overload must select its runtime entry from the live
* union tag. Coercing the whole argument to the string overload loses bytes. */
export function lowerStringOrBytesWrite(
lowerer: Lowerer,
call: ts.CallExpression,
dataType: Extract<IrType, { kind: "union" }>,
arms: readonly IrType[],
append: boolean,
): IrExpr {
const loc = locOf(call);
const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = keyedHelperName(lowerer, "%fs.writeData", key);
lowerer.valueHelpers.set(key, helper);
const body: IrStmt[] = [];
for (let tag = 0; tag < arms.length; tag++) {
const arm = arms[tag]!;
const fn: IrLibFn =
arm.kind === "bytes"
? append
? "fs.appendFileSyncBytes"
: "fs.writeFileSyncBytes"
: append
? "fs.appendFileSync"
: "fs.writeFileSync";
body.push({
kind: "if",
loc,
cond: {
kind: "unionIsTag",
unionId: dataType.unionId,
tag,
negated: false,
value: varRef("data.0", dataType, loc),
type: BOOL,
loc,
},
then: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn,
args: [
varRef("path.0", STRING, loc),
{
kind: "unionNarrow",
unionId: dataType.unionId,
tag,
value: varRef("data.0", dataType, loc),
type: arm,
loc,
},
],
type: VOID,
loc,
},
loc,
},
{ kind: "return", value: null, loc },
],
else_: null,
});
}
body.push({ kind: "return", value: null, loc });
lowerer.liftedFns.push({
name: helper,
returnType: VOID,
loc,
params: [
{ localId: "path.0", name: "path", type: STRING },
{ localId: "data.0", name: "data", type: dataType },
],
locals: [
{ id: "path.0", name: "path", type: STRING, mutable: false },
{ id: "data.0", name: "data", type: dataType, mutable: false },
],
body,
});
}
return {
kind: "call",
callee: helper,
args: [
lowerFsPath(lowerer, call.arguments.nodes[0]!),
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, dataType),
],
type: VOID,
loc,
};
}
/** Calls on an fs/promises FileHandle. The promise-returning methods run
* through the same synchronous descriptor primitives as fs.readSync /
* writeSync, then settle so failures reject at await. read/write retain
* the caller's buffer in the Node-shaped result record. */
export function lowerFileHandleMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "fileHandle") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, FILEHANDLE_T);
const promise = (inner: IrType): IrType => ({ kind: "promise", inner });
if (name === "createReadStream" || name === "createWriteStream" || name === "readLines") {
return lowerFileHandleStreamCall(lowerer, call, name, receiver(), loc);
}
const num = (
node: ts.Expression | undefined,
dflt: number,
lowered?: IrExpr,
): { value: IrExpr; defaulted: IrExpr } => {
const defaultValue: IrExpr = { kind: "numLit", value: dflt, type: F64, loc };
if (!node && !lowered) return { value: defaultValue, defaulted: boolLit(true, loc) };
if (lowered && isUnitType(lowered.type)) {
return { value: defaultAfterUndefined(lowered, defaultValue), defaulted: boolLit(true, loc) };
}
const undefinedArg = !lowered && node ? lowerStaticallyUndefinedArgument(lowerer, node) : null;
if (undefinedArg) {
return {
value: defaultAfterUndefined(undefinedArg, defaultValue),
defaulted: boolLit(true, loc),
};
}
if (!lowered && node && (lowerer.typeOf(node).flags & ts.TypeFlags.Null) !== 0) {
return {
value: defaultAfterUndefined(lowerer.lowerExpr(node), defaultValue),
defaulted: boolLit(true, loc),
};
}
const value = lowered ?? lowerer.lowerExpr(node!);
if (value.type.kind === "union") {
const def = lowerer.unions.get(value.type.unionId);
const units = def?.arms.filter(isUnitType) ?? [];
if (units.length > 0 && def?.arms.every((arm) => typeEquals(arm, F64) || isUnitType(arm))) {
// The normalized number and the separate "length was omitted"
// bit must observe one evaluation of a runtime optional union.
// Bind it in the numeric argument; the later bool argument reads
// the same hidden local (libCall arguments evaluate left-to-right).
const saved = lowerer.declareHiddenLocal("%fhOptNum", value.type);
const savedRef = (): IrExpr => ({
kind: "varRef",
localId: saved.id,
type: value.type,
loc: value.loc,
});
let defaulted: IrExpr = {
kind: "unionIsTag",
unionId: value.type.unionId,
tag: lowerer.armTag(value.type.unionId, units[0]!),
negated: false,
value: savedRef(),
type: BOOL,
loc: value.loc,
};
for (const unit of units.slice(1)) {
defaulted = {
kind: "logical",
op: "||",
left: defaulted,
right: {
kind: "unionIsTag",
unionId: value.type.unionId,
tag: lowerer.armTag(value.type.unionId, unit),
negated: false,
value: savedRef(),
type: BOOL,
loc: value.loc,
},
type: BOOL,
loc: value.loc,
};
}
return {
value: {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: saved.id, init: value, loc: value.loc }],
result: {
kind: "nullish",
left: savedRef(),
right: defaultValue,
type: F64,
loc: value.loc,
},
type: F64,
loc: value.loc,
},
defaulted,
};
}
}
return { value: lowerer.coerceInto(node ?? call, value, F64), defaulted: boolLit(false, loc) };
};
const utf8 = (node: ts.Expression | undefined): IrExpr => {
const dflt = { kind: "strLit", value: "utf8", type: STRING, loc } satisfies IrExpr;
if (!node) return dflt;
const nodeType = lowerer.typeOf(node);
const parts: readonly ts.Type[] = nodeType.isUnionType()
? ts.constituentTypes(nodeType)
: [nodeType];
const supported = parts.every(
(t) =>
(t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null)) !== 0 ||
(t.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8")),
);
if (!supported) {
lowerer.noLowering(
`FileHandle.${name} with a non-utf8 encoding`,
node,
"only utf8 data is supported",
);
}
return lowerBuiltinOptionalDefault(lowerer, node, STRING, dflt, true);
};
if (name === "sync" || name === "datasync") {
if (call.arguments.nodes.length !== 0)
lowerer.noLowering(`FileHandle.${name} with arguments`, call);
return {
kind: "libCall",
fn: name === "sync" ? "fileHandle.sync" : "fileHandle.datasync",
args: [receiver()],
type: promise(VOID),
loc,
};
}
if (name === "truncate" || name === "chmod") {
if (call.arguments.nodes.length > 1 || (name === "chmod" && call.arguments.nodes.length !== 1))
lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call);
return {
kind: "libCall",
fn: name === "truncate" ? "fileHandle.truncate" : "fileHandle.chmod",
args: [receiver(), num(call.arguments.nodes[0], 0).value],
type: promise(VOID),
loc,
};
}
if (name === "utimes") {
if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering("FileHandle.utimes with this argument shape", call);
return {
kind: "libCall",
fn: "fileHandle.utimes",
args: [
receiver(),
lowerFsTimestampValue(lowerer, call.arguments.nodes[0], loc),
lowerFsTimestampValue(lowerer, call.arguments.nodes[1], loc),
],
type: promise(VOID),
loc,
};
}
if (name === "readv" || name === "writev") {
if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2)
lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call);
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind !== "promise" || type.inner.kind !== "record")
lowerer.badType(call, lowerer.typeOf(call));
return {
kind: "libCall",
fn: name === "readv" ? "fileHandle.readv" : "fileHandle.writev",
args: [
receiver(),
lowerer.lowerExprExpecting(call.arguments.nodes[0]!, arrayOf(BYTES_U8)),
num(call.arguments.nodes[1], -1).value,
],
type,
loc,
};
}
if (name === "close" || name === "stat") {
if (call.arguments.nodes.length !== 0)
lowerer.noLowering(`FileHandle.${name} with arguments`, call);
return {
kind: "libCall",
fn: name === "close" ? "fileHandle.close" : "fileHandle.stat",
args: [receiver()],
type: promise(name === "close" ? VOID : { kind: "stats" }),
loc,
};
}
if (name === "readFile") {
if (call.arguments.nodes.length > 1) {
lowerer.noLowering(`FileHandle.readFile with ${call.arguments.nodes.length} arguments`, call);
}
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind !== "promise") lowerer.badType(call, lowerer.typeOf(call));
const encoding = utf8(call.arguments.nodes[0]);
if (typeEquals(type.inner, BYTES_U8)) {
return {
kind: "libCall",
fn: "fileHandle.readFileBytes",
args: [receiver(), encoding],
type,
loc,
};
}
if (!typeEquals(type.inner, STRING)) lowerer.badType(call, lowerer.typeOf(call));
return {
kind: "libCall",
fn: "fileHandle.readFile",
args: [receiver(), encoding],
type,
loc,
};
}
if (name === "writeFile" || name === "appendFile") {
if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) {
lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call);
}
const dataNode = call.arguments.nodes[0]!;
const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
// Evaluate a supplied utf8 encoding even for Buffer data, matching
// Node's argument order; it does not affect the bytes.
const encoding = utf8(call.arguments.nodes[1]);
if (dataT?.kind === "string") {
return {
kind: "libCall",
fn: "fileHandle.writeFile",
args: [receiver(), lowerer.lowerExprExpecting(dataNode, STRING), encoding],
type: promise(VOID),
loc,
};
}
if (dataT?.kind === "bytes" && dataT.elem === "u8") {
return {
kind: "libCall",
fn: "fileHandle.writeFileBytes",
args: [receiver(), lowerer.lowerExprExpecting(dataNode, BYTES_U8), encoding],
type: promise(VOID),
loc,
};
}
lowerer.noLowering(
`FileHandle.${name} of '${dataT ? lowerer.fmt(dataT) : lowerer.checker.typeToString(lowerer.typeOf(dataNode))}' data`,
dataNode,
"string and Uint8Array data are supported",
);
}
if (name === "read" || name === "write") {
const firstNode = call.arguments.nodes[0];
const firstT = firstNode ? lowerer.mapTypeOf(lowerer.typeOf(firstNode)) : undefined;
const secondT = call.arguments.nodes[1]
? lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[1]!))
: undefined;
const readOptions =
name === "read" &&
(!firstNode || firstT?.kind === "record" || (firstT && isUnitType(firstT)));
const bufferOptions =
firstT?.kind === "bytes" && firstT.elem === "u8" && secondT?.kind === "record";
if (readOptions || bufferOptions) {
if (call.arguments.nodes.length > (bufferOptions ? 2 : 1))
lowerer.noLowering(`FileHandle.${name} options with extra arguments`, call);
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind !== "promise" || type.inner.kind !== "record")
lowerer.badType(call, lowerer.typeOf(call));
const stmts: IrStmt[] = [];
const stage = (value: IrExpr, label: string): IrExpr => {
const local = lowerer.declareHiddenLocal(label, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
return varRef(local.id, local.type, loc);
};
const handle = stage(receiver(), "%fhWindowReceiver");
let buffer = bufferOptions
? stage(lowerer.lowerExprExpecting(firstNode!, BYTES_U8), "%fhWindowBuffer")
: undefined;
const optionsNode = bufferOptions ? call.arguments.nodes[1] : firstNode;
const options = optionsNode
? stage(lowerer.lowerExpr(optionsNode), "%fhWindowOptions")
: undefined;
const field = (key: string): IrExpr | undefined => {
if (options?.type.kind !== "record") return undefined;
const fields = lowerer.shapes.get(options.type.shapeId)?.fields;
const getter = fields?.find((f) => f.name === `%get:${key}`);
if (getter?.type.kind === "func") {
const callee: IrExpr = {
kind: "recordGet",
obj: options,
shapeId: options.type.shapeId,
field: getter.name,
type: getter.type,
loc,
};
return { kind: "callValue", callee, args: [], type: getter.type.ret, loc };
}
const member = fields?.find((f) => f.name === key);
return member
? {
kind: "recordGet",
obj: options,
shapeId: options.type.shapeId,
field: key,
type: member.type,
loc,
}
: undefined;
};
if (!buffer) {
const supplied = field("buffer");
const fresh: IrExpr = { kind: "bytesNew", source: numLit(16384, loc), type: BYTES_U8, loc };
if (!supplied || isUnitType(supplied.type))
buffer = supplied ? defaultAfterUndefined(supplied, fresh) : fresh;
else if (supplied.type.kind === "union") {
const arms = lowerer.unions.get(supplied.type.unionId)?.arms;
if (!arms?.every((arm) => typeEquals(arm, BYTES_U8) || isUnitType(arm)))
lowerer.noLowering("FileHandle.read with this buffer type", call);
buffer = { kind: "nullish", left: supplied, right: fresh, type: BYTES_U8, loc };
} else buffer = lowerer.coerceInto(call, supplied, BYTES_U8);
buffer = stage(buffer, "%fhWindowBuffer");
}
const offset = num(undefined, 0, field("offset"));
const length = num(undefined, -1, field("length"));
const position = num(undefined, -1, field("position"));
const result: IrExpr = {
kind: "libCall",
fn: name === "read" ? "fileHandle.read" : "fileHandle.writeBytes",
args: [handle, buffer, offset.value, length.value, position.value, length.defaulted],
type,
loc,
};
return { kind: "seqExpr", stmts, result, type, loc };
}
}
if (name === "read") {
if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) {
lowerer.noLowering(
`FileHandle.read with ${call.arguments.nodes.length} arguments`,
call,
"use read(buffer[, offset[, length[, position]]])",
);
}
const buffer = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BYTES_U8);
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind !== "promise" || type.inner.kind !== "record") {
lowerer.badType(call, lowerer.typeOf(call));
}
const offset = num(call.arguments.nodes[1], 0);
const length = num(call.arguments.nodes[2], -1);
const position = num(call.arguments.nodes[3], -1);
return {
kind: "libCall",
fn: "fileHandle.read",
args: [receiver(), buffer, offset.value, length.value, position.value, length.defaulted],
type,
loc,
};
}
if (name === "write") {
if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) {
lowerer.noLowering(`FileHandle.write with ${call.arguments.nodes.length} arguments`, call);
}
const dataNode = call.arguments.nodes[0]!;
const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind !== "promise" || type.inner.kind !== "record") {
lowerer.badType(call, lowerer.typeOf(call));
}
if (dataT?.kind === "bytes" && dataT.elem === "u8") {
const offset = num(call.arguments.nodes[1], 0);
const length = num(call.arguments.nodes[2], -1);
const position = num(call.arguments.nodes[3], -1);
return {
kind: "libCall",
fn: "fileHandle.writeBytes",
args: [
receiver(),
lowerer.lowerExprExpecting(dataNode, BYTES_U8),
offset.value,
length.value,
position.value,
length.defaulted,
],
type,
loc,
};
}
if (dataT?.kind === "string") {
if (call.arguments.nodes.length > 3) {
lowerer.noLowering(
`FileHandle.write(string) with ${call.arguments.nodes.length} arguments`,
call,
'use write(string[, position[, "utf8"]])',
);
}
const position = num(call.arguments.nodes[1], -1);
return {
kind: "libCall",
fn: "fileHandle.writeStr",
args: [
receiver(),
lowerer.lowerExprExpecting(dataNode, STRING),
position.value,
utf8(call.arguments.nodes[2]),
],
type,
loc,
};
}
lowerer.noLowering(
`FileHandle.write of '${dataT ? lowerer.fmt(dataT) : lowerer.checker.typeToString(lowerer.typeOf(dataNode))}' data`,
dataNode,
"string and Uint8Array data are supported",
);
}
lowerer.noLowering(
`FileHandle.${name}`,
call,
"fd, close(), read(), write(), readv(), writev(), readFile(), writeFile(), appendFile(), stat(), sync(), datasync(), truncate(), and chmod() are the supported FileHandle members",
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** FileHandle.createReadStream / createWriteStream(options?) and
* readLines(options?): the options cross as one dyn value (the runtime
* spreads an options object, as Node's `{ ...options, fd: this }` does).
* readLines is `new readline.Interface({ input: createReadStream(options),
* crlfDelay: Infinity })`. */
function lowerFileHandleStreamCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: "createReadStream" | "createWriteStream" | "readLines",
receiver: IrExpr,
loc: SrcLoc,
): IrExpr {
const args = call.arguments.nodes;
if (args.some(ts.isSpreadElement) || args.length > 1) {
lowerer.noLowering(`FileHandle.${name} with ${args.length} arguments`, call);
}
let options: IrExpr = dynUndefinedExpr(loc);
if (args[0] !== undefined) {
const raw = lowerer.lowerExpr(args[0]);
if (raw.type.kind === "dyn") options = raw;
else if (raw.kind === "unitLit" || lowerer.dynConvertible(raw.type)) {
options = { kind: "dynFrom", value: raw, type: DYN, loc };
} else {
lowerer.noLowering(
`FileHandle.${name} with a '${lowerer.fmt(raw.type)}' options value`,
args[0],
"the options are a plain object",
);
}
}
const writable = name === "createWriteStream";
const stream: IrExpr = {
kind: "libCall",
fn: writable ? "fileHandle.createWriteStream" : "fileHandle.createReadStream",
args: [receiver, options],
type: { kind: "object", className: writable ? "%Writable" : "%Readable" },
loc,
};
if (name !== "readLines") return stream;
// rl.create's mode 1: an input stream, no output, then crlfDelay.
return {
kind: "libCall",
fn: "rl.create",
args: [
{ kind: "numLit", value: 1, type: F64, loc },
stream,
{ kind: "numLit", value: Infinity, type: F64, loc },
],
type: F64,
loc,
};
}
/** fs.Dir's members: path, and the sync and promise read/close forms (the
* callback overloads have no lowering; for-await lowers in
* lower-async-iteration.ts). */
export const DIR_METHODS = new Set(["readSync", "read", "closeSync", "close"]);
export const DIR_MEMBERS =
"path, readSync(), read(), closeSync(), close(), and for-await iteration are the supported Dir members (no callbacks)";
/** Method calls on Dir-typed receivers. readSync answers the call site's
* `Dirent | null` (the emitter builds the row); read is its promise twin.
* Null for non-Dir receivers. */
export function lowerDirMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "dir") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (DIR_METHODS.has(name) && call.arguments.nodes.length === 0) {
const receiver = lowerer.lowerExprExpecting(access.expression, DIR_T);
if (name === "closeSync")
return { kind: "libCall", fn: "dir.closeSync", args: [receiver], type: VOID, loc };
if (name === "close") {
return {
kind: "libCall",
fn: "dir.close",
args: [receiver],
type: { kind: "promise", inner: VOID },
loc,
};
}
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
const entry = type?.kind === "promise" ? type.inner : type;
const arms = entry?.kind === "union" ? lowerer.unions.get(entry.unionId)?.arms : undefined;
if (
type &&
arms?.length === 2 &&
arms.some((arm) => arm.kind === "record") &&
arms.some((arm) => arm.kind === "nullT") &&
(name === "read") === (type.kind === "promise")
) {
return {
kind: "libCall",
fn: name === "read" ? "dir.read" : "dir.readSync",
args: [receiver],
type,
loc,
};
}
}
return lowerer.noLowering(
`Dir.${name}`,
call,
DIR_MEMBERS,
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** The Stats type tests, each a read of the snapshot's mode. */
export const STATS_TYPE_TESTS: Record<string, IrLibFn> = {
isFile: "stats.isFile",
isDirectory: "stats.isDirectory",
isSymbolicLink: "stats.isSymbolicLink",
isBlockDevice: "stats.isBlockDevice",
isCharacterDevice: "stats.isCharacterDevice",
isFIFO: "stats.isFIFO",
isSocket: "stats.isSocket",
};
/** The same type tests on a BigIntStats receiver (statSync with
* `{ bigint: true }`): one more snapshot read, no bigint arithmetic. */
const BIGINT_STATS_TYPE_TESTS: Record<string, IrLibFn> = {
isFile: "bigstats.isFile",
isDirectory: "bigstats.isDirectory",
isSymbolicLink: "bigstats.isSymbolicLink",
isBlockDevice: "bigstats.isBlockDevice",
isCharacterDevice: "bigstats.isCharacterDevice",
isFIFO: "bigstats.isFIFO",
isSocket: "bigstats.isSocket",
};
/** Method calls on Stats-typed receivers: the type tests are pure reads on
* the stat snapshot (a followed statSync snapshot never answers true to
* isSymbolicLink — take lstatSync's, Node's own split). Anything else
* fences member-qualified. Null for non-Stats receivers. */
export function lowerStatsMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
// `stat?.isFile()` on a `Stats | undefined` receiver: the optional-chain
// lowering guards the receiver, then re-enters here with it narrowed.
if (
(call.questionDotToken && !lowerer.chainHandled.has(call)) ||
(access.questionDotToken && !lowerer.chainHandled.has(access))
) {
return null;
}
const receiverKind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
if (receiverKind !== "stats" && receiverKind !== "bigintStats") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
// BigIntStats' type tests are the same snapshot mode read; only the kind
// of the handle differs.
const table = receiverKind === "bigintStats" ? BIGINT_STATS_TYPE_TESTS : STATS_TYPE_TESTS;
const fn = Object.hasOwn(table, name) ? table[name] : undefined;
if (fn && call.arguments.nodes.length === 0) {
const receiver = lowerer.lowerExpr(access.expression);
return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) };
}
lowerer.noLowering(
`Stats.${name}`,
call,
"isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(), dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, atimeMs, mtimeMs, ctimeMs, birthtimeMs, atime, mtime, ctime, and birthtime are the supported Stats members",
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** The watch-listener shapes: () , (eventType: string), and
* (eventType: string, filename: string | null) — the filename parameter
* must admit null (Node passes null when the platform names nothing), or
* be the checked dynamic. Returns the parameter count, or null when the
* shape has no lowering. */
function watchListenerArity(lowerer: Lowerer, cb: IrExpr): 0 | 1 | 2 | null {
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 2)
return null;
const [event, name] = cb.type.params;
if (event !== undefined && event.kind !== "string" && event.kind !== "dyn") return null;
if (name !== undefined && name.kind !== "dyn") {
const def = name.kind === "union" ? lowerer.unions.get(name.unionId) : undefined;
if (
!def ||
!def.arms.some((a) => a.kind === "nullT") ||
!def.arms.some((a) => a.kind === "string") ||
!def.arms.every((a) => a.kind === "nullT" || a.kind === "undefinedT" || a.kind === "string")
)
return null;
}
return cb.type.params.length as 0 | 1 | 2;
}
/** `fs.watch(path[, options][, listener])` → the fs.watch libCall
* (scr_watch.c — kqueue EVFILT_VNODE on the opened path; an unopenable
* path THROWS Node's fs error synchronously, the polling-fallback catch
* shape). The listener fires with "rename"/"change" and takes zero
* parameters, the eventType string, or (eventType, filename: string |
* null) — the filename is the entry name on a directory watch and the
* watched file's basename otherwise (Linux inotify names both; a macOS
* directory watch has no name source until F3). The options record follows the options-record
* stance: persistent: true, recursive: false, and encoding: "utf8"
* state the lowered behavior and are accepted; persistent: false
* (a watcher that does NOT hold the loop), recursive: true (kqueue
* watches one inode), signal, and non-utf8 encodings fence by name;
* undocumented keys drop like Node. An open watcher keeps the loop
* alive until watcher.close(). */
export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
if (
expr.arguments.nodes.length < 1 ||
expr.arguments.nodes.length > 3 ||
expr.arguments.nodes.some(ts.isSpreadElement)
) {
lowerer.noLowering(
"fs.watch with this argument shape",
expr,
"the supported forms are watch(path[, options][, listener])",
);
}
const hasOptions =
expr.arguments.nodes.length >= 2 && ts.isObjectLiteralExpression(expr.arguments.nodes[1]!);
if (expr.arguments.nodes.length === 3 && !hasOptions) {
lowerer.noLowering(
"fs.watch with a non-literal options argument",
expr.arguments.nodes[1]!,
"pass the options as an object literal: watch(path, { recursive?, persistent?, encoding? }, listener)",
);
}
if (hasOptions) {
for (const prop of (expr.arguments.nodes[1] as ts.ObjectLiteralExpression).properties.nodes) {
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) {
lowerer.noLowering(
"fs.watch options with computed keys or spreads",
prop,
"each option must be a plain `name: value` entry with a literal key",
);
}
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
lowerer.noLowering(
"fs.watch options with computed keys",
prop,
"each option must be a plain `name: value` entry with a literal key",
);
}
const key = prop.name.text;
const init = ts.isPropertyAssignment(prop) ? prop.initializer : null;
if (key === "persistent") {
// true IS the lowering (an open watcher holds the loop) —
// stating the default is a no-op; false has no lowering.
if (init !== null && init.kind === ts.SyntaxKind.TrueKeyword) continue;
lowerer.noLowering(
"fs.watch with persistent disabled",
prop,
"an open watcher keeps the loop alive until close() — that IS the lowering; " +
"persistent: false (a watcher the process does not wait for) has no lowering",
);
}
if (key === "recursive") {
if (init !== null && init.kind === ts.SyntaxKind.FalseKeyword) continue;
lowerer.noLowering(
"fs.watch with the recursive option",
prop,
"recursive watching has no lowering yet — kqueue watches the one opened path; watch each path",
);
}
if (key === "encoding") {
const enc = init !== null && ts.isStringLiteralLike(init) ? init.text : null;
if (enc === "utf8" || enc === "utf-8") continue;
lowerer.noLowering(
"fs.watch with a non-utf8 encoding",
prop,
"the filename argument is a string — utf8 (the default) is the accepted encoding",
);
}
if (key === "signal") {
lowerer.noLowering(
"fs.watch with an abort signal",
prop,
"abortable watchers have no lowering — call watcher.close() instead",
);
}
fenceOrDropOptionKey(
lowerer,
prop,
key,
"fs.watch",
FS_WATCH_DOCUMENTED_OPTIONS,
'persistent: true, recursive: false, and encoding: "utf8" are the accepted options',
);
// An undocumented key, dropped like Node drops it.
}
}
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const args: IrExpr[] = [path];
const listenerArg = hasOptions
? expr.arguments.nodes.length === 3
? expr.arguments.nodes[2]!
: null
: expr.arguments.nodes.length === 2
? expr.arguments.nodes[1]!
: null;
if (listenerArg !== null) {
const cb = lowerer.lowerExpr(listenerArg);
if (watchListenerArity(lowerer, cb) === null) {
lowerer.noLowering(
"fs.watch with this listener shape",
listenerArg,
"the listener takes (), (eventType: string), or (eventType: string, filename: string | null)",
);
}
args.push(cb);
}
return {
kind: "libCall",
fn: args.length === 2 ? "fs.watchCb" : "fs.watch",
args,
type: FSWATCHER_T,
loc,
};
}
/** `fs.watchFile(path[, options], listener)` and
* `fs.unwatchFile(path[, listener])` → the polling libCalls
* (scr_watch_file.c: libuv's uv_fs_poll shape — one poller per RESOLVED
* path, shared by every listener on it, re-armed every `interval` ms,
* 5007 by default, persistent by default). The listener takes
* (current: Stats, previous: Stats) — zero, one or both — and runs when a
* poll sees a difference; the first poll only records the baseline, and a
* file that cannot be stat'd polls as all-zero Stats. The options record
* follows the options-record stance: interval, persistent: true/false and
* bigint: false are accepted; bigint: true fences (BigIntStats); other
* documented keys fence by name; undocumented keys drop like Node.
* Statement position only — Node returns a StatWatcher, which has no
* lowering. Returns null for any untyped shape so the checked-dynamic
* ladder (fs.watchFileChk) keeps its current IR. */
export function lowerFsWatchFileCall(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
unwatch: boolean,
): IrExpr | null {
const nodes = expr.arguments.nodes;
if (nodes.length < 1 || nodes.length > 3 || nodes.some(ts.isSpreadElement)) return null;
if (lowerer.mapTypeOf(lowerer.typeOf(nodes[0]!))?.kind !== "string") return null;
const path = lowerer.lowerExprExpecting(nodes[0]!, STRING);
if (unwatch) {
if (nodes.length > 2) return null;
const args: IrExpr[] = [path];
if (nodes.length === 2) {
if (lowerer.mapTypeOf(lowerer.typeOf(nodes[1]!))?.kind !== "func") return null;
args.push(lowerer.lowerExpr(nodes[1]!));
}
return { kind: "libCall", fn: "fs.unwatchFile", args, type: VOID, loc };
}
const hasOptions = nodes.length >= 2 && ts.isObjectLiteralExpression(nodes[1]!);
if (nodes.length === 3 && !hasOptions) return null;
if (nodes.length === 2 && hasOptions) return null; /* no listener */
const listenerArg = nodes[nodes.length - 1]!;
if (lowerer.mapTypeOf(lowerer.typeOf(listenerArg))?.kind !== "func") return null;
const parent = expr.parent;
if (!ts.isExpressionStatement(parent) && !(ts.isArrowFunction(parent) && parent.body === expr)) {
lowerer.unsupported(
"SC1090",
expr,
"using the returned StatWatcher (register the fs.watchFile call as its own statement)",
);
}
let interval: IrExpr | null = null;
let persistent: IrExpr | null = null;
if (hasOptions) {
for (const prop of (nodes[1] as ts.ObjectLiteralExpression).properties.nodes) {
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) {
lowerer.noLowering(
"fs.watchFile options with computed keys or spreads",
prop,
"each option must be a plain `name: value` entry with a literal key",
);
}
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
lowerer.noLowering(
"fs.watchFile options with computed keys",
prop,
"each option must be a plain `name: value` entry with a literal key",
);
}
const key = prop.name.text;
const value = ts.isPropertyAssignment(prop)
? prop.initializer
: ts.isShorthandPropertyAssignment(prop)
? prop.name
: null;
if (key === "interval" && value !== null) {
interval = lowerer.lowerExprExpecting(value, F64);
continue;
}
if (key === "persistent" && value !== null) {
persistent = lowerer.lowerExprExpecting(value, BOOL);
continue;
}
if (key === "bigint") {
// The fallback's option record declares `bigint: false`, so the
// false literal (or `false` as a value) states the lowered Stats
// shape; BigIntStats has no lowering.
if (value !== null && value.kind === ts.SyntaxKind.FalseKeyword) continue;
lowerer.noLowering(
"fs.watchFile with bigint: true",
prop,
"the listener receives Stats (double integers); BigIntStats has no lowering",
);
}
fenceOrDropOptionKey(
lowerer,
prop,
key,
"fs.watchFile",
FS_WATCH_FILE_DOCUMENTED_OPTIONS,
"interval, persistent: true/false, and bigint: false are the accepted options",
);
// An undocumented key, dropped like Node drops it.
}
}
const cb = lowerer.lowerExpr(listenerArg);
if (
cb.type.kind !== "func" ||
cb.type.ret.kind !== "void" ||
cb.type.params.length > 2 ||
!cb.type.params.every((p) => p.kind === "stats")
) {
lowerer.noLowering(
"fs.watchFile with this listener shape",
listenerArg,
"the listener takes (current: Stats, previous: Stats)",
);
}
return {
kind: "libCall",
fn: "fs.watchFile",
args: [path, interval ?? numLit(5007, loc), persistent ?? boolLit(true, loc), cb],
type: VOID,
loc,
};
}
/** Method calls on FSWatcher receivers: close() (idempotent, statement
* position — Node returns void there too) and the EventEmitter surface for
* 'change', 'error' and 'close' (on/once/addListener/prependListener/
* prependOnceListener/off/removeListener, registered as their own
* statement). Everything else @types/node declares (ref/unref, other
* events) fences member-qualified. Null for non-watcher receivers. */
export function lowerWatcherMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "fsWatcher") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (name === "close" && call.arguments.nodes.length === 0) {
const receiver = lowerer.lowerExpr(access.expression);
return { kind: "libCall", fn: "watcher.close", args: [receiver], type: VOID, loc };
}
// The EventEmitter surface for the three events the watcher emits:
// "change" (the listener shapes fs.watch takes), "error", and "close".
const ON = ["on", "once", "addListener", "prependListener", "prependOnceListener"];
if (
(ON.includes(name) || name === "off" || name === "removeListener") &&
call.arguments.nodes.length === 2
) {
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
const code = event === "change" ? 0 : event === "error" ? 1 : event === "close" ? 2 : -1;
if (code < 0) {
lowerer.noLowering(
`FSWatcher.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
'"change", "error", and "close" are the supported FSWatcher events (as literals)',
);
}
const parent = call.parent;
if (
!ts.isExpressionStatement(parent) &&
!(ts.isArrowFunction(parent) && parent.body === call)
) {
lowerer.unsupported(
"SC1090",
call,
`using the result of FSWatcher.${name} (register it as its own statement)`,
);
}
const receiver = lowerer.lowerExpr(access.expression);
const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
if (name === "off" || name === "removeListener") {
return {
kind: "libCall",
fn: "watcher.off",
args: [receiver, numLit(code, loc), cb],
type: VOID,
loc,
};
}
const ok =
code === 0
? watchListenerArity(lowerer, cb) !== null
: code === 1
? cb.type.kind === "func" &&
cb.type.ret.kind === "void" &&
cb.type.params.length <= 1 &&
(cb.type.params[0] === undefined ||
cb.type.params[0].kind === "dyn" ||
(cb.type.params[0].kind === "object" && cb.type.params[0].className === "%Error"))
: cb.type.kind === "func" && cb.type.ret.kind === "void" && cb.type.params.length === 0;
if (!ok) {
lowerer.noLowering(
`FSWatcher.${name}("${event}", ...) with this listener shape`,
call.arguments.nodes[1]!,
code === 0
? "the listener takes (), (eventType: string), or (eventType: string, filename: string | null)"
: code === 1
? "the listener takes () or (error: Error)"
: "the listener takes ()",
);
}
const flags =
(name === "once" || name === "prependOnceListener" ? 1 : 0) |
(name.startsWith("prepend") ? 2 : 0);
return {
kind: "libCall",
fn: "watcher.on",
args: [receiver, numLit(code, loc), numLit(flags, loc), cb],
type: VOID,
loc,
};
}
lowerer.noLowering(
`FSWatcher.${name}`,
call,
'close(), and on/once/addListener/prependListener/prependOnceListener/off/removeListener for "change", "error", and "close" are the supported FSWatcher members',
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** `os.networkInterfaces()` — getifaddrs(3) behind Node's exact result
* type. The libCall's type is the CALL SITE's mapped
* `NodeJS.Dict<NetworkInterfaceInfo[]>`: a pure index-signature record
* whose value is `Info[] | undefined`, Info the two-record union
* @types/node declares (IPv4: `scopeid?: number` — the undefined-armed
* union — and IPv6: `scopeid: number`; family literal types collapse to
* string in both). The emitter derives every shape/union/tag from that
* type, so the structure is verified HERE and anything else (an older
* @types/node, a user alias reshaping the result) fences honestly. Key
* and row order follow getifaddrs enumeration — Node itself guarantees
* no order (compare structurally). */
/** `fs.readdirSync` / `fs.promises.readdir` with `{ withFileTypes: true }`
* — Dirent rows: name + parentPath (the path argument as given, Node's
* own rule) + the hidden %dtype entry kind (libuv's UV_DIRENT encoding;
* DT_UNKNOWN falls back to lstat, Node's getDirents rule). The options
* literal is checked member-by-member; the result type must carry the
* interned Dirent record array from type-mapper.ts — anything else
* (encoding: 'buffer', a user alias reshaping Dirent) fences honestly. */
export function lowerFsReaddirTypesCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
promiseForm: boolean,
): IrExpr {
const operation = promiseForm ? "fs.promises.readdir" : "readdirSync";
const spelling = promiseForm ? "readdir" : "readdirSync";
const optsNode = call.arguments.nodes[1]!;
if (!ts.isObjectLiteralExpression(optsNode)) {
lowerer.noLowering(
`${operation} with a non-literal options argument`,
optsNode,
`pass the options inline so each member can be checked: ${spelling}(path, { withFileTypes: true })`,
);
}
let sawWithFileTypes = false;
let recursive: ts.Expression | null = null;
for (const p of optsNode.properties.nodes) {
const m = optionMember(p);
if (!m) {
lowerer.noLowering(
`${operation} with this options shape`,
p,
"spreads and computed keys have no lowering — write each member inline",
);
}
if (m.name === "withFileTypes") {
const t = lowerer.typeOf(m.value);
if (!(t.flags & ts.TypeFlags.BooleanLiteral) || lowerer.checker.typeToString(t) !== "true") {
lowerer.noLowering(
`${operation} with a non-literal-true withFileTypes`,
m.value,
"withFileTypes: true is the Dirent form; omit the options for plain names",
);
}
sawWithFileTypes = true;
} else if (m.name === "encoding") {
const t = lowerer.typeOf(m.value);
if (!t.isStringLiteralType() || (t.value !== "utf8" && t.value !== "utf-8")) {
lowerer.noLowering(
`${operation} with a non-utf8 encoding`,
m.value,
"names decode as utf8 (the default); encoding: 'buffer' has no lowering",
);
}
} else if (m.name === "recursive" && !promiseForm) {
recursive = m.value;
} else {
// The options-record stance: recursive (a documented knob with
// no lowering here) fences by name; undocumented keys drop like Node.
fenceOrDropOptionKey(
lowerer,
p,
m.name,
operation,
FS_READDIR_DOCUMENTED_OPTIONS,
"withFileTypes: true (and the default encoding) is the supported options surface — recursive listings want an explicit walk",
);
}
}
if (recursive !== null) {
// Only the `true`/`false` keywords fold; any other expression, even one
// typed as a literal, evaluates at run time for its effects.
let flag = recursive;
while (ts.isParenthesizedExpression(flag)) flag = flag.expression;
const literal =
flag.kind === ts.SyntaxKind.TrueKeyword
? "true"
: flag.kind === ts.SyntaxKind.FalseKeyword
? "false"
: null;
if (sawWithFileTypes && literal !== "false") {
lowerer.noLowering(
`${operation} with withFileTypes and recursive`,
recursive,
"recursive: true lists relative names; withFileTypes: true lists one directory's Dirents",
);
}
if (!sawWithFileTypes) return lowerFsReaddirRecursive(lowerer, call, recursive, literal, loc);
}
if (!sawWithFileTypes) {
lowerer.noLowering(
`${operation} with 2 arguments`,
call,
`${spelling}(path) lists names; ${spelling}(path, { withFileTypes: true }) lists Dirents`,
);
}
const fence: () => never = () =>
lowerer.noLowering(
`${operation}(path, { withFileTypes: true }) where the result is not the Dirent array`,
call,
"{ name, parentPath, isFile(), isDirectory(), isSymbolicLink() } rows are the supported result shape",
);
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
if (!callType) fence();
let result = callType;
if (promiseForm) {
if (callType.kind !== "promise") fence();
result = callType.inner;
}
if (result?.kind !== "array" || result.elem.kind !== "record") fence();
const shape = lowerer.shapes.get(result.elem.shapeId);
if (
!shape ||
shape.tuple ||
shape.indexValue ||
shape.fields.length !== 3 ||
!shape.fields.some((f) => f.name === "%dtype")
) {
fence();
}
const path = lowerFsPath(lowerer, call.arguments.nodes[0]!);
return {
kind: "libCall",
fn: promiseForm ? "fsp.readdirTypes" : "fs.readdirTypesSync",
args: [path],
type: callType,
loc,
};
}
/** `fs.readdirSync(path, { recursive })` without withFileTypes: the
* relative names of every entry below the path when `recursive` is true
* (scr_fs_readdir_recursive, Node's queue order), the directory's own names
* when it is false. A recursive flag that is not a boolean literal selects
* between the two at run time, after the path is evaluated. */
function lowerFsReaddirRecursive(
lowerer: Lowerer,
call: ts.CallExpression,
recursive: ts.Expression,
literal: string | null,
loc: SrcLoc,
): IrExpr {
const names = arrayOf(STRING);
// @types/node types the call `string[] | Buffer[]` when the options name
// no encoding; the names are strings, so the call is the string[] arm.
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
if (
!callType ||
!(
typeEquals(callType, names) ||
(callType.kind === "union" && lowerer.armTag(callType.unionId, names) >= 0)
)
) {
lowerer.noLowering(
"readdirSync(path, { recursive }) where the result is not a string array",
call,
"names decode as utf8 strings (the default encoding)",
);
}
return lowerFsReaddirNames(lowerer, call, recursive, literal, names, loc);
}
function lowerFsReaddirNames(
lowerer: Lowerer,
call: ts.CallExpression,
recursive: ts.Expression,
literal: string | null,
names: IrType,
loc: SrcLoc,
): IrExpr {
const path = lowerFsPath(lowerer, call.arguments.nodes[0]!);
const listing = (fn: IrLibFn, arg: IrExpr): IrExpr => ({
kind: "libCall",
fn,
args: [arg],
type: names,
loc,
});
if (literal === "true") return listing("fs.readdirRecursiveSync", path);
if (literal === "false") return listing("fs.readdirSync", path);
const pathLocal = lowerer.declareHiddenLocal("%readdirPath", STRING);
const pathRef = varRef(pathLocal.id, STRING, loc);
const flag = lowerer.lowerExprExpecting(recursive, BOOL);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: pathLocal.id, init: path, loc }],
result: {
kind: "ternary",
cond: flag,
then: listing("fs.readdirRecursiveSync", pathRef),
else_: listing("fs.readdirSync", pathRef),
type: names,
loc,
},
type: names,
loc,
};
}
/** fs.statSync / fs.lstatSync's documented option keys. */
const FS_STAT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set(["bigint", "throwIfNoEntry"]);
/** fs.fstatSync's: an open descriptor never misses, so there is no
* throwIfNoEntry. */
const FS_FSTAT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set(["bigint"]);
/** The stat representation a mapped result type carries: BigIntStats and
* `BigIntStats | undefined` read the bigint member table, Stats and
* `Stats | undefined` the number one; anything else is neither. */
function statRepresentation(
callType: IrType | undefined | null,
arms: readonly IrType[] | undefined,
): "bigint" | "number" | null {
const kindOf = (t: IrType): "bigint" | "number" | null =>
t.kind === "bigintStats" ? "bigint" : t.kind === "stats" ? "number" : null;
if (callType === undefined || callType === null) return null;
const own = kindOf(callType);
if (own !== null) return own;
if (arms !== undefined) {
for (const arm of arms) {
const repr = kindOf(arm);
if (repr !== null) return repr;
}
}
return null;
}
/** The `bigint` option member. The literal picks the representation —
* BigIntStats for true, Stats for false — and the call's own type must
* already agree with it, because a `Stats | BigIntStats`-valued flag has no
* static member table. Answers the member's lowered value when evaluating
* it can have an effect (`bigint: (count++, true)`), which the call then
* runs in its place among the options; null when it can be dropped. */
function checkStatBigintOption(
lowerer: Lowerer,
value: ts.Expression,
spelling: string,
representation: "bigint" | "number" | null,
): IrExpr | null {
const t = lowerer.typeOf(value);
const literal = t.flags & ts.TypeFlags.BooleanLiteral ? lowerer.checker.typeToString(t) : null;
if (literal === null) {
lowerer.noLowering(
`${spelling} with a runtime-valued bigint option`,
value,
"pass the literal true or false: the result's representation is chosen statically",
);
}
if (representation === null || (literal === "true") !== (representation === "bigint")) {
lowerer.noLowering(
`${spelling} with bigint: ${literal} where the result is not ${literal === "true" ? "BigIntStats" : "Stats"}`,
value,
"pass bigint: true to receive BigIntStats, or omit it to receive Stats",
);
}
const lowered = lowerer.lowerExprExpecting(value, BOOL);
return isSafeToDiscard(lowered) ? null : lowered;
}
/** fs.statSync / fs.lstatSync(path, options). The options are an object
* literal, where throwIfNoEntry takes any boolean and bigint is the literal
* true (BigIntStats) or false (Stats), or a stored record whose only member
* is a boolean throwIfNoEntry. When the call's type is
* `Stats | undefined` (or `BigIntStats | undefined`), the call lowers to the
* probe, which answers undefined for a missing entry without building the
* error. An explicitly undefined options argument is the one-argument
* form. */
export function lowerFsStatOptionsCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
noFollow: boolean,
): IrExpr {
const spelling = noFollow ? "lstatSync" : "statSync";
const plainFn: IrLibFn = noFollow ? "fs.lstatSync" : "fs.statSync";
const optsNode = call.arguments.nodes[1]!;
const path = lowerFsPath(lowerer, call.arguments.nodes[0]!);
// `{ bigint: true }` selects Node's BigIntStats: the SAME snapshot handle,
// read through the bigint member table. The checker's answer for the call
// already names the variant, so the mapped call type is the source of
// truth and the option member below has to agree with it.
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
const def = callType?.kind === "union" ? lowerer.unions.get(callType.unionId) : undefined;
const representation = statRepresentation(callType, def?.arms);
const statsType: IrType = representation === "bigint" ? BIGINT_STATS_T : STATS_T;
// The path binds first, then the options' effects, then the stat runs.
const plainAfter = (effect: IrExpr | null): IrExpr => {
if (effect === null)
return { kind: "libCall", fn: plainFn, args: [path], type: statsType, loc };
const pathLocal = lowerer.declareHiddenLocal("%statPath", STRING);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: pathLocal.id, init: path, loc: path.loc },
{ kind: "exprStmt", expr: effect, loc: effect.loc },
],
result: {
kind: "libCall",
fn: plainFn,
args: [varRef(pathLocal.id, STRING, loc)],
type: statsType,
loc,
},
type: statsType,
loc,
};
};
const undefinedOptions = lowerStaticallyUndefinedArgument(lowerer, optsNode);
if (undefinedOptions !== null) {
return plainAfter(isSafeToDiscard(undefinedOptions) ? null : undefinedOptions);
}
let throwIfNoEntry: IrExpr | null = null;
if (!ts.isObjectLiteralExpression(optsNode)) {
// A stored options record (`const statSyncOptions = { throwIfNoEntry:
// false }`): the flag is read from it at the call, as Node reads
// options.throwIfNoEntry.
const optsType = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
const shape = optsType?.kind === "record" ? lowerer.shapes.get(optsType.shapeId) : undefined;
const field = shape?.fields.find((f) => f.name === "throwIfNoEntry");
if (
optsType?.kind !== "record" ||
!shape ||
shape.tuple ||
shape.indexValue ||
shape.fields.length !== 1 ||
field?.type.kind !== "bool"
) {
lowerer.noLowering(
`${spelling} with this options value`,
optsNode,
`pass an object literal, or a record whose only member is a boolean throwIfNoEntry: ${spelling}(path, { throwIfNoEntry: false })`,
);
}
const opts = lowerer.lowerExpr(optsNode);
if (opts.type.kind === "record") {
throwIfNoEntry = {
kind: "recordGet",
obj: opts,
shapeId: opts.type.shapeId,
field: "throwIfNoEntry",
type: BOOL,
loc,
};
} else if (opts.type.kind === "dyn") {
// A JavaScript binding held as a dynamic value: Node keeps throwing
// unless the member is exactly false.
throwIfNoEntry = {
kind: "dynScalarEq",
left: {
kind: "dynKeyGet",
key: { kind: "strLit", value: "throwIfNoEntry", type: STRING, loc },
optional: true,
value: opts,
type: DYN,
loc,
},
right: boolLit(false, loc),
negated: true,
type: BOOL,
loc,
};
} else {
lowerer.noLowering(`${spelling} with this options value`, optsNode, "pass an object literal");
}
}
// The members in source order, when a bigint member has an effect: the
// throwIfNoEntry value ("throwIfNoEntry") and each effect.
const members: (IrExpr | "throwIfNoEntry")[] = [];
let bigintEffect = false;
for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties.nodes : []) {
const m = optionMember(p);
if (!m) {
lowerer.noLowering(
`${spelling} with this options shape`,
p,
"spreads and computed keys have no lowering — write each member inline",
);
}
if (m.name === "throwIfNoEntry") {
throwIfNoEntry = lowerer.lowerExprExpecting(m.value, BOOL);
members.push("throwIfNoEntry");
} else if (m.name === "bigint") {
const effect = checkStatBigintOption(lowerer, m.value, spelling, representation);
if (effect !== null) {
members.push(effect);
bigintEffect = true;
}
} else {
fenceOrDropOptionKey(
lowerer,
p,
m.name,
spelling,
FS_STAT_DOCUMENTED_OPTIONS,
"throwIfNoEntry and bigint are the supported options",
);
}
}
// With a bigint member's effect, the path binds first, then the members
// run in source order (a runtime throwIfNoEntry into a local), then the
// stat; without one, the call evaluates its operands in that order itself.
const ordered = (stat: (pathArg: IrExpr, flag: IrExpr | null) => IrExpr): IrExpr => {
if (!bigintEffect) return stat(path, throwIfNoEntry);
const pathLocal = lowerer.declareHiddenLocal("%statPath", STRING);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: pathLocal.id, init: path, loc: path.loc }];
let flag = throwIfNoEntry;
for (const member of members) {
if (member !== "throwIfNoEntry") {
stmts.push({ kind: "exprStmt", expr: member, loc: member.loc });
} else if (flag !== null && flag.kind !== "boolLit") {
const flagLocal = lowerer.declareHiddenLocal("%statThrowIfNoEntry", BOOL);
stmts.push({ kind: "varDecl", localId: flagLocal.id, init: flag, loc: flag.loc });
flag = varRef(flagLocal.id, BOOL, flag.loc);
}
}
const result = stat(varRef(pathLocal.id, STRING, loc), flag);
return { kind: "seqExpr", stmts, result, type: result.type, loc };
};
if (callType?.kind === "stats" || callType?.kind === "bigintStats") {
// A runtime flag that is false answers undefined, which a Stats result
// cannot hold.
if (throwIfNoEntry !== null && !(throwIfNoEntry.kind === "boolLit" && throwIfNoEntry.value)) {
lowerer.noLowering(
`${spelling} with a runtime-valued throwIfNoEntry where the result is typed Stats`,
call,
"the result must be Stats | undefined when throwIfNoEntry may be false",
);
}
if (!bigintEffect) return plainAfter(null);
return ordered((pathArg) => ({
kind: "libCall",
fn: plainFn,
args: [pathArg],
type: statsType,
loc,
}));
}
if (
!def ||
def.arms.length !== 2 ||
!def.arms.some((a) => a.kind === "stats" || a.kind === "bigintStats") ||
!def.arms.some((a) => a.kind === "undefinedT")
) {
lowerer.noLowering(
`${spelling}(path, options) where the result is not Stats or Stats | undefined`,
call,
`${spelling}(path, { throwIfNoEntry: false }) answers Stats | undefined`,
);
}
return ordered((pathArg, flag) => ({
kind: "libCall",
fn: noFollow ? "fs.lstatProbeSync" : "fs.statProbeSync",
args: [pathArg, flag ?? boolLit(true, loc)],
type: callType!,
loc,
}));
}
/** fs.fstatSync(fd, options): the descriptor form of the same snapshot.
* Node documents only `bigint` here — an open descriptor cannot miss, so the
* result is Stats or BigIntStats, never undefined. */
export function lowerFsStatFdOptionsCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
const spelling = "fstatSync";
const fd = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64);
const optsNode = call.arguments.nodes[1]!;
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
const representation = statRepresentation(callType, undefined);
const statsType: IrType = representation === "bigint" ? BIGINT_STATS_T : STATS_T;
const plain = (): IrExpr => ({
kind: "libCall",
fn: "fs.fstatSync",
args: [fd],
type: statsType,
loc,
});
const undefinedOptions = lowerStaticallyUndefinedArgument(lowerer, optsNode);
if (undefinedOptions !== null) {
if (isSafeToDiscard(undefinedOptions)) return plain();
// The descriptor binds first, then the options' effects, then the stat.
const fdLocal = lowerer.declareHiddenLocal("%fstatFd", F64);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: fdLocal.id, init: fd, loc: fd.loc },
{ kind: "exprStmt", expr: undefinedOptions, loc: undefinedOptions.loc },
],
result: {
kind: "libCall",
fn: "fs.fstatSync",
args: [varRef(fdLocal.id, F64, loc)],
type: statsType,
loc,
},
type: statsType,
loc,
};
}
if (!ts.isObjectLiteralExpression(optsNode)) {
lowerer.noLowering(
`${spelling} with this options value`,
optsNode,
`pass an object literal: ${spelling}(fd, { bigint: true })`,
);
}
const effects: IrExpr[] = [];
for (const p of optsNode.properties.nodes) {
const m = optionMember(p);
if (!m) {
lowerer.noLowering(
`${spelling} with this options shape`,
p,
"spreads and computed keys have no lowering — write each member inline",
);
}
if (m.name === "bigint") {
const effect = checkStatBigintOption(lowerer, m.value, spelling, representation);
if (effect !== null) effects.push(effect);
} else {
fenceOrDropOptionKey(
lowerer,
p,
m.name,
spelling,
FS_FSTAT_DOCUMENTED_OPTIONS,
"bigint is the supported option",
);
}
}
if (effects.length === 0) return plain();
// The descriptor binds first, then the bigint member's effect, then the stat.
const fdLocal = lowerer.declareHiddenLocal("%fstatFd", F64);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: fdLocal.id, init: fd, loc: fd.loc },
...effects.map((expr): IrStmt => ({ kind: "exprStmt", expr, loc: expr.loc })),
],
result: {
kind: "libCall",
fn: "fs.fstatSync",
args: [varRef(fdLocal.id, F64, loc)],
type: statsType,
loc,
},
type: statsType,
loc,
};
}
/** `d.isFile()` / `d.isDirectory()` / `d.isSymbolicLink()` on a Dirent-
* typed receiver (the interned record — provenance via the checker's
* Dirent symbol, the StringDecoder pattern): a read of the hidden
* %dtype field compared against libuv's UV_DIRENT code. Node's other
* type probes (isBlockDevice, ...) fence with the supported list. */
export function lowerDirentMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (!lowerer.isStdlibMember(access)) return null;
const recvSym = lowerer.typeOf(access.expression).getSymbol();
if (recvSym === undefined || ts.symbolName(recvSym) !== "Dirent") return null;
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind !== "record") return null;
const shape = lowerer.shapes.get(receiver.type.shapeId);
if (!shape?.fields.some((f) => f.name === "%dtype")) return null;
const name = access.name.text;
const loc = locOf(call);
// libuv's UV_DIRENT encoding (scr_fs_scandir answers it).
const code =
name === "isFile" ? 1 : name === "isDirectory" ? 2 : name === "isSymbolicLink" ? 3 : -1;
if (code < 0) {
lowerer.noLowering(
`Dirent.${name}`,
call,
"name, parentPath, isFile(), isDirectory(), and isSymbolicLink() are the supported Dirent members",
lowerer.checker.getSymbolAtLocation(access.name),
);
}
if (call.arguments.nodes.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call);
const dtype: IrExpr = {
kind: "recordGet",
obj: receiver,
shapeId: receiver.type.shapeId,
field: "%dtype",
type: F64,
loc,
};
return {
kind: "bin",
op: "===",
left: dtype,
right: { kind: "numLit", value: code, type: F64, loc },
type: BOOL,
loc,
};
}