@scriptc/compiler 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +54 -9
  2. package/dist/backend/emission/emit-exprs.js +170 -14
  3. package/dist/backend/emission/emit-exprs.js.map +1 -1
  4. package/dist/backend/emission/emit-walkers.js +29 -2
  5. package/dist/backend/emission/emit-walkers.js.map +1 -1
  6. package/dist/backend/emission/emitter.js +25 -1
  7. package/dist/backend/emission/emitter.js.map +1 -1
  8. package/dist/backend/llvm/dyn.d.ts +1 -0
  9. package/dist/backend/llvm/dyn.js +83 -1
  10. package/dist/backend/llvm/dyn.js.map +1 -1
  11. package/dist/backend/llvm/emitter.js +210 -13
  12. package/dist/backend/llvm/emitter.js.map +1 -1
  13. package/dist/coverage/surface-manifest.js +16 -2
  14. package/dist/coverage/surface-manifest.js.map +1 -1
  15. package/dist/diagnostics/diagnostic.d.ts +41 -14
  16. package/dist/diagnostics/diagnostic.js +79 -27
  17. package/dist/diagnostics/diagnostic.js.map +1 -1
  18. package/dist/diagnostics/render.js +8 -0
  19. package/dist/diagnostics/render.js.map +1 -1
  20. package/dist/frontend/lowering/lib-boundary.js +11 -1
  21. package/dist/frontend/lowering/lib-boundary.js.map +1 -1
  22. package/dist/frontend/lowering/lower-builtins.d.ts +69 -0
  23. package/dist/frontend/lowering/lower-builtins.js +380 -4
  24. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  25. package/dist/frontend/lowering/lower-calls.d.ts +41 -0
  26. package/dist/frontend/lowering/lower-calls.js +559 -11
  27. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  28. package/dist/frontend/lowering/lower-classes.d.ts +7 -0
  29. package/dist/frontend/lowering/lower-classes.js +51 -1
  30. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  31. package/dist/frontend/lowering/lower-containers.d.ts +21 -0
  32. package/dist/frontend/lowering/lower-containers.js +262 -17
  33. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  34. package/dist/frontend/lowering/lower-dgram.js +92 -17
  35. package/dist/frontend/lowering/lower-dgram.js.map +1 -1
  36. package/dist/frontend/lowering/lower-emitter.d.ts +70 -3
  37. package/dist/frontend/lowering/lower-emitter.js +436 -29
  38. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  39. package/dist/frontend/lowering/lower-exprs.d.ts +80 -5
  40. package/dist/frontend/lowering/lower-exprs.js +651 -23
  41. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  42. package/dist/frontend/lowering/lower-modules.d.ts +8 -5
  43. package/dist/frontend/lowering/lower-modules.js +144 -19
  44. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  45. package/dist/frontend/lowering/lower-namespaces.d.ts +39 -11
  46. package/dist/frontend/lowering/lower-namespaces.js +166 -24
  47. package/dist/frontend/lowering/lower-namespaces.js.map +1 -1
  48. package/dist/frontend/lowering/lower-server.js +211 -4
  49. package/dist/frontend/lowering/lower-server.js.map +1 -1
  50. package/dist/frontend/lowering/lower-stmts.d.ts +1 -5
  51. package/dist/frontend/lowering/lower-stmts.js +293 -10
  52. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  53. package/dist/frontend/lowering/lower-stream.js +178 -10
  54. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  55. package/dist/frontend/lowering/lowerer.d.ts +53 -1
  56. package/dist/frontend/lowering/lowerer.js +210 -19
  57. package/dist/frontend/lowering/lowerer.js.map +1 -1
  58. package/dist/frontend/lowering/surfaces.d.ts +44 -0
  59. package/dist/frontend/lowering/surfaces.js +234 -3
  60. package/dist/frontend/lowering/surfaces.js.map +1 -1
  61. package/dist/frontend/npm.d.ts +12 -0
  62. package/dist/frontend/npm.js +30 -0
  63. package/dist/frontend/npm.js.map +1 -1
  64. package/dist/frontend/program.d.ts +25 -0
  65. package/dist/frontend/program.js +144 -135
  66. package/dist/frontend/program.js.map +1 -1
  67. package/dist/frontend/shared.d.ts +1 -1
  68. package/dist/frontend/shared.js +1 -1
  69. package/dist/frontend/shared.js.map +1 -1
  70. package/dist/frontend/types.d.ts +13 -0
  71. package/dist/frontend/types.js +84 -7
  72. package/dist/frontend/types.js.map +1 -1
  73. package/dist/index.d.ts +2 -1
  74. package/dist/index.js +157 -12
  75. package/dist/index.js.map +1 -1
  76. package/dist/ir/nodes.d.ts +163 -13
  77. package/dist/ir/nodes.js +65 -6
  78. package/dist/ir/nodes.js.map +1 -1
  79. package/dist/ir/validate.js +64 -5
  80. package/dist/ir/validate.js.map +1 -1
  81. package/dist/library/fence-eval.d.ts +77 -0
  82. package/dist/library/fence-eval.js +395 -0
  83. package/dist/library/fence-eval.js.map +1 -0
  84. package/dist/library/int-infer.d.ts +119 -0
  85. package/dist/library/int-infer.js +1301 -0
  86. package/dist/library/int-infer.js.map +1 -0
  87. package/dist/library/int-infer.test.d.ts +1 -0
  88. package/dist/library/int-infer.test.js +279 -0
  89. package/dist/library/int-infer.test.js.map +1 -0
  90. package/dist/library/profile.d.ts +43 -6
  91. package/dist/library/profile.js +179 -31
  92. package/dist/library/profile.js.map +1 -1
  93. package/dist/library/sidecar.d.ts +23 -0
  94. package/dist/library/sidecar.js +150 -16
  95. package/dist/library/sidecar.js.map +1 -1
  96. package/package.json +2 -2
  97. package/surface-manifest.json +208 -10
@@ -2,10 +2,16 @@
2
2
  * crypto, child_process spawn/spawnSync and child/stats/spawn-result
3
3
  * methods), JSON.parse/stringify, process properties/methods and
4
4
  * process.env access, and console.log detection. */
5
+ import { readFileSync } from "node:fs";
5
6
  import { builtinModules } from "node:module";
7
+ import { dirname, resolve } from "node:path";
6
8
  import * as ts from "../ts7/adapter.js";
7
- import { dynUndefinedExpr, nodeThrowExpr, own } from "./lowerer.js";
9
+ import { PoisonError, dynUndefinedExpr, ladderFenceExpr, nodeThrowExpr, own } from "./lowerer.js";
8
10
  import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../program.js";
11
+ import { isRelativeSpecifier } from "../shared.js";
12
+ import { probeNodeRequireRefusal } from "../npm.js";
13
+ import { isNpmStaticPackage } from "../npm-static.js";
14
+ import { invalidJsonModuleDiag, requiresDynamicImportDiag } from "../../diagnostics/diagnostic.js";
9
15
  import { builtinModuleFnOf, FS_READDIR_DOCUMENTED_OPTIONS, FS_WATCH_DOCUMENTED_OPTIONS, FS_WRITE_FILE_DOCUMENTED_OPTIONS, QS_PARSE_DOCUMENTED_OPTIONS, QS_STRINGIFY_DOCUMENTED_OPTIONS, READLINE_DOCUMENTED_OPTIONS, builtinConstLit, fenceOrDropOptionKey, isChildSurfaceMember, } from "./surfaces.js";
10
16
  import { conditionalSpreadOf, lowerDynObjectLiteral } from "./lower-exprs.js";
11
17
  import { HTTP2_CONSTANTS } from "./http2-constants.js";
@@ -132,9 +138,17 @@ export function builtinMemberRequireDecl(nameNode, init) {
132
138
  export function builtinNamespaceDestructureModuleOf(L, decl) {
133
139
  if (decl.name === undefined || !ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined)
134
140
  return null;
135
- if (!ts.isIdentifier(decl.initializer))
136
- return null;
137
- const module = L.builtinNamespaceModuleOf(decl.initializer);
141
+ // `const { join } = require("node:path")` through a createRequire
142
+ // binding: the call IS the namespace — same table keying.
143
+ let module = null;
144
+ const init = stripTypeCasts(decl.initializer);
145
+ if (ts.isCallExpression(init)) {
146
+ const cr = createRequireSpecOf(L, init);
147
+ module = cr !== null && cr.spec !== null ? canonicalBuiltinModule(cr.spec) : null;
148
+ }
149
+ else if (ts.isIdentifier(init)) {
150
+ module = L.builtinNamespaceModuleOf(init);
151
+ }
138
152
  if (module === null)
139
153
  return null;
140
154
  for (const el of decl.name.elements) {
@@ -145,6 +159,223 @@ export function builtinNamespaceDestructureModuleOf(L, decl) {
145
159
  }
146
160
  return module;
147
161
  }
162
+ /* ── node:module — createRequire's static erasure ─────────────────────
163
+ * The config/version-reading pattern real CLIs ship:
164
+ * import { createRequire } from "node:module";
165
+ * const require = createRequire(import.meta.url);
166
+ * const pkg = require("../package.json");
167
+ * A compiled program's module graph is fixed at build time, so the
168
+ * indirection ERASES where the required specifier is a static string
169
+ * literal naming something the compiler already handles: a builtin (the
170
+ * binding is a namespace import in const clothing), a relative .json
171
+ * document (the file bakes and parses like JSON.parse of its text), or
172
+ * an installed npm package (the island's require-condition entry under
173
+ * --dynamic). Dynamic specifiers and other targets fence by name. */
174
+ /** Strips the type-only wrappers the require pattern rides in typed
175
+ * code (`require("node:os") as typeof import("node:os")` — the
176
+ * fallback declarations answer `unknown`, so the cast IS the idiom),
177
+ * plus parens, legacy assertions, and non-null suffixes. */
178
+ export function stripTypeCasts(e) {
179
+ let cur = e;
180
+ while (ts.isParenthesizedExpression(cur) || ts.isAsExpression(cur) ||
181
+ ts.isTypeAssertion(cur) || ts.isNonNullExpression(cur)) {
182
+ cur = cur.expression;
183
+ }
184
+ return cur;
185
+ }
186
+ /** The `createRequire(<base>)` call over the node:module import binding
187
+ * with a supported base — import.meta.url, import.meta.filename, or
188
+ * __filename, every spelling of "this file" — or null. The base never
189
+ * LOWERS (import.meta has no value representation): it only names the
190
+ * file whose directory anchors the returned require's relative
191
+ * resolution, and every supported spelling names the call's own file. */
192
+ export function createRequireBaseCallOf(L, expr) {
193
+ const e = stripTypeCasts(expr);
194
+ if (!ts.isCallExpression(e) || e.questionDotToken)
195
+ return null;
196
+ if (!ts.isIdentifier(e.expression))
197
+ return null;
198
+ const bi = builtinImportOf(L, e.expression);
199
+ if (!bi || bi.module !== "module" || bi.member !== "createRequire")
200
+ return null;
201
+ if (e.arguments.length !== 1)
202
+ return null;
203
+ const base = stripTypeCasts(e.arguments[0]);
204
+ if (ts.isIdentifier(base) && base.text === "__filename")
205
+ return e;
206
+ if (ts.isPropertyAccessExpression(base) &&
207
+ !base.questionDotToken &&
208
+ ts.isMetaProperty(base.expression) &&
209
+ base.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
210
+ (base.name.text === "url" || base.name.text === "filename")) {
211
+ return e;
212
+ }
213
+ return null;
214
+ }
215
+ /** True for `const require = createRequire(import.meta.url)` — the
216
+ * binding is compile-time plumbing (each call through it resolves per
217
+ * site) with no storage and no code; both declaration walks skip by
218
+ * this test. A reassignable (let/var) binding never matches — callers
219
+ * gate on constness like the other alias decls. */
220
+ export function createRequireBindingDecl(L, nameNode, init) {
221
+ if (!ts.isIdentifier(nameNode) || init === undefined)
222
+ return false;
223
+ return createRequireBaseCallOf(L, init) !== null;
224
+ }
225
+ /** The declaring source file when `callee` denotes a createRequire-made
226
+ * require: a CONST binding over createRequire(import.meta.url) (the
227
+ * binding's own file anchors resolution) or the inline
228
+ * `createRequire(import.meta.url)(...)` spelling (the call's file).
229
+ * Null off the pattern, so call chains keep trying. */
230
+ export function createRequireCalleeFileOf(L, callee) {
231
+ const e = stripTypeCasts(callee);
232
+ if (ts.isCallExpression(e)) {
233
+ return createRequireBaseCallOf(L, e) !== null ? e.getSourceFile() : null;
234
+ }
235
+ if (!ts.isIdentifier(e))
236
+ return null;
237
+ const symbol = L.checker.getSymbolAtLocation(e);
238
+ const decl = symbol ? L.checker.declarationsOf(symbol)[0] : undefined;
239
+ if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined)
240
+ return null;
241
+ if (!ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0)
242
+ return null;
243
+ return createRequireBaseCallOf(L, decl.initializer) !== null ? decl.getSourceFile() : null;
244
+ }
245
+ /** The static require of `R("spec")` through a createRequire binding:
246
+ * the literal specifier plus the file anchoring relative resolution.
247
+ * Null when the callee is not a createRequire-made require; a matching
248
+ * callee with a non-literal (or missing) specifier answers spec null,
249
+ * so the call lowering fences by name instead of falling through to
250
+ * the generic call paths. */
251
+ export function createRequireSpecOf(L, call) {
252
+ if (call.questionDotToken)
253
+ return null;
254
+ const baseFile = createRequireCalleeFileOf(L, call.expression);
255
+ if (baseFile === null)
256
+ return null;
257
+ if (call.arguments.length !== 1)
258
+ return { spec: null, baseFile };
259
+ const a = call.arguments[0];
260
+ return { spec: ts.isStringLiteralLike(a) ? a.text : null, baseFile };
261
+ }
262
+ /** True for `const fs = require("node:fs")` through a createRequire
263
+ * binding — a builtin namespace import in const clothing: alias
264
+ * plumbing with no storage (uses resolve through
265
+ * builtinNamespaceModuleOf's createRequire arm); both declaration
266
+ * walks skip by this test. */
267
+ export function createRequireNamespaceDecl(L, nameNode, init) {
268
+ if (!ts.isIdentifier(nameNode) || init === undefined)
269
+ return false;
270
+ const call = stripTypeCasts(init);
271
+ if (!ts.isCallExpression(call))
272
+ return false;
273
+ const cr = createRequireSpecOf(L, call);
274
+ return cr !== null && cr.spec !== null && canonicalBuiltinModule(cr.spec) !== null;
275
+ }
276
+ /** `require("spec")` through a createRequire binding — the erasure per
277
+ * target. Builtins are reached here only OUTSIDE the const-namespace-
278
+ * binding shape (that declaration erases; member uses resolve through
279
+ * the namespace tables) and fence toward it. A relative .json document
280
+ * bakes: the file's text validates as JSON at compile time and the
281
+ * call lowers to json.parse over the baked literal — JSON.parse's
282
+ * checked-dynamic `unknown` stance, and exactly Node's value (require
283
+ * of JSON IS JSON.parse of the file; the per-call re-parse forgoes
284
+ * Node's module-cache identity, unobservable without mutation). A bare
285
+ * specifier NOTHING installed resolves compiles to Node's catchable
286
+ * MODULE_NOT_FOUND throw (the optional-dependency try/require
287
+ * pattern); an installed package loads through the island's
288
+ * require-condition entry under --dynamic (collectCreateRequires
289
+ * embedded it) and reports the requires-dynamic diagnostic in a static
290
+ * build. Null when the callee is not a createRequire require. */
291
+ export function lowerCreateRequireCall(L, call, loc) {
292
+ const cr = createRequireSpecOf(L, call);
293
+ if (cr === null)
294
+ return null;
295
+ if (cr.spec === null) {
296
+ L.noLowering("createRequire's require with this argument shape", call, "the compiled module graph is fixed at build time — the one lowered form is require(\"<static string literal>\")");
297
+ }
298
+ const spec = cr.spec;
299
+ if (canonicalBuiltinModule(spec) !== null) {
300
+ L.unsupported("SC1090", call, `module namespace objects as values (bind it first: const m = require("${spec}"), then access members through the binding)`);
301
+ }
302
+ if (spec.startsWith("#")) {
303
+ L.noLowering(`createRequire's require of the '${spec}' project import`, call, "imports-field specifiers have no require lowering yet — import the target statically");
304
+ }
305
+ if (isRelativeSpecifier(spec) || spec.startsWith("/")) {
306
+ if (!spec.endsWith(".json")) {
307
+ L.noLowering(`createRequire's require of '${spec}'`, call, "relative requires lower for .json documents only — a program module is a static import");
308
+ }
309
+ const abs = spec.startsWith("/")
310
+ ? spec
311
+ : resolve(dirname(cr.baseFile.fileName), spec);
312
+ let text = null;
313
+ try {
314
+ text = readFileSync(abs, "utf8");
315
+ }
316
+ catch {
317
+ /* the fence below speaks */
318
+ }
319
+ if (text === null) {
320
+ L.noLowering(`createRequire's require of '${spec}' (no file at ${abs})`, call, "the required document resolves at build time — check the path against the requiring file");
321
+ }
322
+ try {
323
+ JSON.parse(text);
324
+ }
325
+ catch (e) {
326
+ L.pushDiag(invalidJsonModuleDiag(abs, e instanceof Error ? e.message : String(e), loc));
327
+ throw new PoisonError();
328
+ }
329
+ return {
330
+ kind: "libCall",
331
+ fn: "json.parse",
332
+ args: [{ kind: "strLit", value: text, type: STRING, loc }],
333
+ type: DYN,
334
+ loc,
335
+ };
336
+ }
337
+ // Bare package specifiers. --npm-static opt-ins are program modules —
338
+ // their exports bind through static imports, not a require value.
339
+ const pkgName = spec.startsWith("@")
340
+ ? spec.split("/").slice(0, 2).join("/")
341
+ : spec.split("/")[0];
342
+ if (isNpmStaticPackage(pkgName)) {
343
+ L.noLowering(`createRequire's require of the --npm-static package '${pkgName}'`, call, "the opted-in package compiles as program modules — import it statically");
344
+ }
345
+ const refusal = probeNodeRequireRefusal(cr.baseFile.fileName, spec);
346
+ if (refusal !== null) {
347
+ // Node's require-site MODULE_NOT_FOUND, catchable — the compiled
348
+ // expression IS that throw (the typed dummy is abandoned by the
349
+ // pending check's unwind).
350
+ return nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, DYN, loc);
351
+ }
352
+ if (!L.dynamic) {
353
+ L.pushDiag(requiresDynamicImportDiag(pkgName, loc));
354
+ throw new PoisonError();
355
+ }
356
+ const res = L.createRequireImports.get(`${cr.baseFile.fileName}\u0000${spec}`);
357
+ if (res === undefined) {
358
+ // Collection never saw the site (a shape this walk and that walk
359
+ // disagree on) — fence rather than mis-embed.
360
+ L.noLowering(`createRequire's require of '${spec}'`, call, undefined);
361
+ }
362
+ if (res === "")
363
+ throw new PoisonError(); // reported at collection
364
+ // A CJS facade's default IS module.exports (Node's require answer);
365
+ // an ESM-resolved entry answers its namespace (Node's require(esm)).
366
+ const exportName = res.format === "esm" ? "*" : "default";
367
+ return {
368
+ kind: "libCall",
369
+ fn: "island.import",
370
+ args: [
371
+ { kind: "strLit", value: res.entryKey, type: STRING, loc },
372
+ { kind: "strLit", value: exportName, type: STRING, loc },
373
+ { kind: "strLit", value: spec, type: STRING, loc },
374
+ ],
375
+ type: JSVAL,
376
+ loc,
377
+ };
378
+ }
148
379
  /** True when `node` denotes the http2.constants OBJECT — any of its
149
380
  * spellings: `http2.constants` through a namespace/require binding, a
150
381
  * destructured or member-bound `constants` alias from the http2 module,
@@ -371,6 +602,151 @@ export function lowerFsToUnixTimestampCall(L, expr, bi, loc) {
371
602
  }
372
603
  return null;
373
604
  }
605
+ /** The fs validation-ladder spoke (checked-dynamic lane, JS sources
606
+ * only — TypeScript keeps its compile fences): implemented-namespace
607
+ * calls whose misuse Node rejects with typed errors lower to fs.*Chk
608
+ * libCalls that replicate the validation ladder over DOM values and
609
+ * throw Node's exact ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE /
610
+ * ERR_OUT_OF_RANGE — the honest tail (the real operation where one
611
+ * exists, the compiler-rendered SC2020 fence otherwise) runs only
612
+ * after every validation passes, exactly Node's order. Null when this
613
+ * is not a claimed member/shape (the table or fence path stands). */
614
+ export function lowerFsLadderCall(L, expr, bi, loc) {
615
+ if (bi.module !== "fs" && bi.module !== "fs/promises")
616
+ return null;
617
+ if (!isJsSourceFile(expr.getSourceFile()))
618
+ return null;
619
+ const args = expr.arguments;
620
+ if (args.some(ts.isSpreadElement))
621
+ return null;
622
+ // Every ladder argument crosses as a DOM value; an argument that
623
+ // cannot leaves the historical fence in place.
624
+ const dynArg = (node) => {
625
+ if (!node)
626
+ return dynUndefinedExpr(loc);
627
+ const raw = L.lowerExpr(node);
628
+ if (raw.type.kind === "dyn")
629
+ return raw;
630
+ if (raw.kind === "unitLit" || L.dynConvertible(raw.type)) {
631
+ return { kind: "dynFrom", value: raw, type: DYN, loc };
632
+ }
633
+ return null;
634
+ };
635
+ const dynArgs = (nodes) => {
636
+ const out = [];
637
+ for (const n of nodes) {
638
+ const v = dynArg(n);
639
+ if (v === null)
640
+ return null;
641
+ out.push(v);
642
+ }
643
+ return out;
644
+ };
645
+ const resultT = L.mapTypeOf(L.typeOf(expr)) ?? DYN;
646
+ const chk = (fn, chkArgs, type) => ({ kind: "libCall", fn, args: chkArgs, type, loc });
647
+ if (bi.module === "fs/promises") {
648
+ if (bi.member !== "lchmod" || args.length !== 2)
649
+ return null;
650
+ const a = dynArgs([args[0], args[1]]);
651
+ return a && chk("fsp.lchmodChk", a, { kind: "promise", inner: VOID });
652
+ }
653
+ switch (bi.member) {
654
+ case "exists": {
655
+ // The REAL deprecated-API shape: the callback validates
656
+ // synchronously (Node's one throwing arm), invalid paths answer
657
+ // false THROUGH it, and the answer is asynchronous.
658
+ // A provably-non-file `new URL('<literal>')` path (the suite's
659
+ // https://foo probe): Node answers false through the callback
660
+ // synchronously — the DOM has no URL kind, so the path slot
661
+ // carries the unvalidatable token instead (construction of a
662
+ // parseable literal is effect-free; file: URLs keep the fence —
663
+ // they would need the real path conversion).
664
+ let pathNode = args[0];
665
+ let pathExpr = null;
666
+ if (pathNode && ts.isNewExpression(pathNode) && ts.isIdentifier(pathNode.expression) &&
667
+ pathNode.expression.text === "URL" && L.mapTypeOf(L.typeOf(pathNode))?.kind === "url" &&
668
+ pathNode.arguments?.length === 1 && ts.isStringLiteral(pathNode.arguments[0])) {
669
+ let parsed = null;
670
+ try {
671
+ parsed = new URL(pathNode.arguments[0].text);
672
+ }
673
+ catch {
674
+ parsed = null;
675
+ }
676
+ if (parsed === null || parsed.protocol === "file:")
677
+ return null;
678
+ pathExpr = dynUndefinedExpr(loc);
679
+ pathNode = undefined;
680
+ }
681
+ pathExpr ??= dynArg(pathNode);
682
+ const cbExpr = dynArg(args[1]);
683
+ if (pathExpr === null || cbExpr === null)
684
+ return null;
685
+ return chk("fs.existsChk", [pathExpr, cbExpr], DYN);
686
+ }
687
+ case "mkdtemp": {
688
+ // mkdtemp(prefix[, options], callback) — the callback is the
689
+ // LAST argument (makeCallback runs first, then the prefix).
690
+ const a = dynArgs([args[0], args.length >= 2 ? args[args.length - 1] : undefined]);
691
+ return a && chk("fs.mkdtempChk", [...a, ladderFenceExpr(L, "fs.mkdtemp", expr)], resultT);
692
+ }
693
+ case "mkdtempSync": {
694
+ // The table serves the plain string 1-arg form; the ladder takes
695
+ // every other shape — prefix/encoding validation, then the REAL
696
+ // mkdtemp when the options leave utf8 semantics.
697
+ if (args.length === 1 && L.mapTypeOf(L.typeOf(args[0]))?.kind === "string")
698
+ return null;
699
+ if (args.length > 2)
700
+ return null;
701
+ const a = dynArgs([args[0], args[1]]);
702
+ return a && chk("fs.mkdtempSyncChk", [...a, ladderFenceExpr(L, "fs.mkdtempSync with these options", expr)], STRING);
703
+ }
704
+ case "readFile": {
705
+ // readFile(path[, options], callback): callback, assertEncoding,
706
+ // path — then the async read fences.
707
+ const a = dynArgs([args[0], args.length >= 3 ? args[1] : undefined, args.length >= 2 ? args[args.length - 1] : undefined]);
708
+ return a && chk("fs.readFileChk", [...a, ladderFenceExpr(L, "fs.readFile", expr)], resultT);
709
+ }
710
+ case "opendirSync": {
711
+ const a = dynArgs([args[0], args[1]]);
712
+ return a && chk("fs.opendirChk", [...a, ladderFenceExpr(L, "fs.opendirSync", expr)], resultT);
713
+ }
714
+ case "watchFile": {
715
+ // watchFile(filename[, options], listener): the path first, the
716
+ // listener's function contract second; real watching fences.
717
+ const a = dynArgs([args[0], args.length >= 2 ? args[args.length - 1] : undefined]);
718
+ return a && chk("fs.watchFileChk", [...a, ladderFenceExpr(L, "fs.watchFile", expr)], resultT);
719
+ }
720
+ case "lchmod": {
721
+ // lchmod(path, mode, callback): callback, path, mode — macOS
722
+ // shapes (non-APPLE answers Node's not-a-function TypeError).
723
+ const a = dynArgs([args[0], args[1], args[2]]);
724
+ return a && chk("fs.lchmodChk", [...a, ladderFenceExpr(L, "fs.lchmod", expr)], resultT);
725
+ }
726
+ case "lchmodSync": {
727
+ if (args.length > 2)
728
+ return null;
729
+ const a = dynArgs([args[0], args[1]]);
730
+ return a && chk("fs.lchmodSyncChk", a, DYN);
731
+ }
732
+ case "read": {
733
+ // read(fd, buffer, offset, length, position, callback) — the
734
+ // positional form's full ladder; options-object forms keep the
735
+ // fence (their misuse arms are not in the target set).
736
+ if (args.length < 4)
737
+ return null;
738
+ const a = dynArgs([args[0], args[1], args[2], args[3], args.length >= 6 ? args[4] : undefined]);
739
+ return a && chk("fs.readChk", [...a, ladderFenceExpr(L, "fs.read", expr)], resultT);
740
+ }
741
+ case "createReadStream":
742
+ case "createWriteStream": {
743
+ const a = dynArgs([args[0], args[1]]);
744
+ return a && chk("fs.streamOptsChk", [...a, ladderFenceExpr(L, `fs.${bi.member}`, expr)], resultT);
745
+ }
746
+ default:
747
+ return null;
748
+ }
749
+ }
374
750
  export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
375
751
  const name = expr.expression.getText();
376
752
  if (bi.module === "child_process" && bi.member === "spawnSync") {