@scriptc/compiler 0.0.23 → 0.0.24

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 (65) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +100 -11
  2. package/dist/backend/cc.d.ts +13 -0
  3. package/dist/backend/cc.js +11 -0
  4. package/dist/backend/cc.js.map +1 -1
  5. package/dist/backend/emission/emit-async.d.ts +4 -0
  6. package/dist/backend/emission/emit-async.js +31 -1
  7. package/dist/backend/emission/emit-async.js.map +1 -1
  8. package/dist/backend/emission/emit-exprs.js +147 -14
  9. package/dist/backend/emission/emit-exprs.js.map +1 -1
  10. package/dist/backend/emission/emit-shapes.js +1 -0
  11. package/dist/backend/emission/emit-shapes.js.map +1 -1
  12. package/dist/backend/emission/emit-stmts.js +24 -5
  13. package/dist/backend/emission/emit-stmts.js.map +1 -1
  14. package/dist/backend/emission/emit-types.js +10 -0
  15. package/dist/backend/emission/emit-types.js.map +1 -1
  16. package/dist/backend/emission/emitter.d.ts +10 -2
  17. package/dist/backend/emission/emitter.js +49 -30
  18. package/dist/backend/emission/emitter.js.map +1 -1
  19. package/dist/backend/llvm/emitter.js +284 -25
  20. package/dist/backend/llvm/emitter.js.map +1 -1
  21. package/dist/backend/llvm/shapes.js +9 -1
  22. package/dist/backend/llvm/shapes.js.map +1 -1
  23. package/dist/backend/mangle.d.ts +3 -0
  24. package/dist/backend/mangle.js +5 -0
  25. package/dist/backend/mangle.js.map +1 -1
  26. package/dist/frontend/lowering/lower-builtins.d.ts +14 -9
  27. package/dist/frontend/lowering/lower-builtins.js +737 -62
  28. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  29. package/dist/frontend/lowering/lower-calls.js +2 -1
  30. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  31. package/dist/frontend/lowering/lower-containers.d.ts +2 -1
  32. package/dist/frontend/lowering/lower-containers.js +99 -32
  33. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  34. package/dist/frontend/lowering/lower-exprs.js +53 -2
  35. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  36. package/dist/frontend/lowering/lower-inspect.js +59 -1
  37. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  38. package/dist/frontend/lowering/lower-server.d.ts +5 -0
  39. package/dist/frontend/lowering/lower-server.js +1 -1
  40. package/dist/frontend/lowering/lower-server.js.map +1 -1
  41. package/dist/frontend/lowering/lower-stmts.d.ts +6 -2
  42. package/dist/frontend/lowering/lower-stmts.js +45 -10
  43. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  44. package/dist/frontend/lowering/lowerer.d.ts +2 -2
  45. package/dist/frontend/lowering/lowerer.js +11 -6
  46. package/dist/frontend/lowering/lowerer.js.map +1 -1
  47. package/dist/frontend/lowering/surfaces.js +65 -13
  48. package/dist/frontend/lowering/surfaces.js.map +1 -1
  49. package/dist/frontend/types.d.ts +3 -0
  50. package/dist/frontend/types.js +78 -0
  51. package/dist/frontend/types.js.map +1 -1
  52. package/dist/index.js +8 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/ir/integer-loops.d.ts +23 -0
  55. package/dist/ir/integer-loops.js +82 -0
  56. package/dist/ir/integer-loops.js.map +1 -0
  57. package/dist/ir/nodes.d.ts +84 -21
  58. package/dist/ir/nodes.js +84 -1
  59. package/dist/ir/nodes.js.map +1 -1
  60. package/dist/ir/validate.js +92 -18
  61. package/dist/ir/validate.js.map +1 -1
  62. package/dist/library/fence-eval.js +4 -3
  63. package/dist/library/fence-eval.js.map +1 -1
  64. package/package.json +2 -2
  65. package/surface-manifest.json +50 -2
@@ -13,12 +13,72 @@ import { probeNodeRequireRefusal } from "../npm.js";
13
13
  import { isNpmStaticPackage } from "../npm-static.js";
14
14
  import { invalidJsonModuleDiag, requiresDynamicImportDiag } from "../../diagnostics/diagnostic.js";
15
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";
16
- import { conditionalSpreadOf, lowerDynObjectLiteral } from "./lower-exprs.js";
16
+ import { conditionalSpreadOf, droppableStatic, lowerDynObjectLiteral } from "./lower-exprs.js";
17
17
  import { HTTP2_CONSTANTS } from "./http2-constants.js";
18
18
  import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "./crypto-tables.js";
19
19
  import { timerStyleCallback } from "./lower-calls.js";
20
- import { registerHttpClientFnBinding } from "./lower-server.js";
21
- import { BOOL, BYTES_U8, CHILD_T, CHILDSTREAM_T, DYN, F64, FSWATCHER_T, PROCSTREAM_T, JSVAL, NULL_T, SEARCH_PARAMS_T, SPAWNRES_T, STRING, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, funcOf, isUnitType, typeEquals, typeKey } from "../../ir/nodes.js";
20
+ import { registerHttpClientFnBinding, voidizedCallback } from "./lower-server.js";
21
+ import { BOOL, BYTES_U8, CHILD_T, CHILDSTREAM_T, DYN, F64, FILEHANDLE_T, FSWATCHER_T, PROCSTREAM_T, JSVAL, NULL_T, SEARCH_PARAMS_T, SPAWNRES_T, STRING, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, funcOf, isUnitType, typeEquals, typeKey } from "../../ir/nodes.js";
22
+ /** Lower an optional builtin argument whose checker type is statically
23
+ * undefined/void. The returned expression exists only to preserve effects;
24
+ * callers replace its value with the API default. */
25
+ function lowerStaticallyUndefinedBuiltinArg(L, node) {
26
+ const peel = (value) => {
27
+ let expr = value;
28
+ while (ts.isParenthesizedExpression(expr) ||
29
+ ts.isAsExpression(expr) ||
30
+ ts.isTypeAssertion(expr) ||
31
+ ts.isSatisfiesExpression(expr)) {
32
+ expr = expr.expression;
33
+ }
34
+ return expr;
35
+ };
36
+ let expr = node;
37
+ while (ts.isParenthesizedExpression(expr))
38
+ expr = expr.expression;
39
+ if ((L.typeOf(expr).flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0)
40
+ return null;
41
+ expr = peel(expr);
42
+ let sawVoid = false;
43
+ while (ts.isVoidExpression(expr)) {
44
+ sawVoid = true;
45
+ expr = peel(expr.expression);
46
+ }
47
+ return L.lowerExpr(sawVoid ? expr : node);
48
+ }
49
+ function defaultAfterUndefined(value, dflt) {
50
+ if (droppableStatic(value))
51
+ return dflt;
52
+ return {
53
+ kind: "seqExpr",
54
+ stmts: [{ kind: "exprStmt", expr: value, loc: value.loc }],
55
+ result: dflt,
56
+ type: dflt.type,
57
+ loc: value.loc,
58
+ };
59
+ }
60
+ /** Complete an optional builtin argument. Statically undefined spellings
61
+ * preserve their effects and select the default; a runtime unit-armed union
62
+ * uses nullish selection when every non-unit arm is the expected type. */
63
+ function lowerBuiltinOptionalDefault(L, node, expected, dflt, nullIsDefault = false) {
64
+ const undefinedArg = lowerStaticallyUndefinedBuiltinArg(L, node);
65
+ if (undefinedArg)
66
+ return defaultAfterUndefined(undefinedArg, dflt);
67
+ if (nullIsDefault && (L.typeOf(node).flags & ts.TypeFlags.Null) !== 0) {
68
+ return defaultAfterUndefined(L.lowerExpr(node), dflt);
69
+ }
70
+ const value = L.lowerExpr(node);
71
+ if (value.type.kind === "union") {
72
+ const def = L.unions.get(value.type.unionId);
73
+ const allowed = def?.arms.every((arm) => typeEquals(arm, expected) ||
74
+ arm.kind === "undefinedT" ||
75
+ (nullIsDefault && arm.kind === "nullT"));
76
+ if (allowed && def.arms.some((arm) => isUnitType(arm))) {
77
+ return { kind: "nullish", left: value, right: dflt, type: expected, loc: value.loc };
78
+ }
79
+ }
80
+ return L.coerceInto(node, value, expected);
81
+ }
22
82
  /** Resolves an identifier to a supported builtin-module IMPORT BINDING:
23
83
  * a named import (through its alias, so `import { join as j }` matches)
24
84
  * whose declaration is an ImportSpecifier under a supported builtin
@@ -785,6 +845,82 @@ export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
785
845
  if (bi.module === "fs" && bi.member === "watch") {
786
846
  return lowerFsWatchCall(L, expr, loc);
787
847
  }
848
+ // fs/promises.open(path[, flags[, mode]]) — string flags and numeric
849
+ // creation mode, with Node's "r"/0o666 defaults. The runtime wraps
850
+ // the descriptor in a shared FileHandle and settles/rejects exactly
851
+ // like the existing fs/promises operations.
852
+ if (bi.module === "fs/promises" && bi.member === "open") {
853
+ if (expr.arguments.length < 1 || expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) {
854
+ L.noLowering(`fs.promises.open with ${expr.arguments.length} arguments`, expr, "use open(path[, stringFlags[, numericMode]])");
855
+ }
856
+ const path = L.lowerExprExpecting(expr.arguments[0], STRING);
857
+ const defaultFlags = { kind: "strLit", value: "r", type: STRING, loc };
858
+ const defaultMode = { kind: "numLit", value: 0o666, type: F64, loc };
859
+ const flags = expr.arguments[1]
860
+ ? lowerBuiltinOptionalDefault(L, expr.arguments[1], STRING, defaultFlags)
861
+ : defaultFlags;
862
+ const mode = expr.arguments[2]
863
+ ? lowerBuiltinOptionalDefault(L, expr.arguments[2], F64, defaultMode)
864
+ : defaultMode;
865
+ return {
866
+ kind: "libCall",
867
+ fn: "fsp.open",
868
+ args: [path, flags, mode],
869
+ type: { kind: "promise", inner: FILEHANDLE_T },
870
+ loc,
871
+ };
872
+ }
873
+ // fs.rename(oldPath, newPath, callback): the callback is a
874
+ // program-shaped closure (zero parameters are valid; the ordinary
875
+ // form receives NodeJS.ErrnoException | null). Keep it typed here so
876
+ // the backends can emit the same union-building adapter used by
877
+ // dns.lookup instead of losing the Error arm through a dyn boundary.
878
+ if (bi.module === "fs" && bi.member === "rename") {
879
+ if (expr.arguments.length !== 3 || expr.arguments.some(ts.isSpreadElement)) {
880
+ L.noLowering(`rename with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, expr, "the supported form is rename(oldPath, newPath, callback)");
881
+ }
882
+ const oldPath = L.lowerExprExpecting(expr.arguments[0], STRING);
883
+ const newPath = L.lowerExprExpecting(expr.arguments[1], STRING);
884
+ let callback = L.lowerExpr(expr.arguments[2]);
885
+ // JS/checkJs callback values may arrive as checked-dynamic callables.
886
+ // Adapt them to the one error-first slot; the runtime passes a dyn
887
+ // Error on failure and null on success through the emitted thunk.
888
+ if (callback.type.kind === "dyn") {
889
+ callback = {
890
+ kind: "dynCheck",
891
+ value: callback,
892
+ type: funcOf([DYN], VOID),
893
+ loc: locOf(expr.arguments[2]),
894
+ };
895
+ }
896
+ let callbackOk = callback.type.kind === "func" && callback.type.params.length <= 1;
897
+ if (callbackOk && callback.type.kind === "func" && callback.type.params.length === 1) {
898
+ const param = callback.type.params[0];
899
+ if (param.kind === "dyn") {
900
+ callbackOk = true;
901
+ }
902
+ else if (param.kind === "union") {
903
+ const def = L.unions.get(param.unionId);
904
+ callbackOk = !!def &&
905
+ def.arms.some((a) => a.kind === "nullT") &&
906
+ def.arms.some((a) => a.kind === "object" && a.className === "%Error") &&
907
+ def.arms.every((a) => a.kind === "nullT" || a.kind === "undefinedT" ||
908
+ (a.kind === "object" && a.className === "%Error"));
909
+ }
910
+ else {
911
+ callbackOk = false;
912
+ }
913
+ }
914
+ if (!callbackOk) {
915
+ L.unsupported("SC1090", expr.arguments[2], "fs.rename callbacks must accept at most one Error | null parameter");
916
+ }
917
+ // TypeScript deliberately permits a value-returning function where a
918
+ // void callback is expected; Node ignores that value. Normalize the
919
+ // closure to the runtime's void callback ABI after validating its
920
+ // error-first parameter shape.
921
+ callback = voidizedCallback(L, callback, locOf(expr.arguments[2]));
922
+ return { kind: "libCall", fn: "fs.renameCb", args: [oldPath, newPath, callback], type: VOID, loc };
923
+ }
788
924
  // fs.readSync(fd, buffer, offset, length[, position]) — normalize the
789
925
  // current-offset forms to Node/libuv's -1 sentinel so the IR and native
790
926
  // ABI stay fixed-width. A real numeric position is a positioned read
@@ -820,6 +956,80 @@ export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
820
956
  }
821
957
  return { kind: "libCall", fn: "fs.readSync", args, type: F64, loc };
822
958
  }
959
+ // fs.writeSync has two static families. Buffer writes use the classic
960
+ // (fd, buffer, offset, length[, position]) shape; string writes accept
961
+ // (fd, string[, position[, "utf8"]]). The runtime uses -1 for the
962
+ // current-offset forms. Node treats a negative, fractional, non-finite,
963
+ // or over-MAX_SAFE numeric write position like null rather than throwing;
964
+ // preserve the value here so the runtime can make that dispatch.
965
+ if (bi.module === "fs" && bi.member === "writeSync") {
966
+ const supported = 'use writeSync(fd, buffer, offset, length[, position]) or writeSync(fd, string[, position[, "utf8"]]); options objects, bigint positions, and other encodings have no lowering';
967
+ if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2) {
968
+ L.noLowering(`writeSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, expr, supported);
969
+ }
970
+ const dataNode = expr.arguments[1];
971
+ const dataType = L.mapTypeOf(L.typeOf(dataNode));
972
+ const fd = L.lowerExprExpecting(expr.arguments[0], F64);
973
+ const position = (node) => {
974
+ let valueNode = node;
975
+ while (valueNode && ts.isParenthesizedExpression(valueNode))
976
+ valueNode = valueNode.expression;
977
+ if (node === undefined || valueNode.kind === ts.SyntaxKind.NullKeyword) {
978
+ return { kind: "numLit", value: -1, type: F64, loc };
979
+ }
980
+ const t = L.mapTypeOf(L.typeOf(node));
981
+ if (t?.kind !== "f64") {
982
+ L.noLowering(`writeSync with a '${t ? L.fmt(t) : L.checker.typeToString(L.typeOf(node))}' position`, node, supported);
983
+ }
984
+ return L.lowerExprExpecting(node, F64);
985
+ };
986
+ if (dataType?.kind === "bytes") {
987
+ if (dataType.elem !== "u8") {
988
+ L.noLowering(`writeSync of '${L.fmt(dataType)}' data`, dataNode, "byte writes take Uint8Array/Buffer data");
989
+ }
990
+ if (expr.arguments.length !== 4 && expr.arguments.length !== 5) {
991
+ L.noLowering(`writeSync of Buffer data with ${expr.arguments.length} arguments`, expr, supported);
992
+ }
993
+ return {
994
+ kind: "libCall",
995
+ fn: "fs.writeSync",
996
+ args: [
997
+ fd,
998
+ L.lowerExprExpecting(dataNode, BYTES_U8),
999
+ L.lowerExprExpecting(expr.arguments[2], F64),
1000
+ L.lowerExprExpecting(expr.arguments[3], F64),
1001
+ position(expr.arguments[4]),
1002
+ ],
1003
+ type: F64,
1004
+ loc,
1005
+ };
1006
+ }
1007
+ if (dataType?.kind === "string") {
1008
+ if (expr.arguments.length > 4) {
1009
+ L.noLowering(`writeSync of string data with ${expr.arguments.length} arguments`, expr, supported);
1010
+ }
1011
+ const encNode = expr.arguments[3];
1012
+ let enc = { kind: "strLit", value: "utf8", type: STRING, loc };
1013
+ if (encNode !== undefined) {
1014
+ const encType = L.typeOf(encNode);
1015
+ if (!encType.isStringLiteralType() || (encType.value !== "utf8" && encType.value !== "utf-8")) {
1016
+ L.noLowering("writeSync with a non-utf8 encoding", encNode, supported);
1017
+ }
1018
+ // Even though utf8 is the runtime's only encoding, the argument
1019
+ // remains an ordinary JS argument: evaluate it in source order so
1020
+ // a call/getter whose type is the accepted literal cannot vanish.
1021
+ enc = L.lowerExprExpecting(encNode, STRING);
1022
+ }
1023
+ return {
1024
+ kind: "libCall",
1025
+ fn: "fs.writeStrSync",
1026
+ args: [fd, L.lowerExprExpecting(dataNode, STRING), position(expr.arguments[2]), enc],
1027
+ type: F64,
1028
+ loc,
1029
+ };
1030
+ }
1031
+ L.noLowering(`writeSync of '${dataType ? L.fmt(dataType) : L.checker.typeToString(L.typeOf(dataNode))}' data`, dataNode, supported);
1032
+ }
823
1033
  if (bi.module === "child_process" &&
824
1034
  (bi.member === "execFileSync" || bi.member === "execSync")) {
825
1035
  return L.lowerExecSyncCall(expr, bi.member === "execSync", loc);
@@ -1186,14 +1396,54 @@ export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
1186
1396
  return { kind: "libCall", fn: "fs.writeFileSyncBytes", args: [path, data], type: VOID, loc };
1187
1397
  }
1188
1398
  }
1189
- // writeFileSync(p, data, options): the lowered options are a literal
1399
+ // writeFileSync(p, data, options) / fs.promises.writeFile(p, data,
1400
+ // options): the lowered options are a literal
1190
1401
  // `{ mode?: <number>, encoding?: "utf8" }` — the mode is open(2)'s
1191
1402
  // O_CREAT argument (creation only; an existing file keeps its
1192
1403
  // permissions, exactly Node), and the encoding may only spell the
1193
- // utf8 the runtime writes anyway. String data only — the Buffer form
1194
- // never carries options in the supported corpus.
1195
- if (bi.module === "fs" && bi.member === "writeFileSync" && expr.arguments.length === 3) {
1404
+ // utf8 the runtime writes anyway. String data only — Buffer options
1405
+ // remain outside the static surface.
1406
+ const syncWriteOptions = bi.module === "fs" && bi.member === "writeFileSync";
1407
+ const promiseWriteOptions = bi.module === "fs/promises" && bi.member === "writeFile";
1408
+ if ((syncWriteOptions || promiseWriteOptions) && expr.arguments.length === 3) {
1196
1409
  const optsNode = expr.arguments[2];
1410
+ const operation = promiseWriteOptions ? "fs.promises.writeFile" : "writeFileSync";
1411
+ const plainFn = promiseWriteOptions ? "fsp.writeFile" : "fs.writeFileSync";
1412
+ const modeFn = promiseWriteOptions ? "fsp.writeFileMode" : "fs.writeFileModeSync";
1413
+ const resultType = promiseWriteOptions ? { kind: "promise", inner: VOID } : VOID;
1414
+ // The runtime needs only `mode`, but every source option value is an
1415
+ // ordinary JS expression. Stage path/data and then evaluate the option
1416
+ // values in object-literal order before issuing the write; otherwise a
1417
+ // call/getter statically typed as the accepted utf8 literal can vanish.
1418
+ const finishWrite = (optionValues) => {
1419
+ const path = L.lowerExprExpecting(expr.arguments[0], STRING);
1420
+ const data = L.lowerExprExpecting(expr.arguments[1], STRING);
1421
+ const pathLocal = L.declareHiddenLocal("%writePath", STRING);
1422
+ const dataLocal = L.declareHiddenLocal("%writeData", STRING);
1423
+ const ref = (local) => ({ kind: "varRef", localId: local.id, type: local.type, loc });
1424
+ const stmts = [
1425
+ { kind: "varDecl", localId: pathLocal.id, init: path, loc: path.loc },
1426
+ { kind: "varDecl", localId: dataLocal.id, init: data, loc: data.loc },
1427
+ ];
1428
+ let mode = null;
1429
+ for (const option of optionValues) {
1430
+ if (option.kind === "effect") {
1431
+ stmts.push({ kind: "exprStmt", expr: option.value, loc: option.value.loc });
1432
+ continue;
1433
+ }
1434
+ const modeLocal = L.declareHiddenLocal("%writeMode", F64);
1435
+ stmts.push({ kind: "varDecl", localId: modeLocal.id, init: option.value, loc: option.value.loc });
1436
+ mode = ref(modeLocal);
1437
+ }
1438
+ const result = {
1439
+ kind: "libCall",
1440
+ fn: mode ? modeFn : plainFn,
1441
+ args: mode ? [ref(pathLocal), ref(dataLocal), mode] : [ref(pathLocal), ref(dataLocal)],
1442
+ type: resultType,
1443
+ loc,
1444
+ };
1445
+ return { kind: "seqExpr", stmts, result, type: resultType, loc };
1446
+ };
1197
1447
  // The bare-encoding spelling — writeFileSync(p, data, "utf-8") — is
1198
1448
  // the options record's encoding key alone: utf8 is what the runtime
1199
1449
  // writes anyway, so string data takes the plain write. Any OTHER
@@ -1202,12 +1452,10 @@ export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
1202
1452
  const t = L.typeOf(optsNode);
1203
1453
  if (t.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8") &&
1204
1454
  L.mapTypeOf(L.typeOf(expr.arguments[1]))?.kind === "string") {
1205
- const path = L.lowerExprExpecting(expr.arguments[0], STRING);
1206
- const data = L.lowerExprExpecting(expr.arguments[1], STRING);
1207
- return { kind: "libCall", fn: "fs.writeFileSync", args: [path, data], type: VOID, loc };
1455
+ return finishWrite([{ kind: "effect", value: L.lowerExprExpecting(optsNode, STRING) }]);
1208
1456
  }
1209
1457
  }
1210
- let modeNode = null;
1458
+ const optionValues = [];
1211
1459
  let ok = ts.isObjectLiteralExpression(optsNode);
1212
1460
  if (ok) {
1213
1461
  for (const p of optsNode.properties) {
@@ -1217,7 +1465,7 @@ export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
1217
1465
  break;
1218
1466
  }
1219
1467
  if (m.name === "mode") {
1220
- modeNode = m.value;
1468
+ optionValues.push({ kind: "mode", value: L.lowerExprExpecting(m.value, F64) });
1221
1469
  }
1222
1470
  else if (m.name === "encoding") {
1223
1471
  const t = L.typeOf(m.value);
@@ -1225,30 +1473,24 @@ export function lowerBuiltinModuleCall(L, expr, bi, fn, loc) {
1225
1473
  ok = false;
1226
1474
  break;
1227
1475
  }
1476
+ optionValues.push({ kind: "effect", value: L.lowerExprExpecting(m.value, STRING) });
1228
1477
  }
1229
1478
  else if (m.name === "flag") {
1230
1479
  // Documented, behavior-changing (open(2)'s disposition — 'a'
1231
1480
  // IS appendFileSync), no lowering: fence by name.
1232
- L.noLowering("writeFileSync with the flag option", p, "the write truncates-or-creates (Node's default 'w') — appendFileSync is the lowered append; other flags have no lowering");
1481
+ L.noLowering(`${operation} with the flag option`, p, "the write truncates-or-creates (Node's default 'w'); other flags have no lowering");
1233
1482
  }
1234
1483
  else {
1235
1484
  // The options-record stance: documented keys with no lowering
1236
1485
  // fence by name; undocumented keys drop like Node.
1237
- fenceOrDropOptionKey(L, p, m.name, "writeFileSync", FS_WRITE_FILE_DOCUMENTED_OPTIONS, 'the supported options are { mode: <number>, encoding: "utf8" }');
1486
+ fenceOrDropOptionKey(L, p, m.name, operation, FS_WRITE_FILE_DOCUMENTED_OPTIONS, 'the supported options are { mode: <number>, encoding: "utf8" }');
1238
1487
  }
1239
1488
  }
1240
1489
  }
1241
1490
  if (!ok || L.mapTypeOf(L.typeOf(expr.arguments[1]))?.kind !== "string") {
1242
- L.noLowering("fs.writeFileSync with 3 arguments", optsNode, 'the supported options are { mode: <number>, encoding: "utf8" } over string data');
1243
- }
1244
- const path = L.lowerExprExpecting(expr.arguments[0], STRING);
1245
- const data = L.lowerExprExpecting(expr.arguments[1], STRING);
1246
- if (modeNode === null) {
1247
- // encoding-only options change nothing: the plain write.
1248
- return { kind: "libCall", fn: "fs.writeFileSync", args: [path, data], type: VOID, loc };
1491
+ L.noLowering(`${operation} with 3 arguments`, optsNode, 'the supported options are { mode: <number>, encoding: "utf8" } over string data');
1249
1492
  }
1250
- const mode = L.lowerExprExpecting(modeNode, F64);
1251
- return { kind: "libCall", fn: "fs.writeFileModeSync", args: [path, data, mode], type: VOID, loc };
1493
+ return finishWrite(optionValues);
1252
1494
  }
1253
1495
  // zlib takes Buffers; a string argument (the lib admits it) gets the
1254
1496
  // wrap-it-first hint instead of a generic type mismatch.
@@ -3707,6 +3949,211 @@ function buildSpForEachFn(name, arity, fnRet, loc) {
3707
3949
  loc,
3708
3950
  };
3709
3951
  }
3952
+ /** Calls on an fs/promises FileHandle. The promise-returning methods run
3953
+ * through the same synchronous descriptor primitives as fs.readSync /
3954
+ * writeSync, then settle so failures reject at await. read/write retain
3955
+ * the caller's buffer in the Node-shaped result record. */
3956
+ export function lowerFileHandleMethodCall(L, call, access) {
3957
+ if (call.questionDotToken || access.questionDotToken)
3958
+ return null;
3959
+ if (L.mapTypeOf(L.typeOf(access.expression))?.kind !== "fileHandle")
3960
+ return null;
3961
+ if (!L.isStdlibMember(access))
3962
+ return null;
3963
+ const name = access.name.text;
3964
+ const loc = locOf(call);
3965
+ const receiver = () => L.lowerExprExpecting(access.expression, FILEHANDLE_T);
3966
+ const promise = (inner) => ({ kind: "promise", inner });
3967
+ const bool = (value) => ({ kind: "boolLit", value, type: BOOL, loc });
3968
+ const num = (node, dflt) => {
3969
+ const defaultValue = { kind: "numLit", value: dflt, type: F64, loc };
3970
+ if (!node)
3971
+ return { value: defaultValue, defaulted: bool(true) };
3972
+ const undefinedArg = lowerStaticallyUndefinedBuiltinArg(L, node);
3973
+ if (undefinedArg) {
3974
+ return { value: defaultAfterUndefined(undefinedArg, defaultValue), defaulted: bool(true) };
3975
+ }
3976
+ if ((L.typeOf(node).flags & ts.TypeFlags.Null) !== 0) {
3977
+ return { value: defaultAfterUndefined(L.lowerExpr(node), defaultValue), defaulted: bool(true) };
3978
+ }
3979
+ const value = L.lowerExpr(node);
3980
+ if (value.type.kind === "union") {
3981
+ const def = L.unions.get(value.type.unionId);
3982
+ const units = def?.arms.filter(isUnitType) ?? [];
3983
+ if (units.length > 0 &&
3984
+ def?.arms.every((arm) => typeEquals(arm, F64) || isUnitType(arm))) {
3985
+ // The normalized number and the separate "length was omitted"
3986
+ // bit must observe one evaluation of a runtime optional union.
3987
+ // Bind it in the numeric argument; the later bool argument reads
3988
+ // the same hidden local (libCall arguments evaluate left-to-right).
3989
+ const saved = L.declareHiddenLocal("%fhOptNum", value.type);
3990
+ const savedRef = () => ({
3991
+ kind: "varRef", localId: saved.id, type: value.type, loc: value.loc,
3992
+ });
3993
+ let defaulted = {
3994
+ kind: "unionIsTag", unionId: value.type.unionId,
3995
+ tag: L.armTag(value.type.unionId, units[0]), negated: false,
3996
+ value: savedRef(), type: BOOL, loc: value.loc,
3997
+ };
3998
+ for (const unit of units.slice(1)) {
3999
+ defaulted = {
4000
+ kind: "logical", op: "||", left: defaulted,
4001
+ right: {
4002
+ kind: "unionIsTag", unionId: value.type.unionId,
4003
+ tag: L.armTag(value.type.unionId, unit), negated: false,
4004
+ value: savedRef(), type: BOOL, loc: value.loc,
4005
+ },
4006
+ type: BOOL,
4007
+ loc: value.loc,
4008
+ };
4009
+ }
4010
+ return {
4011
+ value: {
4012
+ kind: "seqExpr",
4013
+ stmts: [{ kind: "varDecl", localId: saved.id, init: value, loc: value.loc }],
4014
+ result: {
4015
+ kind: "nullish", left: savedRef(), right: defaultValue,
4016
+ type: F64, loc: value.loc,
4017
+ },
4018
+ type: F64,
4019
+ loc: value.loc,
4020
+ },
4021
+ defaulted,
4022
+ };
4023
+ }
4024
+ }
4025
+ return { value: L.coerceInto(node, value, F64), defaulted: bool(false) };
4026
+ };
4027
+ const utf8 = (node) => {
4028
+ const dflt = { kind: "strLit", value: "utf8", type: STRING, loc };
4029
+ if (!node)
4030
+ return dflt;
4031
+ const nodeType = L.typeOf(node);
4032
+ const parts = nodeType.isUnionType() ? nodeType.getTypes() : [nodeType];
4033
+ const supported = parts.every((t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null)) !== 0 ||
4034
+ (t.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8")));
4035
+ if (!supported) {
4036
+ L.noLowering(`FileHandle.${name} with a non-utf8 encoding`, node, "only utf8 data is supported");
4037
+ }
4038
+ return lowerBuiltinOptionalDefault(L, node, STRING, dflt, true);
4039
+ };
4040
+ if (name === "close" || name === "stat") {
4041
+ if (call.arguments.length !== 0)
4042
+ L.noLowering(`FileHandle.${name} with arguments`, call);
4043
+ return {
4044
+ kind: "libCall",
4045
+ fn: name === "close" ? "fileHandle.close" : "fileHandle.stat",
4046
+ args: [receiver()],
4047
+ type: promise(name === "close" ? VOID : { kind: "stats" }),
4048
+ loc,
4049
+ };
4050
+ }
4051
+ if (name === "readFile") {
4052
+ if (call.arguments.length > 1) {
4053
+ L.noLowering(`FileHandle.readFile with ${call.arguments.length} arguments`, call);
4054
+ }
4055
+ const type = L.mapTypeOf(L.typeOf(call));
4056
+ if (type?.kind !== "promise")
4057
+ L.badType(call, L.typeOf(call));
4058
+ const encoding = utf8(call.arguments[0]);
4059
+ if (typeEquals(type.inner, BYTES_U8)) {
4060
+ return {
4061
+ kind: "libCall", fn: "fileHandle.readFileBytes", args: [receiver(), encoding],
4062
+ type, loc,
4063
+ };
4064
+ }
4065
+ if (!typeEquals(type.inner, STRING))
4066
+ L.badType(call, L.typeOf(call));
4067
+ return {
4068
+ kind: "libCall", fn: "fileHandle.readFile", args: [receiver(), encoding],
4069
+ type, loc,
4070
+ };
4071
+ }
4072
+ if (name === "writeFile" || name === "appendFile") {
4073
+ if (call.arguments.length < 1 || call.arguments.length > 2) {
4074
+ L.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call);
4075
+ }
4076
+ const dataNode = call.arguments[0];
4077
+ const dataT = L.mapTypeOf(L.typeOf(dataNode));
4078
+ // Evaluate a supplied utf8 encoding even for Buffer data, matching
4079
+ // Node's argument order; it does not affect the bytes.
4080
+ const encoding = utf8(call.arguments[1]);
4081
+ if (dataT?.kind === "string") {
4082
+ return {
4083
+ kind: "libCall", fn: "fileHandle.writeFile",
4084
+ args: [receiver(), L.lowerExprExpecting(dataNode, STRING), encoding],
4085
+ type: promise(VOID), loc,
4086
+ };
4087
+ }
4088
+ if (dataT?.kind === "bytes" && dataT.elem === "u8") {
4089
+ return {
4090
+ kind: "libCall", fn: "fileHandle.writeFileBytes",
4091
+ args: [receiver(), L.lowerExprExpecting(dataNode, BYTES_U8), encoding],
4092
+ type: promise(VOID), loc,
4093
+ };
4094
+ }
4095
+ L.noLowering(`FileHandle.${name} of '${dataT ? L.fmt(dataT) : L.checker.typeToString(L.typeOf(dataNode))}' data`, dataNode, "string and Uint8Array data are supported");
4096
+ }
4097
+ if (name === "read") {
4098
+ if (call.arguments.length < 1 || call.arguments.length > 4) {
4099
+ L.noLowering(`FileHandle.read with ${call.arguments.length} arguments`, call, "use read(buffer[, offset[, length[, position]]])");
4100
+ }
4101
+ const buffer = L.lowerExprExpecting(call.arguments[0], BYTES_U8);
4102
+ const type = L.mapTypeOf(L.typeOf(call));
4103
+ if (type?.kind !== "promise" || type.inner.kind !== "record") {
4104
+ L.badType(call, L.typeOf(call));
4105
+ }
4106
+ const offset = num(call.arguments[1], 0);
4107
+ const length = num(call.arguments[2], -1);
4108
+ const position = num(call.arguments[3], -1);
4109
+ return {
4110
+ kind: "libCall", fn: "fileHandle.read",
4111
+ args: [
4112
+ receiver(), buffer, offset.value, length.value,
4113
+ position.value, length.defaulted,
4114
+ ],
4115
+ type, loc,
4116
+ };
4117
+ }
4118
+ if (name === "write") {
4119
+ if (call.arguments.length < 1 || call.arguments.length > 4) {
4120
+ L.noLowering(`FileHandle.write with ${call.arguments.length} arguments`, call);
4121
+ }
4122
+ const dataNode = call.arguments[0];
4123
+ const dataT = L.mapTypeOf(L.typeOf(dataNode));
4124
+ const type = L.mapTypeOf(L.typeOf(call));
4125
+ if (type?.kind !== "promise" || type.inner.kind !== "record") {
4126
+ L.badType(call, L.typeOf(call));
4127
+ }
4128
+ if (dataT?.kind === "bytes" && dataT.elem === "u8") {
4129
+ const offset = num(call.arguments[1], 0);
4130
+ const length = num(call.arguments[2], -1);
4131
+ const position = num(call.arguments[3], -1);
4132
+ return {
4133
+ kind: "libCall", fn: "fileHandle.writeBytes",
4134
+ args: [
4135
+ receiver(), L.lowerExprExpecting(dataNode, BYTES_U8),
4136
+ offset.value, length.value, position.value,
4137
+ length.defaulted,
4138
+ ],
4139
+ type, loc,
4140
+ };
4141
+ }
4142
+ if (dataT?.kind === "string") {
4143
+ if (call.arguments.length > 3) {
4144
+ L.noLowering(`FileHandle.write(string) with ${call.arguments.length} arguments`, call, 'use write(string[, position[, "utf8"]])');
4145
+ }
4146
+ const position = num(call.arguments[1], -1);
4147
+ return {
4148
+ kind: "libCall", fn: "fileHandle.writeStr",
4149
+ args: [receiver(), L.lowerExprExpecting(dataNode, STRING), position.value, utf8(call.arguments[2])],
4150
+ type, loc,
4151
+ };
4152
+ }
4153
+ L.noLowering(`FileHandle.write of '${dataT ? L.fmt(dataT) : L.checker.typeToString(L.typeOf(dataNode))}' data`, dataNode, "string and Uint8Array data are supported");
4154
+ }
4155
+ L.noLowering(`FileHandle.${name}`, call, "fd, close(), read(), write(), readFile(), writeFile(), appendFile(), and stat() are the supported FileHandle members", L.checker.getSymbolAtLocation(access.name));
4156
+ }
3710
4157
  /** Method calls on Stats-typed receivers: isFile()/isDirectory()/
3711
4158
  * isSymbolicLink() are pure reads on the stat snapshot (a followed
3712
4159
  * statSync snapshot never answers true to isSymbolicLink — take
@@ -3729,7 +4176,7 @@ export function lowerStatsMethodCall(L, call, access) {
3729
4176
  : "stats.isSymbolicLink";
3730
4177
  return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) };
3731
4178
  }
3732
- L.noLowering(`Stats.${name}`, call, "isFile(), isDirectory(), isSymbolicLink(), size, and mtimeMs are the supported Stats members", L.checker.getSymbolAtLocation(access.name));
4179
+ L.noLowering(`Stats.${name}`, call, "isFile(), isDirectory(), isSymbolicLink(), size, blocks, nlink, atimeMs, and mtimeMs are the supported Stats members", L.checker.getSymbolAtLocation(access.name));
3733
4180
  }
3734
4181
  /** `Atomics.wait(int32Array, idx, expected, timeoutMs)` — the
3735
4182
  * synchronous-sleep idiom (RouteStore's
@@ -3768,12 +4215,11 @@ export function lowerAtomicsCall(L, call, access) {
3768
4215
  }
3769
4216
  /** The property-read extensions this spoke owns, tried BEFORE the
3770
4217
  * lower-exprs intrinsic-property fallback (the lowerer's wrapper chains
3771
- * them): Stats.mtimeMs (milliseconds with the nanosecond fraction,
3772
- * Node's arithmetic) and SpawnSyncReturns.signal (the termination
3773
- * signal's name as the call site's `Signals | null` union — null for a
3774
- * normal exit or spawn failure; a timeout kill reports its killSignal,
3775
- * Node's shape). Null for everything else, so the ordinary chain (and
3776
- * its fences) keeps going. */
4218
+ * them): the widened numeric Stats snapshot and SpawnSyncReturns.signal
4219
+ * (the termination signal's name as the call site's `Signals | null` union
4220
+ * — null for a normal exit or spawn failure; a timeout kill reports its
4221
+ * killSignal, Node's shape). Null for everything else, so the ordinary
4222
+ * chain (and its fences) keeps going. */
3777
4223
  export function lowerBuiltinExtraProperty(L, expr) {
3778
4224
  if (expr.questionDotToken && !L.chainHandled.has(expr))
3779
4225
  return null;
@@ -3787,12 +4233,23 @@ export function lowerBuiltinExtraProperty(L, expr) {
3787
4233
  return { kind: "recordGet", obj: receiver, shapeId: receiver.type.shapeId, field: "%enc", type: STRING, loc: locOf(expr) };
3788
4234
  }
3789
4235
  const kind = L.mapTypeOf(L.typeOf(expr.expression))?.kind;
3790
- if (kind !== "stats" && kind !== "spawnRes" && kind !== "child")
4236
+ if (kind !== "stats" && kind !== "fileHandle" && kind !== "spawnRes" && kind !== "child")
3791
4237
  return null;
3792
4238
  if (kind === "child" ? !isChildSurfaceMember(L, expr) : !L.isStdlibMember(expr))
3793
4239
  return null;
3794
4240
  const name = expr.name.text;
3795
4241
  const loc = locOf(expr);
4242
+ if (kind === "fileHandle") {
4243
+ if (name === "fd") {
4244
+ const receiver = L.lowerExprExpecting(expr.expression, FILEHANDLE_T);
4245
+ return { kind: "libCall", fn: "fileHandle.fd", args: [receiver], type: F64, loc };
4246
+ }
4247
+ const methods = new Set(["close", "read", "write", "readFile", "writeFile", "appendFile", "stat"]);
4248
+ if (methods.has(name)) {
4249
+ L.unsupported("SC1090", expr, `FileHandle methods as values (call '${name}' directly)`);
4250
+ }
4251
+ L.noLowering(`FileHandle.${name}`, expr, "fd, close(), read(), write(), readFile(), writeFile(), appendFile(), and stat() are the supported FileHandle members", L.checker.getSymbolAtLocation(expr.name));
4252
+ }
3796
4253
  // child.stdout / child.stderr — the piped-output streams: the
3797
4254
  // checker's `Readable | null` (null exactly when the slot was not
3798
4255
  // piped), constructed type-directedly in the backend over the
@@ -3814,9 +4271,11 @@ export function lowerBuiltinExtraProperty(L, expr) {
3814
4271
  }
3815
4272
  if (kind === "child")
3816
4273
  return null; // pid/exitCode/killed live in lowerIntrinsicProperty
3817
- if (kind === "stats" && name === "mtimeMs") {
4274
+ if (kind === "stats" &&
4275
+ (name === "blocks" || name === "nlink" || name === "atimeMs" || name === "mtimeMs")) {
3818
4276
  const receiver = L.lowerExpr(expr.expression);
3819
- return { kind: "libCall", fn: "stats.mtimeMs", args: [receiver], type: F64, loc };
4277
+ const fn = `stats.${name}`;
4278
+ return { kind: "libCall", fn, args: [receiver], type: F64, loc };
3820
4279
  }
3821
4280
  if (kind === "spawnRes" && name === "signal") {
3822
4281
  const receiver = L.lowerExpr(expr.expression);
@@ -4791,12 +5250,13 @@ export function lowerProcessEnvGet(L, expr) {
4791
5250
  export function lowerProcessMethodCall(L, call, access) {
4792
5251
  if (call.questionDotToken)
4793
5252
  return null;
4794
- // process.stdout.write(s) / process.stderr.write(s): the raw byte
4795
- // write — no newline, no formatting. stdout shares console.log's
5253
+ // process.stdout.write(s[, encoding][, callback]) and stderr's twin:
5254
+ // raw bytes, no newline or formatting. stdout shares console.log's
4796
5255
  // promptly-submitted stream, preserving source order. Node's boolean
4797
5256
  // is a backpressure signal; this synchronous write is constantly true.
4798
- // @types/node's wider forms (Buffer data, encoding, callback) typecheck
4799
- // and fence here.
5257
+ // String encodings are compile-time-known BufferEncoding spellings;
5258
+ // byte chunks evaluate but ignore the encoding like Node. Completion
5259
+ // callbacks ride the next-tick queue and receive the success `null`.
4800
5260
  // process.stdin.destroy(): a deliberate no-op — no stream machinery
4801
5261
  // exists to tear down, and no other stdin surface observes the
4802
5262
  // destroyed state (SEMANTICS.md documents it).
@@ -4880,10 +5340,64 @@ export function lowerProcessMethodCall(L, call, access) {
4880
5340
  const stream = L.stdlibGlobalMember(access.expression, "process");
4881
5341
  if (stream === "stdout" || stream === "stderr") {
4882
5342
  const loc = locOf(call);
4883
- if (call.arguments.length !== 1) {
4884
- L.noLowering(`process.${stream}.write with ${call.arguments.length} arguments`, call, "the supported form is write(data) with one string — encodings and callbacks have no lowering");
5343
+ const args = call.arguments;
5344
+ if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) {
5345
+ L.noLowering(`process.${stream}.write with ${args.length} arguments`, call, "the supported forms are write(data[, encoding][, callback]) with a static BufferEncoding and completion callback");
4885
5346
  }
4886
- let data = L.lowerExpr(call.arguments[0]);
5347
+ const secondNode = args[1];
5348
+ const thirdNode = args[2];
5349
+ const secondUndefined = secondNode
5350
+ ? lowerStaticallyUndefinedBuiltinArg(L, secondNode)
5351
+ : null;
5352
+ const thirdUndefined = thirdNode
5353
+ ? lowerStaticallyUndefinedBuiltinArg(L, thirdNode)
5354
+ : null;
5355
+ const secondT = secondNode && !secondUndefined ? L.mapTypeOf(L.typeOf(secondNode)) : undefined;
5356
+ const callbackNode = args.length === 3 && !thirdUndefined
5357
+ ? thirdNode
5358
+ : args.length === 2 && secondT?.kind === "func"
5359
+ ? secondNode
5360
+ : undefined;
5361
+ const encodingNode = (args.length === 3 || (args.length === 2 && callbackNode === undefined)) && !secondUndefined
5362
+ ? secondNode
5363
+ : undefined;
5364
+ // Node's BufferEncoding aliases normalize before bytes are made.
5365
+ // Preserve an effectful literal-typed expression even when its
5366
+ // spelling folds (for example a function returning `"binary"`).
5367
+ const encoding = (() => {
5368
+ const defaultEncoding = { kind: "strLit", value: "utf8", type: STRING, loc };
5369
+ if (secondUndefined) {
5370
+ return defaultAfterUndefined(secondUndefined, defaultEncoding);
5371
+ }
5372
+ if (!encodingNode) {
5373
+ return defaultEncoding;
5374
+ }
5375
+ const aliases = {
5376
+ utf8: "utf8", "utf-8": "utf8", hex: "hex", base64: "base64",
5377
+ base64url: "base64url", latin1: "latin1", binary: "latin1", ascii: "ascii",
5378
+ utf16le: "utf16le", "utf-16le": "utf16le", ucs2: "utf16le", "ucs-2": "utf16le",
5379
+ };
5380
+ const t = L.typeOf(encodingNode);
5381
+ const raw = t.isStringLiteralType() ? t.value : undefined;
5382
+ const canonical = raw !== undefined ? own(aliases, raw) : undefined;
5383
+ if (canonical === undefined) {
5384
+ L.noLowering(`process.${stream}.write with this encoding`, encodingNode, 'use a literal "utf8", "hex", "base64", "base64url", "latin1", "binary", "ascii", "utf16le", or "ucs2" encoding');
5385
+ }
5386
+ const evaluated = L.lowerExprExpecting(encodingNode, STRING);
5387
+ if (canonical === raw)
5388
+ return evaluated;
5389
+ const normalized = { kind: "strLit", value: canonical, type: STRING, loc: locOf(encodingNode) };
5390
+ return droppableStatic(evaluated)
5391
+ ? normalized
5392
+ : {
5393
+ kind: "seqExpr",
5394
+ stmts: [{ kind: "exprStmt", expr: evaluated, loc: evaluated.loc }],
5395
+ result: normalized,
5396
+ type: STRING,
5397
+ loc: evaluated.loc,
5398
+ };
5399
+ })();
5400
+ let data = L.lowerExpr(args[0]);
4887
5401
  // A checked-dynamic argument in a JS file takes the validated
4888
5402
  // string exit (the trust-but-verify boundary: commander's
4889
5403
  // `writeOut: (str) => process.stdout.write(str)` — str untyped):
@@ -4892,20 +5406,81 @@ export function lowerProcessMethodCall(L, call, access) {
4892
5406
  if (data.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
4893
5407
  data = { kind: "dynCheck", value: data, type: STRING, loc };
4894
5408
  }
4895
- // The Buffer overload writes raw bytes through the same promptly
4896
- // submitted streams.
4897
- if (data.type.kind === "bytes" && data.type.elem === "u8") {
5409
+ const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
5410
+ if (!isBytes && data.type.kind !== "string") {
5411
+ L.noLowering(`process.${stream}.write of non-string data`, args[0], "strings and Buffer/Uint8Array values write; narrow unions first");
5412
+ }
5413
+ let callback;
5414
+ if (callbackNode) {
5415
+ callback = L.lowerExpr(callbackNode);
5416
+ if (callback.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
5417
+ callback = {
5418
+ kind: "dynCheck",
5419
+ value: callback,
5420
+ type: funcOf([DYN], VOID),
5421
+ loc: locOf(callbackNode),
5422
+ };
5423
+ }
5424
+ let callbackOk = callback.type.kind === "func" && callback.type.params.length <= 1;
5425
+ if (callbackOk && callback.type.kind === "func" && callback.type.params.length === 1) {
5426
+ const param = callback.type.params[0];
5427
+ if (param.kind !== "dyn") {
5428
+ const def = param.kind === "union" ? L.unions.get(param.unionId) : undefined;
5429
+ callbackOk = !!def &&
5430
+ def.arms.some((a) => a.kind === "nullT") &&
5431
+ def.arms.some((a) => a.kind === "object" && a.className === "%Error") &&
5432
+ def.arms.every((a) => a.kind === "nullT" || a.kind === "undefinedT" ||
5433
+ (a.kind === "object" && a.className === "%Error"));
5434
+ }
5435
+ }
5436
+ if (!callbackOk) {
5437
+ L.unsupported("SC1090", callbackNode, "process output completion callbacks must accept at most one Error | null parameter");
5438
+ }
5439
+ callback = voidizedCallback(L, callback, locOf(callbackNode));
5440
+ }
5441
+ // Encoding- or callback-bearing writes use one fixed byte ABI. For
5442
+ // strings, Buffer.from's encoder runs after data/encoding evaluation
5443
+ // and before the callback expression; only its pure allocation moves
5444
+ // earlier than Node's internal write conversion.
5445
+ if (callback || args.length > 1 || isBytes) {
5446
+ const bytes = isBytes
5447
+ ? data
5448
+ : { kind: "libCall", fn: "buffer.fromStr", args: [data, encoding], type: BYTES_U8, loc };
5449
+ const defaultRuntimeEncoding = { kind: "strLit", value: "utf8", type: STRING, loc };
5450
+ let runtimeEncoding = isBytes ? encoding : defaultRuntimeEncoding;
5451
+ // An explicitly-undefined third argument is still evaluated after
5452
+ // data and encoding, even though it schedules no callback. Strings
5453
+ // already evaluate encoding while producing `bytes`; byte chunks
5454
+ // fold both ignored argument effects into this final ABI slot.
5455
+ if (thirdUndefined && !droppableStatic(thirdUndefined)) {
5456
+ const effects = isBytes
5457
+ ? [encoding, thirdUndefined].filter((effect) => !droppableStatic(effect))
5458
+ : [thirdUndefined];
5459
+ runtimeEncoding = {
5460
+ kind: "seqExpr",
5461
+ stmts: effects.map((effect) => ({ kind: "exprStmt", expr: effect, loc: effect.loc })),
5462
+ result: defaultRuntimeEncoding,
5463
+ type: STRING,
5464
+ loc: thirdUndefined.loc,
5465
+ };
5466
+ }
5467
+ if (callback) {
5468
+ return {
5469
+ kind: "libCall",
5470
+ fn: stream === "stdout" ? "process.stdoutWriteBytesCb" : "process.stderrWriteBytesCb",
5471
+ args: [bytes, runtimeEncoding, callback],
5472
+ type: BOOL,
5473
+ loc,
5474
+ };
5475
+ }
4898
5476
  return {
4899
5477
  kind: "libCall",
4900
5478
  fn: stream === "stdout" ? "process.stdoutWriteBytes" : "process.stderrWriteBytes",
4901
- args: [data],
5479
+ args: [bytes, runtimeEncoding],
4902
5480
  type: BOOL,
4903
5481
  loc,
4904
5482
  };
4905
5483
  }
4906
- if (data.type.kind !== "string") {
4907
- L.noLowering(`process.${stream}.write of non-string data`, call.arguments[0], "strings and Buffer/Uint8Array values write; narrow unions first");
4908
- }
4909
5484
  return {
4910
5485
  kind: "libCall",
4911
5486
  fn: stream === "stdout" ? "process.stdoutWrite" : "process.stderrWrite",
@@ -5443,6 +6018,79 @@ export function lowerDateCall(L, call, access) {
5443
6018
  }
5444
6019
  L.noLowering(`Date.prototype.${name}`, call, DATE_METHOD_HINT, L.checker.getSymbolAtLocation(access.name));
5445
6020
  }
6021
+ /* WHATWG label registry → native decoder id. The first 27 ids stay in the
6022
+ * same order as scripts/gen-text-decoder-tables.mjs; the remaining ids match
6023
+ * scr_bytes.c's compact enum. ISO-8859-8-I shares ISO-8859-8's byte table,
6024
+ * and GBK/gb18030 share one decoder (their decoders are identical). */
6025
+ const TEXT_DECODER_SINGLE_BYTE_NAMES = [
6026
+ "ibm866", "iso-8859-2", "iso-8859-3", "iso-8859-4", "iso-8859-5",
6027
+ "iso-8859-6", "iso-8859-7", "iso-8859-8", "iso-8859-10", "iso-8859-13",
6028
+ "iso-8859-14", "iso-8859-15", "iso-8859-16", "koi8-r", "koi8-u",
6029
+ "macintosh", "windows-874", "windows-1250", "windows-1251", "windows-1252",
6030
+ "windows-1253", "windows-1254", "windows-1255", "windows-1256", "windows-1257",
6031
+ "windows-1258", "x-mac-cyrillic",
6032
+ ];
6033
+ const TEXT_DECODER_UTF8_LABELS = new Set([
6034
+ "unicode-1-1-utf-8", "unicode11utf8", "unicode20utf8", "utf-8", "utf8", "x-unicode20utf8",
6035
+ ]);
6036
+ const TEXT_DECODER_LEGACY_LABELS = (() => {
6037
+ const labels = Object.create(null);
6038
+ const add = (encoding, names) => {
6039
+ for (const name of names)
6040
+ labels[name] = encoding;
6041
+ };
6042
+ const single = (name, aliases) => {
6043
+ add(TEXT_DECODER_SINGLE_BYTE_NAMES.indexOf(name), aliases);
6044
+ };
6045
+ single("ibm866", ["866", "cp866", "csibm866", "ibm866"]);
6046
+ single("iso-8859-2", ["csisolatin2", "iso-8859-2", "iso-ir-101", "iso8859-2", "iso88592", "iso_8859-2", "iso_8859-2:1987", "l2", "latin2"]);
6047
+ single("iso-8859-3", ["csisolatin3", "iso-8859-3", "iso-ir-109", "iso8859-3", "iso88593", "iso_8859-3", "iso_8859-3:1988", "l3", "latin3"]);
6048
+ single("iso-8859-4", ["csisolatin4", "iso-8859-4", "iso-ir-110", "iso8859-4", "iso88594", "iso_8859-4", "iso_8859-4:1988", "l4", "latin4"]);
6049
+ single("iso-8859-5", ["csisolatincyrillic", "cyrillic", "iso-8859-5", "iso-ir-144", "iso8859-5", "iso88595", "iso_8859-5", "iso_8859-5:1988"]);
6050
+ single("iso-8859-6", ["arabic", "asmo-708", "csiso88596e", "csiso88596i", "csisolatinarabic", "ecma-114", "iso-8859-6", "iso-8859-6-e", "iso-8859-6-i", "iso-ir-127", "iso8859-6", "iso88596", "iso_8859-6", "iso_8859-6:1987"]);
6051
+ single("iso-8859-7", ["csisolatingreek", "ecma-118", "elot_928", "greek", "greek8", "iso-8859-7", "iso-ir-126", "iso8859-7", "iso88597", "iso_8859-7", "iso_8859-7:1987", "sun_eu_greek"]);
6052
+ single("iso-8859-8", ["csiso88598e", "csiso88598i", "csisolatinhebrew", "hebrew", "iso-8859-8", "iso-8859-8-e", "iso-8859-8-i", "iso-ir-138", "iso8859-8", "iso88598", "iso_8859-8", "iso_8859-8:1988", "logical", "visual"]);
6053
+ single("iso-8859-10", ["csisolatin6", "iso-8859-10", "iso-ir-157", "iso8859-10", "iso885910", "l6", "latin6"]);
6054
+ single("iso-8859-13", ["iso-8859-13", "iso8859-13", "iso885913"]);
6055
+ single("iso-8859-14", ["iso-8859-14", "iso8859-14", "iso885914"]);
6056
+ single("iso-8859-15", ["csisolatin9", "iso-8859-15", "iso8859-15", "iso885915", "iso_8859-15", "l9"]);
6057
+ single("iso-8859-16", ["iso-8859-16"]);
6058
+ single("koi8-r", ["cskoi8r", "koi", "koi8", "koi8-r", "koi8_r"]);
6059
+ single("koi8-u", ["koi8-ru", "koi8-u"]);
6060
+ single("macintosh", ["csmacintosh", "mac", "macintosh", "x-mac-roman"]);
6061
+ single("windows-874", ["dos-874", "iso-8859-11", "iso8859-11", "iso885911", "tis-620", "windows-874"]);
6062
+ single("windows-1250", ["cp1250", "windows-1250", "x-cp1250"]);
6063
+ single("windows-1251", ["cp1251", "windows-1251", "x-cp1251"]);
6064
+ single("windows-1252", ["ansi_x3.4-1968", "ascii", "cp1252", "cp819", "csisolatin1", "ibm819", "iso-8859-1", "iso-ir-100", "iso8859-1", "iso88591", "iso_8859-1", "iso_8859-1:1987", "l1", "latin1", "us-ascii", "windows-1252", "x-cp1252"]);
6065
+ single("windows-1253", ["cp1253", "windows-1253", "x-cp1253"]);
6066
+ single("windows-1254", ["cp1254", "csisolatin5", "iso-8859-9", "iso-ir-148", "iso8859-9", "iso88599", "iso_8859-9", "iso_8859-9:1989", "l5", "latin5", "windows-1254", "x-cp1254"]);
6067
+ single("windows-1255", ["cp1255", "windows-1255", "x-cp1255"]);
6068
+ single("windows-1256", ["cp1256", "windows-1256", "x-cp1256"]);
6069
+ single("windows-1257", ["cp1257", "windows-1257", "x-cp1257"]);
6070
+ single("windows-1258", ["cp1258", "windows-1258", "x-cp1258"]);
6071
+ single("x-mac-cyrillic", ["x-mac-cyrillic", "x-mac-ukrainian"]);
6072
+ add(27, ["x-user-defined"]);
6073
+ add(28, ["csunicode", "iso-10646-ucs-2", "ucs-2", "unicode", "unicodefeff", "utf-16", "utf-16le"]);
6074
+ add(29, ["unicodefffe", "utf-16be"]);
6075
+ add(30, ["chinese", "csgb2312", "csiso58gb231280", "gb18030", "gb2312", "gb_2312", "gb_2312-80", "gbk", "iso-ir-58", "x-gbk"]);
6076
+ add(31, ["big5", "big5-hkscs", "cn-big5", "csbig5", "x-x-big5"]);
6077
+ add(32, ["cseucpkdfmtjapanese", "euc-jp", "x-euc-jp"]);
6078
+ add(33, ["csiso2022jp", "iso-2022-jp"]);
6079
+ add(34, ["csshiftjis", "ms932", "ms_kanji", "shift-jis", "shift_jis", "sjis", "windows-31j", "x-sjis"]);
6080
+ add(35, ["cseuckr", "csksc56011987", "euc-kr", "iso-ir-149", "korean", "ks_c_5601-1987", "ks_c_5601-1989", "ksc5601", "ksc_5601", "windows-949"]);
6081
+ return labels;
6082
+ })();
6083
+ /** TextDecoder's get-an-encoding normalization: trim ASCII whitespace and
6084
+ * fold ASCII case only (Unicode case folding must not manufacture a label). */
6085
+ function staticTextDecoderEncoding(label) {
6086
+ const normalized = label
6087
+ .replace(/^[\u0009\u000a\u000c\u000d\u0020]+|[\u0009\u000a\u000c\u000d\u0020]+$/g, "")
6088
+ .replace(/[A-Z]/g, (char) => char.toLowerCase());
6089
+ if (TEXT_DECODER_UTF8_LABELS.has(normalized))
6090
+ return { kind: "utf8" };
6091
+ const id = own(TEXT_DECODER_LEGACY_LABELS, normalized);
6092
+ return id === undefined ? null : { kind: "legacy", id };
6093
+ }
5446
6094
  /** A direct construction of THE stdlib TextEncoder/TextDecoder, through
5447
6095
  * type-only wrappers. Name alone is never enough: a user class with the
5448
6096
  * same spelling keeps the ordinary class lowering. */
@@ -5465,8 +6113,9 @@ function directTextCodecCtorOf(L, expr) {
5465
6113
  /** True when construction itself is effect-free and inside the already
5466
6114
  * lowered codec slice, so a const binding can be erased and calls can
5467
6115
  * resolve back to the initializer. TextDecoder's explicit label must be
5468
- * an actual literal here: accepting an arbitrary expression merely typed
5469
- * as "utf-8" would move or repeat its effects when the binding is erased. */
6116
+ * an actual recognized literal here: accepting an arbitrary expression
6117
+ * merely typed as a label would move or repeat its effects when the
6118
+ * binding is erased. */
5470
6119
  function erasableTextCodecCtor(info) {
5471
6120
  const args = info.ctor.arguments ?? [];
5472
6121
  if (info.cls === "TextEncoder")
@@ -5476,9 +6125,9 @@ function erasableTextCodecCtor(info) {
5476
6125
  if (args.length !== 1)
5477
6126
  return false;
5478
6127
  const label = stripTypeCasts(args[0]);
5479
- return ts.isStringLiteralLike(label) && (label.text === "utf-8" || label.text === "utf8");
6128
+ return ts.isStringLiteralLike(label) && staticTextDecoderEncoding(label.text) !== null;
5480
6129
  }
5481
- /** `const encoder = new TextEncoder()` / the default TextDecoder twin:
6130
+ /** `const encoder = new TextEncoder()` / a statically-labelled TextDecoder twin:
5482
6131
  * compile-time alias plumbing with no runtime object. Calls through the
5483
6132
  * stable binding are recognized by textCodecReceiverOf below; any other
5484
6133
  * reached value use keeps the ordinary SC2020 representation fence. Both
@@ -5573,8 +6222,9 @@ function textCodecReceiverOf(L, expr) {
5573
6222
  * the idiomatic store-then-call equivalents. The codec object never
5574
6223
  * exists (the crypto/Date precedent); bare construction and value uses
5575
6224
  * are fenced with the composed hint. decode is the runtime's WHATWG
5576
- * utf-8 decode with the leading BOM stripped; a zero-argument decode()
5577
- * is "" like the spec's. encode IS Buffer.from(s, "utf8") — ScrStr
6225
+ * decode for every recognized static label (with BOM handling for the
6226
+ * Unicode encodings); a zero-argument decode() is "" like the spec's.
6227
+ * encode IS Buffer.from(s, "utf8") — ScrStr
5578
6228
  * storage is well-formed UTF-8, so the bytes are identical (lone
5579
6229
  * surrogates became U+FFFD at string construction, exactly what the
5580
6230
  * spec's encoder emits). Null when this is neither supported form. */
@@ -5594,17 +6244,33 @@ export function lowerTextCodecCall(L, call, access) {
5594
6244
  const loc = locOf(call);
5595
6245
  const ctorArgs = recv.arguments ?? [];
5596
6246
  if (cls === "TextDecoder") {
5597
- // The constructor may spell the default label; anything else (other
5598
- // labels, { fatal }/{ ignoreBOM } options) changes behavior the
5599
- // runtime doesn't implement.
6247
+ // The label must be statically known. A literal-typed effectful inline
6248
+ // expression is accepted, but its evaluation is sequenced before the
6249
+ // decode below; stored decoder aliases admit only actual literals.
5600
6250
  const labelT = ctorArgs.length >= 1 ? L.typeOf(ctorArgs[0]) : null;
5601
- const utf8Label = labelT !== null && labelT.isStringLiteralType() && (labelT.value === "utf-8" || labelT.value === "utf8");
5602
- if (ctorArgs.length > 1 || (ctorArgs.length === 1 && !utf8Label)) {
5603
- L.noLowering("new TextDecoder beyond the default utf-8", recv, "utf-8 with default options is the supported decoder: new TextDecoder().decode(bytes)");
5604
- }
6251
+ const encoding = ctorArgs.length === 0
6252
+ ? { kind: "utf8" }
6253
+ : labelT !== null && labelT.isStringLiteralType()
6254
+ ? staticTextDecoderEncoding(labelT.value)
6255
+ : null;
6256
+ if (ctorArgs.length > 1 || encoding === null) {
6257
+ L.noLowering("new TextDecoder with runtime-valued options or an unknown label", recv, "a recognized literal WHATWG label with default options compiles");
6258
+ }
6259
+ const labelEffect = ctorArgs.length === 1
6260
+ ? L.lowerExprExpecting(ctorArgs[0], STRING)
6261
+ : null;
6262
+ const afterLabel = (result) => labelEffect === null || labelEffect.kind === "strLit"
6263
+ ? result
6264
+ : {
6265
+ kind: "seqExpr",
6266
+ stmts: [{ kind: "exprStmt", expr: labelEffect, loc: labelEffect.loc }],
6267
+ result,
6268
+ type: result.type,
6269
+ loc,
6270
+ };
5605
6271
  if (call.arguments.length === 0) {
5606
6272
  // decode() with no input is "" per spec — nothing to evaluate.
5607
- return { kind: "strLit", value: "", type: STRING, loc };
6273
+ return afterLabel({ kind: "strLit", value: "", type: STRING, loc });
5608
6274
  }
5609
6275
  if (call.arguments.length !== 1) {
5610
6276
  L.noLowering("decode with a stream option", call, "streaming decode has no lowering — decode whole buffers");
@@ -5614,7 +6280,16 @@ export function lowerTextCodecCall(L, call, access) {
5614
6280
  if (!(arg.type.kind === "bytes" && arg.type.elem === "u8")) {
5615
6281
  L.noLowering(`TextDecoder.decode of '${L.fmt(arg.type)}' values`, argNode, "Uint8Array/Buffer input decodes (ArrayBuffer values have no representation)");
5616
6282
  }
5617
- return { kind: "libCall", fn: "text.decode", args: [arg], type: STRING, loc };
6283
+ const decoded = encoding.kind === "utf8"
6284
+ ? { kind: "libCall", fn: "text.decode", args: [arg], type: STRING, loc }
6285
+ : {
6286
+ kind: "libCall",
6287
+ fn: "text.decodeLegacy",
6288
+ args: [arg, { kind: "numLit", value: encoding.id, type: F64, loc }],
6289
+ type: STRING,
6290
+ loc,
6291
+ };
6292
+ return afterLabel(decoded);
5618
6293
  }
5619
6294
  if (ctorArgs.length > 0) {
5620
6295
  L.noLowering("new TextEncoder with arguments", recv);