@scriptc/compiler 0.0.7 → 0.0.9

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 (94) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +13 -0
  2. package/dist/backend/cc.js +14 -0
  3. package/dist/backend/cc.js.map +1 -1
  4. package/dist/backend/emission/emit-exprs.js +155 -6
  5. package/dist/backend/emission/emit-exprs.js.map +1 -1
  6. package/dist/backend/emission/emit-island.js +9 -1
  7. package/dist/backend/emission/emit-island.js.map +1 -1
  8. package/dist/backend/emission/emit-stmts.js +3 -1
  9. package/dist/backend/emission/emit-stmts.js.map +1 -1
  10. package/dist/backend/emission/emit-types.d.ts +3 -0
  11. package/dist/backend/emission/emit-types.js +12 -0
  12. package/dist/backend/emission/emit-types.js.map +1 -1
  13. package/dist/backend/emission/emit-walkers.d.ts +7 -5
  14. package/dist/backend/emission/emit-walkers.js +45 -18
  15. package/dist/backend/emission/emit-walkers.js.map +1 -1
  16. package/dist/backend/emission/emitter.d.ts +1 -1
  17. package/dist/backend/emission/emitter.js +35 -10
  18. package/dist/backend/emission/emitter.js.map +1 -1
  19. package/dist/backend/emission/may-throw.js +4 -3
  20. package/dist/backend/emission/may-throw.js.map +1 -1
  21. package/dist/backend/llvm/emitter.js +300 -18
  22. package/dist/backend/llvm/emitter.js.map +1 -1
  23. package/dist/diagnostics/diagnostic.d.ts +98 -11
  24. package/dist/diagnostics/diagnostic.js +185 -25
  25. package/dist/diagnostics/diagnostic.js.map +1 -1
  26. package/dist/frontend/lib-contract.d.ts +24 -6
  27. package/dist/frontend/lib-contract.js +125 -44
  28. package/dist/frontend/lib-contract.js.map +1 -1
  29. package/dist/frontend/lowering/lower-assert.js +16 -0
  30. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  31. package/dist/frontend/lowering/lower-builtins.d.ts +11 -0
  32. package/dist/frontend/lowering/lower-builtins.js +127 -4
  33. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  34. package/dist/frontend/lowering/lower-calls.d.ts +77 -18
  35. package/dist/frontend/lowering/lower-calls.js +978 -44
  36. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  37. package/dist/frontend/lowering/lower-classes.d.ts +14 -1
  38. package/dist/frontend/lowering/lower-classes.js +112 -16
  39. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  40. package/dist/frontend/lowering/lower-containers.d.ts +4 -2
  41. package/dist/frontend/lowering/lower-containers.js +236 -44
  42. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  43. package/dist/frontend/lowering/lower-emitter.js +55 -1
  44. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  45. package/dist/frontend/lowering/lower-exprs.d.ts +26 -3
  46. package/dist/frontend/lowering/lower-exprs.js +507 -47
  47. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  48. package/dist/frontend/lowering/lower-island.d.ts +7 -0
  49. package/dist/frontend/lowering/lower-island.js +71 -10
  50. package/dist/frontend/lowering/lower-island.js.map +1 -1
  51. package/dist/frontend/lowering/lower-mixins.d.ts +1 -1
  52. package/dist/frontend/lowering/lower-mixins.js +1 -1
  53. package/dist/frontend/lowering/lower-modules.js +95 -4
  54. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  55. package/dist/frontend/lowering/lower-stmts.js +866 -56
  56. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  57. package/dist/frontend/lowering/lower-stream.js +28 -2
  58. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  59. package/dist/frontend/lowering/lowerer.d.ts +85 -17
  60. package/dist/frontend/lowering/lowerer.js +459 -37
  61. package/dist/frontend/lowering/lowerer.js.map +1 -1
  62. package/dist/frontend/lowering/surfaces.js +20 -3
  63. package/dist/frontend/lowering/surfaces.js.map +1 -1
  64. package/dist/frontend/npm-static-rewrite.d.ts +7 -2
  65. package/dist/frontend/npm-static-rewrite.js +566 -138
  66. package/dist/frontend/npm-static-rewrite.js.map +1 -1
  67. package/dist/frontend/npm-static.js +11 -2
  68. package/dist/frontend/npm-static.js.map +1 -1
  69. package/dist/frontend/program.js +5 -5
  70. package/dist/frontend/program.js.map +1 -1
  71. package/dist/frontend/shared.d.ts +6 -1
  72. package/dist/frontend/shared.js +46 -1
  73. package/dist/frontend/shared.js.map +1 -1
  74. package/dist/frontend/types.d.ts +28 -1
  75. package/dist/frontend/types.js +304 -10
  76. package/dist/frontend/types.js.map +1 -1
  77. package/dist/index.d.ts +2 -1
  78. package/dist/index.js +181 -26
  79. package/dist/index.js.map +1 -1
  80. package/dist/ir/nodes.d.ts +159 -9
  81. package/dist/ir/nodes.js +76 -6
  82. package/dist/ir/nodes.js.map +1 -1
  83. package/dist/ir/validate.js +93 -14
  84. package/dist/ir/validate.js.map +1 -1
  85. package/dist/library/profile.d.ts +17 -3
  86. package/dist/library/profile.js +38 -9
  87. package/dist/library/profile.js.map +1 -1
  88. package/dist/library/sidecar.js +162 -43
  89. package/dist/library/sidecar.js.map +1 -1
  90. package/dist/library/trap-teaching.d.ts +11 -0
  91. package/dist/library/trap-teaching.js +49 -0
  92. package/dist/library/trap-teaching.js.map +1 -0
  93. package/package.json +2 -2
  94. package/surface-manifest.json +46 -4
@@ -7,16 +7,16 @@ import { lowerGenMethodCall } from "./lower-generators.js";
7
7
  import { BOOL, CAUGHT, DYN, F64, JSVAL, STRING, SYMBOL_T, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, funcOf, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/nodes.js";
8
8
  import { isJsSourceFile, locOf } from "../program.js";
9
9
  import { isGenericCallableMemberType, typeKey } from "../types.js";
10
- import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx } from "./lowerer.js";
10
+ import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr } from "./lowerer.js";
11
11
  import { enforceLibBoundary } from "./lib-boundary.js";
12
- import { builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js";
12
+ import { NARROW_FIRST, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js";
13
13
  import { requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
14
14
  import { mixinFnShapeOf } from "./lower-mixins.js";
15
15
  import { bufEncoding, dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerRegexMethodCall, lowerStringMethodCall, lowerTupleReadMethodCall } from "./lower-containers.js";
16
16
  import { lowerChildStreamMethodCall, lowerDirentMethodCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerWatcherMethodCall } from "./lower-builtins.js";
17
- import { lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js";
17
+ import { droppableStatic, lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js";
18
18
  import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerHttpClientFnCall } from "./lower-server.js";
19
- import { EMITTER_API_MEMBERS, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall } from "./lower-classes.js";
19
+ import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall } from "./lower-classes.js";
20
20
  import { emitterRooted, lowerEmitterMethodCall } from "./lower-emitter.js";
21
21
  import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js";
22
22
  import { STREAM_API_MEMBERS, lowerStreamMethodCall, lowerStreamModuleCall, lowerStreamStaticCall, streamSidesOf } from "./lower-stream.js";
@@ -107,9 +107,20 @@ export function paramShape(L, param) {
107
107
  // ARRAY param the dyn call thunk fills with the call's surplus
108
108
  // arguments; the binding is that array (dynRest — funcType marks
109
109
  // `rest`, and the value only ever calls through the boxed thunk).
110
- if (isJsSourceFile(param.getSourceFile()) &&
111
- L.mapTypeOf(L.typeOf(param.name))?.kind !== "array") {
112
- return { type: DYN, mode: "dynRest" };
110
+ if (isJsSourceFile(param.getSourceFile())) {
111
+ const restMapped = L.mapTypeOf(L.typeOf(param.name));
112
+ // `any[]` under --dynamic maps to an island-element array — that
113
+ // is inference residue, not element information; the binding is
114
+ // the ENGINE's own arguments array (an island handle) and the
115
+ // value crosses as a REST host function (the withPlugins
116
+ // `async (...args) =>` shape). Static builds keep the variadic
117
+ // dyn form for every unmappable JS rest.
118
+ if (restMapped?.kind === "array" && restMapped.elem.kind === "jsval" && L.dynamic) {
119
+ return { type: JSVAL, mode: "islandRest" };
120
+ }
121
+ if (restMapped?.kind !== "array") {
122
+ return { type: DYN, mode: "dynRest" };
123
+ }
113
124
  }
114
125
  const type = L.irTypeOf(param.name);
115
126
  // Tuple-typed rest params don't map to an array; generic rest is the
@@ -213,7 +224,7 @@ export function completeArgs(L, argNodes, shapes, loc, blame,
213
224
  leading) {
214
225
  const isIr = (s) => s !== undefined && !("kind" in s);
215
226
  const sources = leading && leading.length > 0 ? [...leading.map((ir) => ({ ir })), ...argNodes] : argNodes;
216
- const restAt = shapes.findIndex((s) => s.mode === "rest" || s.mode === "dynRest");
227
+ const restAt = shapes.findIndex((s) => s.mode === "rest" || s.mode === "dynRest" || s.mode === "islandRest");
217
228
  const positional = restAt >= 0 ? shapes.slice(0, restAt) : [...shapes];
218
229
  const out = positional.map((shape, i) => {
219
230
  const src = sources[i];
@@ -243,7 +254,21 @@ leading) {
243
254
  }
244
255
  return L.undefinedArgFor(shape.type, loc, blame);
245
256
  });
246
- if (restAt >= 0 && shapes[restAt].mode === "dynRest") {
257
+ if (restAt >= 0 && shapes[restAt].mode === "islandRest") {
258
+ // The ISLAND variadic pack: surplus arguments marshal into one
259
+ // fresh ENGINE array — exactly what the REST host-call adapter
260
+ // hands the closure for indirect calls.
261
+ const elems = sources.slice(restAt).map((a) => {
262
+ if (isIr(a))
263
+ return L.coerceInto(blame, a.ir, JSVAL);
264
+ if (ts.isSpreadElement(a)) {
265
+ L.unsupported("SC1090", a, "spread arguments into an island rest parameter");
266
+ }
267
+ return L.lowerExprExpecting(a, JSVAL);
268
+ });
269
+ out.push({ kind: "jsOp", op: "arrLit", args: elems, type: JSVAL, loc });
270
+ }
271
+ else if (restAt >= 0 && shapes[restAt].mode === "dynRest") {
247
272
  // The VARIADIC dyn pack (a JS `...args` with no static element
248
273
  // type, or the synthetic `arguments` slot): surplus arguments
249
274
  // convert through the dyn boundary into one fresh DOM array —
@@ -405,6 +430,7 @@ export function requireExactArityValue(L, blame, contextual, shapes, funcType) {
405
430
  // tier's undefined (omittedArgFor).
406
431
  if (shapes.every((s) => s.mode === "required" ||
407
432
  s.mode === "dynRest" ||
433
+ s.mode === "islandRest" ||
408
434
  (s.mode === "omittable" && (s.type.kind === "dyn" || s.type.kind === "jsval")))) {
409
435
  return;
410
436
  }
@@ -831,24 +857,61 @@ export function genericCallInstance(L, expr, info) {
831
857
  fenceGenericSignatureResult(L, expr, retTs);
832
858
  L.badType(expr, retTs);
833
859
  }
834
- return internGenericInstance(L, expr, info, params, returnType, () => L.inferTypeParamBindings(expr, info, rsig));
860
+ // keyof-constrained type parameters (`K extends keyof T`): the bound
861
+ // LITERAL is semantic — the instance body reads the named field — so
862
+ // the bindings compute EAGERLY (the key needs them) and each literal
863
+ // keys its own instance (`pick(o, "a")` and `pick(o, "b")` map to the
864
+ // same IR signature when the fields agree, but their bodies read
865
+ // different fields). Non-literal bindings (a key union, plain string)
866
+ // share one runtime-keyed instance per IR signature, exactly the
867
+ // widened discipline.
868
+ const keyofTps = keyofConstrainedTypeParams(info);
869
+ if (keyofTps.size > 0) {
870
+ const tsBindings = new Map();
871
+ const bindings = L.inferTypeParamBindings(expr, info, rsig, tsBindings);
872
+ const litKey = info.typeParams
873
+ .filter((tp) => keyofTps.has(tp))
874
+ .map((tp) => {
875
+ const bound = tsBindings.get(tp);
876
+ return bound?.isStringLiteralType() ? JSON.stringify(bound.value)
877
+ : bound?.isNumberLiteralType() ? String(bound.value)
878
+ : "*";
879
+ })
880
+ .join(",");
881
+ return internGenericInstance(L, expr, info, params, returnType, () => bindings, {
882
+ extraKey: `@${litKey}`,
883
+ tsBindings,
884
+ });
885
+ }
886
+ return internGenericInstance(L, expr, info, params, returnType, (tsBindings) => L.inferTypeParamBindings(expr, info, rsig, tsBindings));
835
887
  }
836
888
  /** The one instance table both instantiation routes share: key identity IS
837
889
  * signature identity, so a call (`identity(1)`) and a pinned VALUE
838
890
  * (`const f: (x: number) => number = identity`) reuse one compiled
839
891
  * instance. `makeBindings` runs only for a NEW key (binding inference
840
892
  * costs checker walks). */
841
- export function internGenericInstance(L, blame, info, params, returnType, makeBindings) {
842
- const key = `${params.map((s) => typeKey(s.type)).join(",")}=>${typeKey(returnType)}`;
893
+ export function internGenericInstance(L, blame, info, params, returnType, makeBindings, opts) {
894
+ // keyof-constrained instantiations append their literal keys
895
+ // (extraKey): the IR signature alone under-discriminates there — two
896
+ // literals can map to one IR signature while their bodies read
897
+ // different fields.
898
+ const key = `${params.map((s) => typeKey(s.type)).join(",")}=>${typeKey(returnType)}${opts?.extraKey ?? ""}`;
843
899
  let inst = info.instances.get(key);
844
900
  if (!inst) {
845
901
  if (info.instances.size >= MAX_GENERIC_INSTANCES) {
846
902
  L.unsupported("SC1090", blame, `unbounded generic instantiation ('${info.baseName}' exceeded ` +
847
903
  `${MAX_GENERIC_INSTANCES} instances — polymorphic recursion?)`);
848
904
  }
849
- const bindings = makeBindings();
905
+ const tsBindings = opts?.tsBindings ?? new Map();
906
+ const bindings = makeBindings(tsBindings);
850
907
  const rendered = info.typeParams
851
908
  .map((tp) => {
909
+ // A literal-bound keyof parameter renders its literal — the
910
+ // instance is per-literal, and '<…, string>' would misname it.
911
+ const tsBound = tsBindings.get(tp);
912
+ if (info.keyofTps?.has(tp) && tsBound?.isStringLiteralType()) {
913
+ return JSON.stringify(tsBound.value);
914
+ }
852
915
  const bound = bindings.get(tp);
853
916
  return bound ? L.fmt(bound) : tp.name;
854
917
  })
@@ -861,6 +924,7 @@ export function internGenericInstance(L, blame, info, params, returnType, makeBi
861
924
  params,
862
925
  returnType,
863
926
  bindings,
927
+ tsBindings,
864
928
  typeArgsText,
865
929
  };
866
930
  info.instances.set(key, inst);
@@ -868,40 +932,65 @@ export function internGenericInstance(L, blame, info, params, returnType, makeBi
868
932
  }
869
933
  return inst;
870
934
  }
935
+ /** The type parameters of `info` declared with a `keyof` CONSTRAINT
936
+ * (`K extends keyof T`) — the parameters whose bound literal is semantic
937
+ * (the body's `o[k]` reads the named field), computed once per info from
938
+ * the declaration's syntax. */
939
+ export function keyofConstrainedTypeParams(info) {
940
+ if (info.keyofTps)
941
+ return info.keyofTps;
942
+ const out = new Set();
943
+ info.decl.typeParameters?.forEach((tpDecl, i) => {
944
+ const sym = info.typeParams[i];
945
+ if (!sym || tpDecl.constraint === undefined)
946
+ return;
947
+ if (ts.isTypeOperatorNode(tpDecl.constraint) && tpDecl.constraint.operator === ts.SyntaxKind.KeyOfKeyword) {
948
+ out.add(sym);
949
+ }
950
+ });
951
+ info.keyofTps = out;
952
+ return out;
953
+ }
871
954
  /** Type-parameter symbol → concrete IR type for one instantiation.
872
955
  * Explicit type arguments bind directly; the rest come from structurally
873
956
  * matching each DECLARED param/return type (which mentions the type
874
957
  * parameters) against the checker's INSTANTIATED one — the latter is the
875
958
  * former with the substitution applied, so the shapes are parallel by
876
959
  * construction. A type parameter left unbound only matters if the body
877
- * mentions it, where mapType fails with SC2001 (carrying the
878
- * instantiation context). */
879
- export function inferTypeParamBindings(L, expr, info, rsig) {
960
+ * mentions it, where mapType fails and badType names the shape
961
+ * (carrying the instantiation context). */
962
+ export function inferTypeParamBindings(L, expr, info, rsig, tsBindings) {
880
963
  const bindings = new Map();
881
964
  expr.typeArguments?.forEach((ta, i) => {
882
965
  const tp = info.typeParams[i];
883
966
  if (!tp)
884
967
  return;
885
- const mapped = L.mapTypeOf(L.checker.getTypeFromTypeNode(ta));
886
- if (mapped && mapped.kind !== "void")
968
+ const taT = L.checker.getTypeFromTypeNode(ta);
969
+ const mapped = L.mapTypeOf(taT);
970
+ if (mapped && mapped.kind !== "void") {
887
971
  bindings.set(tp, mapped);
972
+ tsBindings?.set(tp, taT);
973
+ }
888
974
  });
889
- unifySignatureBindings(L, info, rsig, bindings);
890
- bindDefaultTypeParams(L, info.typeParams, info.decl.typeParameters, bindings);
975
+ unifySignatureBindings(L, info, rsig, bindings, tsBindings);
976
+ bindDefaultTypeParams(L, info.typeParams, info.decl.typeParameters, bindings, tsBindings);
891
977
  return bindings;
892
978
  }
893
979
  /** Type parameters still unbound after unification take their declared
894
980
  * DEFAULT (`<T = number>`), mapped — the checker already substituted the
895
981
  * default into every resolved signature, so this only fills the bindings
896
982
  * an instance body's mapType consults. */
897
- export function bindDefaultTypeParams(L, typeParams, typeParamDecls, bindings) {
983
+ export function bindDefaultTypeParams(L, typeParams, typeParamDecls, bindings, tsBindings) {
898
984
  typeParamDecls?.forEach((tpDecl, i) => {
899
985
  const tp = typeParams[i];
900
986
  if (!tp || bindings.has(tp) || !tpDecl.defaultType)
901
987
  return;
902
- const mapped = L.mapTypeOf(L.checker.getTypeFromTypeNode(tpDecl.defaultType));
903
- if (mapped && mapped.kind !== "void")
988
+ const defT = L.checker.getTypeFromTypeNode(tpDecl.defaultType);
989
+ const mapped = L.mapTypeOf(defT);
990
+ if (mapped && mapped.kind !== "void") {
904
991
  bindings.set(tp, mapped);
992
+ tsBindings?.set(tp, defT);
993
+ }
905
994
  });
906
995
  }
907
996
  /** The structural half of binding inference: unify the DECLARED signature
@@ -910,7 +999,7 @@ export function bindDefaultTypeParams(L, typeParams, typeParamDecls, bindings) {
910
999
  * completed signature a VALUE reference is pinned to (the contextual
911
1000
  * type's one call signature). Mutates `bindings`; already-bound
912
1001
  * parameters (explicit type arguments) win. */
913
- export function unifySignatureBindings(L, info, rsig, bindings) {
1002
+ export function unifySignatureBindings(L, info, rsig, bindings, tsBindings) {
914
1003
  const tpSet = new Set(info.typeParams);
915
1004
  const seen = new Set(); // recursive declared types must not loop
916
1005
  // The identity-keyed set cannot catch LAZILY INFINITE anonymous types
@@ -927,10 +1016,22 @@ export function unifySignatureBindings(L, info, rsig, bindings) {
927
1016
  return;
928
1017
  if (declared.flags & ts.TypeFlags.TypeParameter) {
929
1018
  const sym = declared.getSymbol();
930
- if (sym && tpSet.has(sym) && !bindings.has(sym)) {
931
- const mapped = L.mapTypeOf(inst);
932
- if (mapped && mapped.kind !== "void")
933
- bindings.set(sym, mapped);
1019
+ if (sym && tpSet.has(sym)) {
1020
+ // The checker type records even when an explicit type argument
1021
+ // already bound the IrType: the raw type is the SAME binding
1022
+ // pre-widening, and first-hit-wins keeps the two maps parallel.
1023
+ // A generic body FORWARDING its own parameter (`pluck`'s
1024
+ // `pick(it, key)` binds pick's K to pluck's K) resolves through
1025
+ // the enclosing instantiation's ts bindings first — the literal
1026
+ // carries through the chain.
1027
+ if (tsBindings && !tsBindings.has(sym)) {
1028
+ tsBindings.set(sym, L.typeParamTsResolver(inst) ?? inst);
1029
+ }
1030
+ if (!bindings.has(sym)) {
1031
+ const mapped = L.mapTypeOf(inst);
1032
+ if (mapped && mapped.kind !== "void")
1033
+ bindings.set(sym, mapped);
1034
+ }
934
1035
  }
935
1036
  return;
936
1037
  }
@@ -1049,6 +1150,13 @@ export function lowerGenericInstance(L, info, inst) {
1049
1150
  L.typeParamBindings = clsBindings
1050
1151
  ? new Map([...clsBindings, ...inst.bindings])
1051
1152
  : inst.bindings;
1153
+ // The ts-level bindings ride along: `T[K]` and literal-keyed `o[k]`
1154
+ // reads inside the body resolve through the bound CHECKER types
1155
+ // (typeParamTsResolver). Generic-class instantiations carry no
1156
+ // ts-level bindings (their type arguments widened at the reference),
1157
+ // so only the method instantiation's own map installs.
1158
+ const prevTsBindings = L.typeParamTsBindings;
1159
+ L.typeParamTsBindings = inst.tsBindings ?? null;
1052
1160
  // Implicit-any instances thread their param bindings through typeOf
1053
1161
  // (the checker reports `any` inside the body — there is no T for
1054
1162
  // mapType to substitute); see the implicit-monomorphization section.
@@ -1186,6 +1294,7 @@ export function lowerGenericInstance(L, info, inst) {
1186
1294
  L.fnStack.pop();
1187
1295
  L.currentClass = prevClass;
1188
1296
  L.typeParamBindings = prevBindings;
1297
+ L.typeParamTsBindings = prevTsBindings;
1189
1298
  L.implicitParamTypes = prevImplicit;
1190
1299
  L.instantiationContext = prevContext;
1191
1300
  L.suppressStats = prevSuppress;
@@ -2591,6 +2700,37 @@ export function lowerCall(L, expr) {
2591
2700
  : "value";
2592
2701
  return { kind: "dynCall", callee, calleeName, args, type: DYN, loc };
2593
2702
  }
2703
+ // `fn(...args)` — a TRAILING spread into an island call: the
2704
+ // engine's own apply (`fn.apply(undefined, argsArray)`); leading
2705
+ // plain arguments prepend through `[l1, l2].concat(argsArray)`
2706
+ // (concat flattens the array argument one level — exactly the
2707
+ // spread). Other spread shapes keep the syntax fence.
2708
+ if (expr.arguments.length > 0 &&
2709
+ ts.isSpreadElement(expr.arguments[expr.arguments.length - 1]) &&
2710
+ expr.arguments.slice(0, -1).every((a) => !ts.isSpreadElement(a))) {
2711
+ const spread = expr.arguments[expr.arguments.length - 1];
2712
+ const spreadV = L.jsvalIn(L.lowerExpr(spread.expression), spread.expression);
2713
+ const leading = expr.arguments.slice(0, -1).map((a) => L.jsvalIn(L.lowerExpr(a), a));
2714
+ const argsArr = leading.length === 0
2715
+ ? spreadV
2716
+ : {
2717
+ kind: "jsOp",
2718
+ op: "callMethod",
2719
+ name: "concat",
2720
+ args: [{ kind: "jsOp", op: "arrLit", args: leading, type: JSVAL, loc }, spreadV],
2721
+ type: JSVAL,
2722
+ loc,
2723
+ };
2724
+ const result = {
2725
+ kind: "jsOp",
2726
+ op: "callMethod",
2727
+ name: "apply",
2728
+ args: [callee, { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc }, argsArr],
2729
+ type: JSVAL,
2730
+ loc,
2731
+ };
2732
+ return islandPrimitiveExit(L, expr, result);
2733
+ }
2594
2734
  const args = expr.arguments.map((a) => L.jsvalIn(L.lowerExpr(a), a));
2595
2735
  const result = { kind: "jsOp", op: "callFn", args: [callee, ...args], type: JSVAL, loc };
2596
2736
  return islandPrimitiveExit(L, expr, result);
@@ -2664,6 +2804,9 @@ export function lowerCall(L, expr) {
2664
2804
  const streamServed = lowerStreamModuleCall(L, expr, bi, loc);
2665
2805
  if (streamServed)
2666
2806
  return streamServed;
2807
+ const fsTs = L.lowerFsToUnixTimestampCall(expr, bi, loc);
2808
+ if (fsTs)
2809
+ return fsTs;
2667
2810
  const builtinFn = builtinModuleFnOf(L, bi.module, bi.member);
2668
2811
  if (!builtinFn) {
2669
2812
  // Typed by @types/node (the fallback declarations only declare
@@ -2893,15 +3036,24 @@ export function lowerCall(L, expr) {
2893
3036
  // closure object, plain C call). Generic functions route through
2894
3037
  // monomorphization (the call targets a per-instantiation instance).
2895
3038
  if (ts.isIdentifier(expr.expression) && !L.isSelfReference(expr.expression)) {
3039
+ // A JS spread argument the compile-time completion cannot take (a
3040
+ // fixed position, a dynamic rest slot) sends the call down the
3041
+ // VALUE path — the runtime-arity lane (lowerSpreadArgsCall) boxes
3042
+ // the declaration's value and applies through a runtime-built
3043
+ // argument list. Typed .ts spreads keep completeArgs' rest packing.
3044
+ const jsSpreadArgs = expr.arguments.some((a) => ts.isSpreadElement(a)) && isJsSourceFile(expr.getSourceFile());
2896
3045
  if (L.isTopLevelFnSymbol(expr.expression) && !L.peekLocal(expr.expression)) {
2897
3046
  // `import g = N.f; g()` — the alias's own source-order guards
2898
3047
  // (a no-op for every non-import= binding).
2899
3048
  fenceEarlyAliasUse(L, expr.expression, expr);
2900
3049
  const generic = L.genericFnOf(expr.expression);
2901
- if (generic)
3050
+ // An implicit-any JS function spread-forwarded into: per-site
3051
+ // monomorphization has no slot for a runtime-length argument
3052
+ // list — the value path's boxed thunk delivers JS arity instead.
3053
+ if (generic && !(jsSpreadArgs && generic.implicitParams))
2902
3054
  return L.lowerGenericCall(expr, generic);
2903
- const sig = L.fnSigOf(expr.expression);
2904
- if (sig) {
3055
+ const sig = generic ? null : L.fnSigOf(expr.expression);
3056
+ if (sig && !(jsSpreadArgs && spreadNeedsRuntimeArity(sig.params, expr.arguments))) {
2905
3057
  L.noteEdge(sig.name);
2906
3058
  const args = L.completeArgs(expr.arguments, sig.params, loc, expr);
2907
3059
  return reconcileOverloadReturn(L, expr, { kind: "call", callee: sig.name, args, type: sig.returnType, loc });
@@ -2925,10 +3077,12 @@ export function lowerCall(L, expr) {
2925
3077
  // the call monomorphizes against it exactly like a generic function
2926
3078
  // declaration and the binding is never read. Symbol identity does
2927
3079
  // the discrimination — a shadowing local has its own symbol, and
2928
- // registered bindings never declare locals or globals.
3080
+ // registered bindings never declare locals or globals. Implicit-any
3081
+ // JS bindings spread-forwarded into skip to the value path (the
3082
+ // runtime-arity lane), like the declaration form above.
2929
3083
  {
2930
3084
  const generic = L.genericFnOf(expr.expression);
2931
- if (generic)
3085
+ if (generic && !(jsSpreadArgs && generic.implicitParams))
2932
3086
  return L.lowerGenericCall(expr, generic);
2933
3087
  }
2934
3088
  }
@@ -3137,6 +3291,9 @@ export function lowerCall(L, expr) {
3137
3291
  // island path (bytes never cross the boundary).
3138
3292
  L.lowerBytesMethodCall(expr, expr.expression) ??
3139
3293
  L.lowerBufferStaticCall(expr, expr.expression) ??
3294
+ // URL.revokeObjectURL's zero-argument contract (the one-argument
3295
+ // form keeps the fence — createObjectURL does too).
3296
+ lowerUrlStaticCall(L, expr, expr.expression) ??
3140
3297
  // Readable.from — the stream classes' one static (before the
3141
3298
  // stdlib chokepoint claims the member).
3142
3299
  lowerStreamStaticCall(L, expr, expr.expression) ??
@@ -3172,6 +3329,9 @@ export function lowerCall(L, expr) {
3172
3329
  lowerSymbolStaticCall(L, expr, expr.expression) ??
3173
3330
  lowerSymbolMethodCall(L, expr, expr.expression) ??
3174
3331
  lowerRegExpStaticCall(L, expr, expr.expression) ??
3332
+ // The composed en-US Intl.NumberFormat form — before the member
3333
+ // fences (the receiver's Intl.NumberFormat type has no mapping).
3334
+ lowerIntlNumberFormatCall(L, expr, expr.expression) ??
3175
3335
  lowerGroupByStaticCall(L, expr, expr.expression) ??
3176
3336
  // Iterator-helper chains rooted at arr.values() — before the
3177
3337
  // array method paths (the terminal names collide with array
@@ -3280,6 +3440,12 @@ export function lowerCall(L, expr) {
3280
3440
  // any-origin value). Spread arguments keep their fence.
3281
3441
  if (callee.type.kind === "dyn") {
3282
3442
  if (expr.arguments.some((a) => ts.isSpreadElement(a))) {
3443
+ // The runtime-arity lane: a dyn callee is already boxed — the
3444
+ // spread-marked dynCall applies through a fresh DOM argument
3445
+ // array (lowerSpreadArgsCall). Sources outside it keep the fence.
3446
+ const spreadServed = lowerSpreadArgsCall(L, expr, callee, loc);
3447
+ if (spreadServed)
3448
+ return spreadServed;
3283
3449
  L.unsupported("SC1090", expr, "spread arguments in calls through 'unknown' values");
3284
3450
  }
3285
3451
  const args = expr.arguments.map((a) => L.lowerExprExpecting(a, DYN));
@@ -3293,6 +3459,42 @@ export function lowerCall(L, expr) {
3293
3459
  if (callee.type.kind !== "func") {
3294
3460
  L.badType(expr.expression, L.typeOf(expr.expression));
3295
3461
  }
3462
+ // A SPREAD argument on a func-typed callee — the rest-forwarding
3463
+ // idiom (`(...args) => from(...args)`): the runtime-arity lane boxes
3464
+ // or marshals the callee and applies through a runtime-built argument
3465
+ // list (lowerSpreadArgsCall). Shapes outside its lanes fall through
3466
+ // to the historical fences.
3467
+ if (expr.arguments.some((a) => ts.isSpreadElement(a))) {
3468
+ const spreadServed = lowerSpreadArgsCall(L, expr, callee, loc);
3469
+ if (spreadServed)
3470
+ return spreadServed;
3471
+ }
3472
+ // An ISLAND-REST func value called directly (`f(1, 2)` where f is the
3473
+ // --dynamic `(...args) =>` lambda): the type SPELLS its trailing
3474
+ // engine-array param — complete the call exactly like completeArgs'
3475
+ // island pack (fixed slots positionally, missing ones with the
3476
+ // engine's undefined, the surplus marshaled into one fresh engine
3477
+ // array). JS arity, no runtime machinery.
3478
+ if (callee.type.rest === true &&
3479
+ callee.type.restAbi === "jsval" &&
3480
+ callee.type.params.length >= 1 &&
3481
+ callee.type.params[callee.type.params.length - 1].kind === "jsval" &&
3482
+ !expr.arguments.some((a) => ts.isSpreadElement(a))) {
3483
+ const fixed = callee.type.params.slice(0, -1);
3484
+ const args = fixed.map((p, i) => {
3485
+ const a = expr.arguments[i];
3486
+ if (a)
3487
+ return L.lowerExprExpecting(a, p);
3488
+ const absent = omittedArgFor(L, p, loc);
3489
+ if (!absent) {
3490
+ L.unsupported("SC1090", expr, "calls omitting a non-optional parameter of the callee's type");
3491
+ }
3492
+ return absent;
3493
+ });
3494
+ const restArgs = expr.arguments.slice(fixed.length).map((a) => L.lowerExprExpecting(a, JSVAL));
3495
+ args.push({ kind: "jsOp", op: "arrLit", args: restArgs, type: JSVAL, loc });
3496
+ return { kind: "callValue", callee, args, type: callee.type.ret, loc };
3497
+ }
3296
3498
  // A JS call with MORE arguments than the callee's lowered signature
3297
3499
  // (`cb(1, 'x')` where the mustCall wrapper's inferred type declared
3298
3500
  // fewer params — tsc's JS world doesn't police arity): ride the
@@ -3330,6 +3532,101 @@ export function lowerCall(L, expr) {
3330
3532
  }
3331
3533
  return { kind: "callValue", callee, args, type: callee.type.ret, loc };
3332
3534
  }
3535
+ /** The RUNTIME-ARITY spread call — `f(...args)`, the rest-forwarding idiom
3536
+ * (`const f = (...args) => from(...args)`): a spread whose length is a
3537
+ * runtime fact has no home in the compile-time completion, so the call
3538
+ * rides a dynamic boundary instead. Two lanes, picked by the spread
3539
+ * source's tier:
3540
+ *
3541
+ * - CHECKED-DYNAMIC (dyn spread sources — a JS rest binding, a dyn
3542
+ * value): box the callee (dynFrom; a dyn callee is already boxed),
3543
+ * convert every argument into dyn, and emit the spread-marked dynCall
3544
+ * — the emitters build one fresh DOM argument array (spreads flatten
3545
+ * left-to-right, non-iterables throw V8's TypeError) and apply through
3546
+ * it; the boxed thunk delivers JS arity exactly. Result dyn, checked
3547
+ * per use like every any-origin value.
3548
+ * - ISLAND (a jsval spread source — the --dynamic rest binding is the
3549
+ * engine's own arguments array): marshal the callee in and emit jsOp
3550
+ * callSpread — the prelude helper's REAL `f(...pre, ...s)`, so
3551
+ * iterator protocols and the not-iterable TypeError are the engine's
3552
+ * own. One trailing spread after the fixed arguments is the modeled
3553
+ * shape (exactly the forwarding idiom).
3554
+ *
3555
+ * Answers null when neither lane fits (typed .ts spreads keep
3556
+ * completeArgs' rest packing and its fences). JS sources only — the
3557
+ * same guard as the over-arity dynCall precedent. */
3558
+ export function lowerSpreadArgsCall(L, expr, callee, loc) {
3559
+ if (!isJsSourceFile(expr.getSourceFile()))
3560
+ return null;
3561
+ if (!expr.arguments.some((a) => ts.isSpreadElement(a)))
3562
+ return null;
3563
+ if (callee.type.kind !== "dyn" && callee.type.kind !== "func" && callee.type.kind !== "jsval")
3564
+ return null;
3565
+ const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression)
3566
+ ? expr.expression.getText()
3567
+ : ts.isIdentifier(expr.expression)
3568
+ ? expr.expression.text
3569
+ : "value";
3570
+ // Lower every argument ONCE, in source order (the IR nests them in
3571
+ // exactly this order, so runtime evaluation order is JS's).
3572
+ const parts = expr.arguments.map((a) => ts.isSpreadElement(a)
3573
+ ? { spreadOf: a.expression, node: null, v: L.lowerExpr(a.expression) }
3574
+ : { spreadOf: null, node: a, v: L.lowerExpr(a) });
3575
+ const spreadParts = parts.filter((p) => p.spreadOf !== null);
3576
+ const getR = (id) => L.shapes.get(id);
3577
+ const getU = (id) => L.unions.get(id);
3578
+ const anyJsvalSpread = spreadParts.some((p) => p.v.type.kind === "jsval");
3579
+ if (!anyJsvalSpread &&
3580
+ (callee.type.kind === "dyn" || (callee.type.kind === "func" && canBoxFuncIntoDyn(callee.type, getR, getU))) &&
3581
+ spreadParts.every((p) => p.v.type.kind === "dyn" || canConvertToDyn(p.v.type, getR, getU))) {
3582
+ const args = [];
3583
+ const spreads = [];
3584
+ for (const p of parts) {
3585
+ if (p.spreadOf !== null) {
3586
+ // The spelling rides along for V8's nullish spread-call
3587
+ // TypeError ("v is not iterable (cannot read property ...)").
3588
+ spreads.push({ arg: args.length, what: p.spreadOf.getText() });
3589
+ args.push(L.coerceInto(p.spreadOf, p.v, DYN));
3590
+ }
3591
+ else {
3592
+ args.push(L.coerceInto(p.node, p.v, DYN));
3593
+ }
3594
+ }
3595
+ const boxed = callee.type.kind === "dyn" ? callee : L.coerceInto(expr.expression, callee, DYN);
3596
+ return { kind: "dynCall", callee: boxed, calleeName, args, spreads, type: DYN, loc };
3597
+ }
3598
+ if (spreadParts.length > 0 &&
3599
+ spreadParts.every((p) => p.v.type.kind === "jsval") &&
3600
+ (callee.type.kind === "jsval" || callee.type.kind === "func")) {
3601
+ if (spreadParts.length !== 1 || parts[parts.length - 1].spreadOf === null) {
3602
+ L.unsupported("SC1090", expr, "spread arguments before positional arguments in island calls (one trailing spread after the fixed arguments is the supported form)");
3603
+ }
3604
+ const f = L.coerceInto(expr.expression, callee, JSVAL);
3605
+ const pre = parts
3606
+ .slice(0, -1)
3607
+ .map((p) => L.coerceInto(p.node, p.v, JSVAL));
3608
+ const preArr = { kind: "jsOp", op: "arrLit", args: pre, type: JSVAL, loc };
3609
+ const last = parts[parts.length - 1];
3610
+ // The spelling rides in `name` for V8's nullish spread-call
3611
+ // TypeError ("v is not iterable (cannot read property ...)").
3612
+ return { kind: "jsOp", op: "callSpread", name: last.spreadOf.getText(), args: [f, preArr, last.v], type: JSVAL, loc };
3613
+ }
3614
+ // No lane fits (a spread source outside both tiers, a callee neither
3615
+ // boxable nor marshalable, mixed dyn/jsval spreads): fence HERE — the
3616
+ // arguments are already lowered, and falling back to the historical
3617
+ // per-site fences would lower them a second time (duplicate lambda
3618
+ // lifts, duplicated diagnostics).
3619
+ L.unsupported("SC1090", expr, "spread arguments");
3620
+ }
3621
+ /** True when a spread argument lands where the compile-time completion
3622
+ * cannot take it — a FIXED parameter position, or a dynamic rest slot
3623
+ * (dynRest/islandRest, whose packs are built per-argument): the shapes
3624
+ * the runtime-arity lane (lowerSpreadArgsCall) serves. Typed `rest`
3625
+ * slots keep completeArgs' same-element spread packing. */
3626
+ export function spreadNeedsRuntimeArity(shapes, argNodes) {
3627
+ const restAt = shapes.findIndex((s) => s.mode === "rest" || s.mode === "dynRest" || s.mode === "islandRest");
3628
+ return argNodes.some((a, i) => ts.isSpreadElement(a) && (restAt < 0 || i < restAt || shapes[restAt].mode !== "rest"));
3629
+ }
3333
3630
  /** METHOD calls on dyn receivers (`pkg.name.replace(...)`, `rawName.split`,
3334
3631
  * `ws.packages.filter(...)` — JSON.parse-derived values): validate the
3335
3632
  * receiver's DOM kind, extract, and ride the STATIC method machinery — the
@@ -3868,6 +4165,32 @@ export function lowerPrimitiveProtoCall(L, call, recv, name, memberSym) {
3868
4165
  const fn = name === "toExponential" ? "num.toExponential" : "num.toFixed0";
3869
4166
  return { kind: "libCall", fn, args: [operand], type: STRING, loc };
3870
4167
  }
4168
+ // Number.prototype.toLocaleString("en-US") — the spec makes it
4169
+ // NumberFormat(locale).format(this), so the en-US embedded formatter
4170
+ // answers exactly. The unlowered forms fence by NAME: no locale (the
4171
+ // host environment's default, which a compiled binary cannot carry),
4172
+ // other locales (ICU data the binary does not embed), options bags.
4173
+ if (name === "toLocaleString" && recvKind === "f64") {
4174
+ if (call.arguments.length === 0) {
4175
+ L.noLowering("Number.prototype.toLocaleString without a locale", call, "the default locale is the host environment's, which a compiled binary cannot carry — " +
4176
+ 'pass it explicitly: x.toLocaleString("en-US")');
4177
+ }
4178
+ if (call.arguments.length > 1) {
4179
+ L.noLowering("Number.prototype.toLocaleString with an options bag", call, "the embedded data covers DEFAULT options only (decimal notation, up to 3 fraction " +
4180
+ 'digits, grouping) — x.toLocaleString("en-US")');
4181
+ }
4182
+ const locNode = call.arguments[0];
4183
+ if (ts.isSpreadElement(locNode) || !ts.isStringLiteralLike(locNode) || locNode.text !== "en-US") {
4184
+ L.noLowering(!ts.isSpreadElement(locNode) && ts.isStringLiteralLike(locNode)
4185
+ ? `Number.prototype.toLocaleString at locale "${locNode.text}"`
4186
+ : "Number.prototype.toLocaleString with a non-literal locale", locNode, '"en-US" (Node\'s default-build locale) is the one locale whose data the runtime embeds — ' +
4187
+ "everything else is ICU data the binary does not carry");
4188
+ }
4189
+ const operand = L.lowerExpr(recv);
4190
+ if (operand.type.kind !== "f64")
4191
+ return null;
4192
+ return { kind: "libCall", fn: "intl.numFormatEnUs", args: [operand], type: STRING, loc };
4193
+ }
3871
4194
  if (name === "charAt" && recvKind === "string" && call.arguments.length === 1 &&
3872
4195
  !ts.isSpreadElement(call.arguments[0])) {
3873
4196
  const receiver = L.lowerExpr(recv);
@@ -4044,11 +4367,26 @@ export function lowerTaggedTemplate(L, expr) {
4044
4367
  : expr.template.templateSpans.map((s) => s.expression);
4045
4368
  // Island tags (--dynamic): the engine call forms, mirroring lowerCall's
4046
4369
  // island paths — a property-access tag is a method call (this = the
4047
- // receiver, JS-exact), any other island tag a function call.
4370
+ // receiver, JS-exact), any other island tag a function call. The
4371
+ // strings argument builds ENGINE-NATIVE with its `.raw` property (the
4372
+ // tplStrings op): a JSON marshal would drop `.raw`, and tags dispatch
4373
+ // on it (the outdent idiom treats a raw-less argument as its OPTIONS
4374
+ // form and answers a function). A fresh array per evaluation — tags
4375
+ // caching by strings identity re-compute per call (SEMANTICS.md).
4376
+ const islandStrings = () => ({
4377
+ kind: "jsOp",
4378
+ op: "tplStrings",
4379
+ args: [
4380
+ ...pieces.map((p) => ({ kind: "jsMarshal", value: { kind: "strLit", value: p.text, type: STRING, loc }, type: JSVAL, loc })),
4381
+ ...pieces.map((p) => ({ kind: "jsMarshal", value: { kind: "strLit", value: templateRawTextOf(p), type: STRING, loc }, type: JSVAL, loc })),
4382
+ ],
4383
+ type: JSVAL,
4384
+ loc,
4385
+ });
4048
4386
  if (ts.isPropertyAccessExpression(expr.tag) && L.isIslandExpr(expr.tag.expression)) {
4049
4387
  const receiver = L.lowerExpr(expr.tag.expression);
4050
4388
  const args = [
4051
- L.jsvalIn(strings, expr.template),
4389
+ islandStrings(),
4052
4390
  ...values.map((a) => L.jsvalIn(L.lowerExpr(a), a)),
4053
4391
  ];
4054
4392
  return {
@@ -4059,7 +4397,7 @@ export function lowerTaggedTemplate(L, expr) {
4059
4397
  if (L.isIslandExpr(expr.tag)) {
4060
4398
  const callee = L.lowerExpr(expr.tag);
4061
4399
  const args = [
4062
- L.jsvalIn(strings, expr.template),
4400
+ islandStrings(),
4063
4401
  ...values.map((a) => L.jsvalIn(L.lowerExpr(a), a)),
4064
4402
  ];
4065
4403
  return { kind: "jsOp", op: "callFn", args: [callee, ...args], type: JSVAL, loc };
@@ -4247,6 +4585,7 @@ export function lambdaSignature(L, node) {
4247
4585
  // use a rest parameter. Arrows never claim it (JS: an arrow's
4248
4586
  // `arguments` is the enclosing function's).
4249
4587
  const hasDynRest = shapes.some((s) => s.mode === "dynRest");
4588
+ const hasIslandRest = shapes.some((s) => s.mode === "islandRest");
4250
4589
  const usesArguments = !hasDynRest &&
4251
4590
  !ts.isArrowFunction(node) &&
4252
4591
  isJsSourceFile(node.getSourceFile()) &&
@@ -4258,9 +4597,14 @@ export function lambdaSignature(L, node) {
4258
4597
  shapes,
4259
4598
  funcType: {
4260
4599
  kind: "func",
4600
+ // dynRest is EXCLUDED (the boxed thunk fills the trailing DOM
4601
+ // array — no spelled slot); islandRest is INCLUDED (the trailing
4602
+ // jsval param IS the engine arguments array, the REST host-call
4603
+ // adapter's one uniform shape).
4261
4604
  params: shapes.filter((s) => s.mode !== "dynRest").map((s) => s.type),
4262
4605
  ret,
4263
- ...(hasDynRest || usesArguments ? { rest: true } : {}),
4606
+ ...(hasDynRest || usesArguments || hasIslandRest ? { rest: true } : {}),
4607
+ ...(hasIslandRest ? { restAbi: "jsval" } : {}),
4264
4608
  },
4265
4609
  };
4266
4610
  }
@@ -4331,13 +4675,14 @@ export function lowerLambda(L, node) {
4331
4675
  if (isGenerator && funcType.ret.kind === "generator") {
4332
4676
  fnCtx.generator = { yieldT: funcType.ret.yieldT, nextT: funcType.ret.nextT };
4333
4677
  }
4678
+ const diagsBefore = L.diags.length;
4334
4679
  L.fnStack.push(fnCtx);
4335
4680
  try {
4336
4681
  const { params, prologue } = L.declareParams(node.parameters, shapes);
4337
4682
  // The VARIADIC `arguments` form (rest-marked with no declared rest
4338
4683
  // param): a synthetic trailing DOM-array param carries the call's
4339
4684
  // arguments; `arguments` reads resolve to it (identifier lowering).
4340
- if (funcType.rest && !shapes.some((s) => s.mode === "dynRest")) {
4685
+ if (funcType.rest && !shapes.some((s) => s.mode === "dynRest" || s.mode === "islandRest")) {
4341
4686
  const argsLocal = L.declareHiddenLocal("%arguments", DYN);
4342
4687
  params.push({ localId: argsLocal.id, name: "%arguments", type: DYN });
4343
4688
  fnCtx.argumentsLocal = argsLocal;
@@ -4416,6 +4761,62 @@ export function lowerLambda(L, node) {
4416
4761
  L.liftedFns.push(lifted);
4417
4762
  return { kind: "closure", fnName, captures: ctx.captureSources, type: funcType, loc };
4418
4763
  }
4764
+ catch (e) {
4765
+ // JS sources defer LAMBDA poisons like function declarations
4766
+ // (lowerFunction's catch, lambda form — entry the function-level
4767
+ // deferral): a fenced concise body (`(list) => new Intl.ListFormat
4768
+ // (...).format(list)` — the error-message list-join idiom) would
4769
+ // otherwise poison the ENCLOSING statement, stopping module init
4770
+ // where Node only stops when the lambda is CALLED. The value
4771
+ // compiles as a capture-free closure over a runtimeFence body —
4772
+ // calling throws the first captured diagnostic at its source
4773
+ // position. ICEs (SC9001) stay compile errors, exactly like
4774
+ // lowerStmts; probe mode (diagSink) keeps the poison.
4775
+ if (!(e instanceof PoisonError))
4776
+ throw e;
4777
+ if (!isJsSourceFile(node.getSourceFile()) ||
4778
+ L.diagSink !== null ||
4779
+ L.diags.length <= diagsBefore ||
4780
+ L.diags.slice(diagsBefore).some((d) => d.code === "SC9001")) {
4781
+ throw e;
4782
+ }
4783
+ const captured = L.diags.splice(diagsBefore);
4784
+ L.runtimeFences.push(...captured);
4785
+ const first = captured[0];
4786
+ const pos = ts.getLineAndCharacterOfPosition(L.program.getSourceFile(first.loc.file) ?? node.getSourceFile(), first.loc.start);
4787
+ const params = funcType.params.map((t, i) => ({ localId: `%pf${i}`, name: `%pf${i}`, type: t }));
4788
+ // A REST-MARKED value type hides one synthetic trailing DOM-array
4789
+ // param in the lifted function (the boxed call thunk fills it) —
4790
+ // the fence lambda must spell that slot too or the validator's
4791
+ // closure-signature check trips (SC9001). Island rest types SPELL
4792
+ // their trailing engine-array param, so funcType.params already
4793
+ // covers those.
4794
+ if (funcType.rest === true && funcType.restAbi !== "jsval") {
4795
+ params.push({ localId: "%pfrest", name: "%pfrest", type: DYN });
4796
+ }
4797
+ const lifted = {
4798
+ name: fnName,
4799
+ params,
4800
+ returnType: bodyReturn,
4801
+ locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
4802
+ captures: [],
4803
+ body: [
4804
+ {
4805
+ kind: "runtimeFence",
4806
+ code: first.code,
4807
+ message: `${first.message} [${first.code} at ${first.loc.file}:${pos.line + 1}]`,
4808
+ loc,
4809
+ },
4810
+ ],
4811
+ loc,
4812
+ };
4813
+ if (isAsync)
4814
+ lifted.async = true;
4815
+ if (fnCtx.generator)
4816
+ lifted.generator = fnCtx.generator;
4817
+ L.liftedFns.push(lifted);
4818
+ return { kind: "closure", fnName, captures: [], type: funcType, loc };
4819
+ }
4419
4820
  finally {
4420
4821
  L.fnStack.pop();
4421
4822
  }
@@ -5304,8 +5705,8 @@ function filterNarrowHelper(L, test, elem, outElem, tag, loc) {
5304
5705
  * (Symbol.iterator, Symbol.asyncIterator, Symbol.toStringTag, ...) —
5305
5706
  * language-level protocol uses (for-of, template literals) already
5306
5707
  * compile through their constructs without reifying the symbol, so the
5307
- * VALUE forms fence with a named message rather than the generic
5308
- * SymbolConstructor SC2001. */
5708
+ * VALUE forms fence with a named message rather than a blanket
5709
+ * SymbolConstructor type fence. */
5309
5710
  function lowerSymbolStaticCall(L, call, access) {
5310
5711
  if (call.questionDotToken || access.questionDotToken)
5311
5712
  return null;
@@ -5435,6 +5836,110 @@ export function recordKeysArrayCall(L, receiver, argIr, shape, loc) {
5435
5836
  }
5436
5837
  return { kind: "call", callee: helper, args: [receiver], type: resultT, loc };
5437
5838
  }
5839
+ /** Interned `%obj.hasOwn.<n>(r, k)` — Object.hasOwn's membership walk
5840
+ * over a signature-free record shape: the key compares against each
5841
+ * declared field name, undefined-armed fields answering by their tag
5842
+ * (a key is own exactly when Object.keys would list it — the two share
5843
+ * the guard), everything else true, no match false. */
5844
+ function recordHasOwnHelper(L, shapeId, loc) {
5845
+ const key = `obj.hasOwn:${shapeId}`;
5846
+ const existing = L.arrHofHelpers.get(key);
5847
+ if (existing)
5848
+ return existing;
5849
+ const helper = `%obj.hasOwn.${L.arrHofHelpers.size}`;
5850
+ L.arrHofHelpers.set(key, helper);
5851
+ const shape = L.shapes.get(shapeId);
5852
+ const recT = { kind: "record", shapeId };
5853
+ const rRef = { kind: "varRef", localId: "r.0", type: recT, loc };
5854
+ const kRef = { kind: "varRef", localId: "k.0", type: STRING, loc };
5855
+ const body = [];
5856
+ for (const f of shape.fields) {
5857
+ const utag = f.type.kind === "union" ? L.armTag(f.type.unionId, UNDEFINED_T) : -1;
5858
+ const answer = utag >= 0 && f.type.kind === "union"
5859
+ ? {
5860
+ kind: "unionIsTag",
5861
+ unionId: f.type.unionId,
5862
+ tag: utag,
5863
+ negated: true,
5864
+ value: { kind: "recordGet", obj: rRef, shapeId, field: f.name, type: f.type, loc },
5865
+ type: BOOL,
5866
+ loc,
5867
+ }
5868
+ : { kind: "boolLit", value: true, type: BOOL, loc };
5869
+ body.push({
5870
+ kind: "if",
5871
+ cond: { kind: "strEq", negated: false, left: kRef, right: { kind: "strLit", value: f.name, type: STRING, loc }, type: BOOL, loc },
5872
+ then: [{ kind: "return", value: answer, loc }],
5873
+ else_: null,
5874
+ loc,
5875
+ });
5876
+ }
5877
+ body.push({ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc });
5878
+ L.liftedFns.push({
5879
+ name: helper,
5880
+ params: [
5881
+ { localId: "r.0", name: "r", type: recT },
5882
+ { localId: "k.0", name: "k", type: STRING },
5883
+ ],
5884
+ returnType: BOOL,
5885
+ locals: [
5886
+ { id: "r.0", name: "r", type: recT, mutable: true },
5887
+ { id: "k.0", name: "k", type: STRING, mutable: false },
5888
+ ],
5889
+ body,
5890
+ loc,
5891
+ });
5892
+ return helper;
5893
+ }
5894
+ /** Interned `%obj.assign.<n>(t, s)` — Object.assign's per-field copy
5895
+ * over signature-free records (every source field lands on a same-named,
5896
+ * same-typed target field — the caller's gate): undefined-armed source
5897
+ * fields copy behind the not-undefined guard, everything else straight,
5898
+ * and the TARGET returns (JS's aliasing). */
5899
+ function recordAssignHelper(L, targetShapeId, srcShapeId, loc) {
5900
+ const key = `obj.assign:${targetShapeId}:${srcShapeId}`;
5901
+ const existing = L.arrHofHelpers.get(key);
5902
+ if (existing)
5903
+ return existing;
5904
+ const helper = `%obj.assign.${L.arrHofHelpers.size}`;
5905
+ L.arrHofHelpers.set(key, helper);
5906
+ const sShape = L.shapes.get(srcShapeId);
5907
+ const tT = { kind: "record", shapeId: targetShapeId };
5908
+ const sT = { kind: "record", shapeId: srcShapeId };
5909
+ const tRef = { kind: "varRef", localId: "t.0", type: tT, loc };
5910
+ const sRef = { kind: "varRef", localId: "s.0", type: sT, loc };
5911
+ const body = [];
5912
+ for (const f of sShape.fields) {
5913
+ const get = { kind: "recordGet", obj: sRef, shapeId: srcShapeId, field: f.name, type: f.type, loc };
5914
+ const set = { kind: "recordSet", obj: tRef, shapeId: targetShapeId, field: f.name, value: get, loc };
5915
+ const utag = f.type.kind === "union" ? L.armTag(f.type.unionId, UNDEFINED_T) : -1;
5916
+ body.push(utag >= 0 && f.type.kind === "union"
5917
+ ? {
5918
+ kind: "if",
5919
+ cond: { kind: "unionIsTag", unionId: f.type.unionId, tag: utag, negated: true, value: get, type: BOOL, loc },
5920
+ then: [set],
5921
+ else_: null,
5922
+ loc,
5923
+ }
5924
+ : set);
5925
+ }
5926
+ body.push({ kind: "return", value: tRef, loc });
5927
+ L.liftedFns.push({
5928
+ name: helper,
5929
+ params: [
5930
+ { localId: "t.0", name: "t", type: tT },
5931
+ { localId: "s.0", name: "s", type: sT },
5932
+ ],
5933
+ returnType: tT,
5934
+ locals: [
5935
+ { id: "t.0", name: "t", type: tT, mutable: true },
5936
+ { id: "s.0", name: "s", type: sT, mutable: true },
5937
+ ],
5938
+ body,
5939
+ loc,
5940
+ });
5941
+ return helper;
5942
+ }
5438
5943
  /** The `Iterator` global's statics (ES2025 — Iterator.from, and the
5439
5944
  * abstract constructor as a value): no first-class iterator objects
5440
5945
  * exist here, so every member fences with the working spelling named
@@ -5469,12 +5974,184 @@ function lowerRegExpStaticCall(L, call, access) {
5469
5974
  L.badType(call.arguments[0], L.typeOf(call.arguments[0]));
5470
5975
  return { kind: "libCall", fn: "regexp.escape", args: [arg], type: STRING, loc: locOf(call) };
5471
5976
  }
5977
+ /** The composed en-US Intl.NumberFormat form: `new Intl.NumberFormat(
5978
+ * "en-US").format(x)` (and the callable spelling without `new` — the
5979
+ * spec makes them the same formatter). Only the COMPOSED form lowers —
5980
+ * formatter values have no representation — and only the one locale
5981
+ * whose data the runtime embeds, with default options: decimal
5982
+ * notation, 0–3 fraction digits rounded half-up on the shortest
5983
+ * round-tripping decimal (ICU's rounding input — format(1.0005) is
5984
+ * "1.001" though toFixed(3) answers "1.000"), "," grouping. The
5985
+ * unlowered forms fence by NAME (no locale — the host environment's
5986
+ * default, which a compiled binary cannot carry; other locales — ICU
5987
+ * data the binary does not embed; options bags; non-number arguments).
5988
+ * Null when the callee isn't a NumberFormat-construction .format. */
5989
+ function lowerIntlNumberFormatCall(L, call, access) {
5990
+ if (call.questionDotToken || access.questionDotToken)
5991
+ return null;
5992
+ if (access.name.text !== "format")
5993
+ return null;
5994
+ let recv = access.expression;
5995
+ while (ts.isParenthesizedExpression(recv))
5996
+ recv = recv.expression;
5997
+ let ctorArgs;
5998
+ if (ts.isNewExpression(recv) || (ts.isCallExpression(recv) && !recv.questionDotToken)) {
5999
+ const ctor = recv.expression;
6000
+ if (!ts.isPropertyAccessExpression(ctor) || ctor.questionDotToken ||
6001
+ ctor.name.text !== "NumberFormat" || !L.isStdlibGlobal(ctor.expression, "Intl")) {
6002
+ return null;
6003
+ }
6004
+ ctorArgs = recv.arguments ?? [];
6005
+ }
6006
+ else {
6007
+ return null;
6008
+ }
6009
+ const loc = locOf(call);
6010
+ if (ctorArgs.length === 0) {
6011
+ L.noLowering("Intl.NumberFormat without a locale", recv, "the default locale is the host environment's, which a compiled binary cannot carry — " +
6012
+ 'pass it explicitly: new Intl.NumberFormat("en-US").format(x)');
6013
+ }
6014
+ if (ctorArgs.length > 1) {
6015
+ L.noLowering("Intl.NumberFormat with an options bag", ctorArgs[1], "the embedded data covers DEFAULT options only (decimal notation, up to 3 fraction digits, " +
6016
+ 'grouping) — new Intl.NumberFormat("en-US").format(x)');
6017
+ }
6018
+ const locArg = ctorArgs[0];
6019
+ if (ts.isSpreadElement(locArg) || !ts.isStringLiteralLike(locArg) || locArg.text !== "en-US") {
6020
+ L.noLowering(!ts.isSpreadElement(locArg) && ts.isStringLiteralLike(locArg)
6021
+ ? `Intl.NumberFormat at locale "${locArg.text}"`
6022
+ : "Intl.NumberFormat with a non-literal locale", locArg, '"en-US" (Node\'s default-build locale) is the one locale whose data the runtime embeds — ' +
6023
+ "everything else is ICU data the binary does not carry");
6024
+ }
6025
+ if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0])) {
6026
+ L.noLowering(`Intl.NumberFormat("en-US").format with ${call.arguments.length} arguments`, call);
6027
+ }
6028
+ const argNode = call.arguments[0];
6029
+ if (L.mapTypeOf(L.typeOf(argNode))?.kind !== "f64") {
6030
+ L.noLowering(`Intl.NumberFormat("en-US").format over a '${L.checker.typeToString(L.typeOf(argNode))}'`, argNode, "a number argument is the lowered form (bigint and numeric-string inputs have no representation)");
6031
+ }
6032
+ const arg = L.lowerExprExpecting(argNode, F64);
6033
+ if (arg.type.kind !== "f64")
6034
+ L.badType(argNode, L.typeOf(argNode));
6035
+ return { kind: "libCall", fn: "intl.numFormatEnUs", args: [arg], type: STRING, loc };
6036
+ }
6037
+ /** Object.is over statically disjoint kinds: the constant false, with
6038
+ * both operands still evaluated for their effects (droppable statics
6039
+ * fold away — JS evaluates arguments, but nothing observes a pure one). */
6040
+ function objectIsDisjointFalse(left, right, loc) {
6041
+ const stmts = [];
6042
+ for (const e of [left, right]) {
6043
+ if (!droppableStatic(e))
6044
+ stmts.push({ kind: "exprStmt", expr: e, loc });
6045
+ }
6046
+ const answer = { kind: "boolLit", value: false, type: BOOL, loc };
6047
+ if (stmts.length === 0)
6048
+ return answer;
6049
+ return { kind: "seqExpr", stmts, result: answer, type: BOOL, loc };
6050
+ }
5472
6051
  function lowerObjectStaticCall(L, call, access) {
5473
6052
  if (call.questionDotToken || access.questionDotToken)
5474
6053
  return null;
5475
6054
  if (!L.isStdlibGlobal(access.expression, "Object"))
5476
6055
  return null;
5477
6056
  const member = access.name.text;
6057
+ // Object.is — the spec's SameValue over the static kinds. Number
6058
+ // pairs take the runtime SameValue (NaN equals NaN, +0 differs from
6059
+ // -0 — the two divergences from ===); every other supported pair
6060
+ // rides exactly the strict-equality machinery, whose answers
6061
+ // SameValue shares: strings by bytes, bools by value, unit literals
6062
+ // by tag, unions per arm (a number arm's payload compare upgrades to
6063
+ // SameValue via unionEq's flag), and the reference kinds by pointer
6064
+ // identity. Statically DISJOINT kind pairs answer the constant false
6065
+ // with the operands still evaluated (tsc admits any pair — Object.is
6066
+ // is (any, any) — and JS evaluates the arguments either way).
6067
+ // dyn/jsval operands keep strict equality's stance: validate first.
6068
+ if (member === "is") {
6069
+ if (call.arguments.length !== 2 || call.arguments.some((a) => ts.isSpreadElement(a))) {
6070
+ L.noLowering(`Object.is with ${call.arguments.length} arguments`, call, "exactly two arguments are the lowered form (JS treats a missing one as undefined — pass it explicitly)");
6071
+ }
6072
+ const loc = locOf(call);
6073
+ const leftNode = call.arguments[0];
6074
+ const rightNode = call.arguments[1];
6075
+ const left = L.lowerExpr(leftNode);
6076
+ const right = L.lowerExpr(rightNode);
6077
+ const lk = left.type.kind;
6078
+ const rk = right.type.kind;
6079
+ if (lk === "f64" && rk === "f64") {
6080
+ return { kind: "libCall", fn: "num.sameValue", args: [left, right], type: BOOL, loc };
6081
+ }
6082
+ if (left.type.kind === "string" && right.type.kind === "string") {
6083
+ return { kind: "strEq", negated: false, left, right, type: BOOL, loc };
6084
+ }
6085
+ if (lk === "bool" && rk === "bool") {
6086
+ return { kind: "bin", op: "===", left, right, type: BOOL, loc };
6087
+ }
6088
+ const unitTest = L.lowerUnitComparison(left, right, false, loc);
6089
+ if (unitTest)
6090
+ return unitTest;
6091
+ if (lk === "dyn" || rk === "dyn" || lk === "jsval" || rk === "jsval") {
6092
+ L.noLowering("Object.is over a dynamic operand", call, "validate/narrow the value first (strict equality's rule) — SameValue only differs from === on numbers (NaN, ±0)");
6093
+ }
6094
+ if (left.type.kind === "union" || right.type.kind === "union") {
6095
+ const ut = left.type.kind === "union" ? left.type : right.type;
6096
+ const bothUnion = left.type.kind === "union" && right.type.kind === "union";
6097
+ const sameUnion = bothUnion && typeEquals(left.type, right.type);
6098
+ if ((sameUnion || !bothUnion) && L.eqComparableUnion(ut.unionId)) {
6099
+ const plain = left.type.kind === "union" ? right : left;
6100
+ const arms = L.unions.get(ut.unionId)?.arms ?? [];
6101
+ // The plain side wraps into the union exactly like === when the
6102
+ // union holds its type; a plain PRIMITIVE the union has no arm
6103
+ // for is the disjoint constant false (coercing it would strand).
6104
+ if (bothUnion || arms.some((a) => typeEquals(a, plain.type))) {
6105
+ const sameValue = arms.some((a) => a.kind === "f64");
6106
+ return {
6107
+ kind: "unionEq",
6108
+ unionId: ut.unionId,
6109
+ negated: false,
6110
+ sameValue,
6111
+ left: L.coerceInto(leftNode, left, ut),
6112
+ right: L.coerceInto(rightNode, right, ut),
6113
+ type: BOOL,
6114
+ loc,
6115
+ };
6116
+ }
6117
+ if (plain.type.kind === "f64" || plain.type.kind === "string" ||
6118
+ plain.type.kind === "bool" || isUnitType(plain.type)) {
6119
+ return objectIsDisjointFalse(left, right, loc);
6120
+ }
6121
+ }
6122
+ L.noLowering("Object.is over these union operands", call, `union-typed comparisons need one comparable shape (${NARROW_FIRST})`);
6123
+ }
6124
+ // Reference kinds: pointer identity — exactly strict equality
6125
+ // (hierarchy-related classes widen the derived side first).
6126
+ let idLeft = left;
6127
+ let idRight = right;
6128
+ if (left.type.kind === "object" && right.type.kind === "object") {
6129
+ if (L.isSubclassOf(left.type.className, right.type.className)) {
6130
+ idLeft = L.upcastTo(left, right.type.className);
6131
+ }
6132
+ else if (L.isSubclassOf(right.type.className, left.type.className)) {
6133
+ idRight = L.upcastTo(right, left.type.className);
6134
+ }
6135
+ }
6136
+ if ((idLeft.type.kind === "func" && idRight.type.kind === "func") ||
6137
+ (idLeft.type.kind === "classval" && idRight.type.kind === "classval")) {
6138
+ return { kind: "bin", op: "===", left: idLeft, right: idRight, type: BOOL, loc };
6139
+ }
6140
+ if ((idLeft.type.kind === "array" || idLeft.type.kind === "map" ||
6141
+ idLeft.type.kind === "set" || idLeft.type.kind === "object" ||
6142
+ idLeft.type.kind === "record" || idLeft.type.kind === "symbol" ||
6143
+ idLeft.type.kind === "bytes" || idLeft.type.kind === "promise") &&
6144
+ typeEquals(idLeft.type, idRight.type)) {
6145
+ return { kind: "bin", op: "===", left: idLeft, right: idRight, type: BOOL, loc };
6146
+ }
6147
+ // Statically disjoint pairs with a primitive/unit side: SameValue
6148
+ // never crosses kinds, so the answer is the constant false.
6149
+ const disjoint = new Set(["f64", "string", "bool", "undefinedT", "nullT"]);
6150
+ if (lk !== rk && (disjoint.has(lk) || disjoint.has(rk))) {
6151
+ return objectIsDisjointFalse(left, right, loc);
6152
+ }
6153
+ L.noLowering(`Object.is over '${L.fmt(left.type)}' and '${L.fmt(right.type)}' operands`, call, "the operands must share one comparable kind (numbers, strings, booleans, units, one union shape, or one reference type)");
6154
+ }
5478
6155
  // `Object.assign(fn, { props })` whose RESULT type maps to the hybrid
5479
6156
  // (function-with-properties) record: the chalk-shape CONSTRUCTOR.
5480
6157
  if (member === "assign") {
@@ -5504,6 +6181,53 @@ function lowerObjectStaticCall(L, call, access) {
5504
6181
  const merged = lowerObjectAssignIndexShape(L, call);
5505
6182
  if (merged)
5506
6183
  return merged;
6184
+ // `Object.assign(target, source)` over signature-free RECORDS whose
6185
+ // source fields all land on same-named, same-typed target fields
6186
+ // (the mockable-clock restore: `Object.assign(mocked,
6187
+ // implementations)` over one shape): the per-field copy helper,
6188
+ // returning the TARGET — JS's aliasing, the target mutates in
6189
+ // place. Undefined-armed source fields copy behind the
6190
+ // not-undefined guard (an omitted optional field holds the
6191
+ // undefined arm and must not erase the target's value — Node
6192
+ // copies own keys only; an EXPLICIT `k: undefined` source diverges,
6193
+ // the explicit-undefined-is-absent stance). Everything else keeps
6194
+ // the spread hint.
6195
+ if (call.arguments.length === 2 && !call.arguments.some((a) => ts.isSpreadElement(a))) {
6196
+ const tProbe = probeLower(L, call.arguments[0]);
6197
+ const sProbe = probeLower(L, call.arguments[1]);
6198
+ // CHECKED-DYNAMIC target and source (the JS file-scope
6199
+ // object-literal identity story): the runtime DOM copy — own
6200
+ // members of the source land on the target, which returns.
6201
+ if (tProbe?.type.kind === "dyn") {
6202
+ const loc = locOf(call);
6203
+ const target = L.lowerExpr(call.arguments[0]);
6204
+ const source = L.coerceToExpected(L.lowerExpr(call.arguments[1]), DYN);
6205
+ if (target.type.kind === "dyn" && source.type.kind === "dyn") {
6206
+ return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
6207
+ }
6208
+ }
6209
+ if (tProbe?.type.kind === "record" && sProbe?.type.kind === "record") {
6210
+ const tShape = L.shapes.get(tProbe.type.shapeId);
6211
+ const sShape = L.shapes.get(sProbe.type.shapeId);
6212
+ const ok = tShape && sShape &&
6213
+ !tShape.tuple && !sShape.tuple &&
6214
+ !tShape.indexValue && !sShape.indexValue &&
6215
+ !shapeHasAccessorSlots(tShape) && !shapeHasAccessorSlots(sShape) &&
6216
+ sShape.fields.every((sf) => {
6217
+ const tf = tShape.fields.find((x) => x.name === sf.name);
6218
+ return tf !== undefined && typeEquals(tf.type, sf.type);
6219
+ });
6220
+ if (ok) {
6221
+ const loc = locOf(call);
6222
+ const target = L.lowerExpr(call.arguments[0]);
6223
+ const source = L.lowerExpr(call.arguments[1]);
6224
+ if (target.type.kind === "record" && source.type.kind === "record") {
6225
+ const helper = recordAssignHelper(L, target.type.shapeId, source.type.shapeId, loc);
6226
+ return { kind: "call", callee: helper, args: [target, source], type: target.type, loc };
6227
+ }
6228
+ }
6229
+ }
6230
+ }
5507
6231
  return null;
5508
6232
  }
5509
6233
  // Object.defineProperties over a CHECKED-DYNAMIC target (test/common's
@@ -5557,6 +6281,54 @@ function lowerObjectStaticCall(L, call, access) {
5557
6281
  }
5558
6282
  L.noLowering("Object.freeze of a possibly-aliased value", call, "freeze of a FRESH object/array literal (and of primitives) compiles — frozen-ness is unobservable there; an aliased target's later writes would need the runtime frozen bit");
5559
6283
  }
6284
+ // `Object.hasOwn(r, k)` over a RECORD receiver: a record's own-key set
6285
+ // is its declared field list, so membership is a compare chain against
6286
+ // the field names (interned per shape). Undefined-armed (optional)
6287
+ // fields answer by their runtime tag — the explicit-undefined-is-absent
6288
+ // stance: an omitted optional field holds the undefined arm and reads
6289
+ // as NOT own, exactly Node's absent key (an EXPLICIT `k: undefined`
6290
+ // diverges — documented next to the child-env/JSON rule). Tuple,
6291
+ // index-signature (overflow membership lives in the runtime map), and
6292
+ // accessor-carrying shapes keep the SC2020 fence; non-record receivers
6293
+ // do too.
6294
+ if (member === "hasOwn" && call.arguments.length === 2 && !call.arguments.some((a) => ts.isSpreadElement(a))) {
6295
+ const recvNode = call.arguments[0];
6296
+ const keyNode = call.arguments[1];
6297
+ const probed = probeLower(L, recvNode);
6298
+ // A CHECKED-DYNAMIC receiver (the JS file-scope object-literal
6299
+ // identity story): the runtime DOM probe — OBJ member presence, ARR
6300
+ // index bounds, Node's ToObject TypeError on nullish.
6301
+ if (probed?.type.kind === "dyn") {
6302
+ const loc = locOf(call);
6303
+ const receiver = L.lowerExpr(recvNode);
6304
+ let key = L.lowerExpr(keyNode);
6305
+ if (key.type.kind === "f64" || key.type.kind === "bool" || key.type.kind === "dyn") {
6306
+ key = { kind: "toString", operand: key, type: STRING, loc: locOf(keyNode) };
6307
+ }
6308
+ if (key.type.kind !== "string")
6309
+ return null;
6310
+ return { kind: "libCall", fn: "dyn.hasOwn", args: [receiver, key], type: BOOL, loc };
6311
+ }
6312
+ if (probed?.type.kind !== "record")
6313
+ return null;
6314
+ const shape = L.shapes.get(probed.type.shapeId);
6315
+ if (!shape || shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape))
6316
+ return null;
6317
+ const loc = locOf(call);
6318
+ const receiver = L.lowerExpr(recvNode);
6319
+ if (receiver.type.kind !== "record")
6320
+ return null; // probe/lower drift: keep the fence
6321
+ let key = L.lowerExpr(keyNode);
6322
+ // Number/boolean/dyn keys stringify — ToPropertyKey, the keyed-write
6323
+ // path's rule; symbol and composite keys keep the fence.
6324
+ if (key.type.kind === "f64" || key.type.kind === "bool" || key.type.kind === "dyn") {
6325
+ key = { kind: "toString", operand: key, type: STRING, loc: locOf(keyNode) };
6326
+ }
6327
+ if (key.type.kind !== "string")
6328
+ return null;
6329
+ const helper = recordHasOwnHelper(L, receiver.type.shapeId, loc);
6330
+ return { kind: "call", callee: helper, args: [receiver, key], type: BOOL, loc };
6331
+ }
5560
6332
  if (member !== "keys" && member !== "values" && member !== "entries")
5561
6333
  return null;
5562
6334
  if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]))
@@ -6109,6 +6881,29 @@ export function bindingGenericFnNodeOf(decl) {
6109
6881
  }
6110
6882
  return null;
6111
6883
  }
6884
+ /** The CONTEXTUAL twin of bindingGenericFnNodeOf: `const g: Mapper = (x)
6885
+ * => x` where `type Mapper = <T>(x: T) => T` — the initializer declares
6886
+ * no type parameters of its own, but the ANNOTATION's one call signature
6887
+ * does, and the checker types the arrow's parameters by those (`x: T`).
6888
+ * Such a binding monomorphizes exactly like `const g = <T>(x: T) => x`;
6889
+ * bindingGenericFnInfoOf reads the type parameters off the annotation's
6890
+ * signature. Null when the shape doesn't match (a concrete annotation, a
6891
+ * generic arrow — the syntactic probe's case, an overloaded alias). */
6892
+ export function bindingContextualGenericFnNodeOf(L, decl) {
6893
+ if (!ts.isIdentifier(decl.name) || decl.initializer === undefined || decl.type === undefined)
6894
+ return null;
6895
+ let init = decl.initializer;
6896
+ while (ts.isParenthesizedExpression(init))
6897
+ init = init.expression;
6898
+ if (!(ts.isArrowFunction(init) || ts.isFunctionExpression(init)) ||
6899
+ init.typeParameters !== undefined || init.body === undefined) {
6900
+ return null;
6901
+ }
6902
+ const sigs = L.checker.getCallSignatures(L.typeOf(decl.name));
6903
+ if (sigs.length !== 1 || sigs[0].getTypeParameters().length === 0)
6904
+ return null;
6905
+ return init;
6906
+ }
6112
6907
  /** The interned GenericFnInfo for one generic arrow/function-expression
6113
6908
  * binding initializer, with the supportability fences applied ONCE per
6114
6909
  * declaration: the binding must sit at module scope (the compiled
@@ -6150,11 +6945,29 @@ export function bindingGenericFnInfoOf(L, decl, fnNode) {
6150
6945
  L.unsupported("SC1090", decl.name, `generic function values in reassigned bindings (calls of '${name}' resolve statically against this initializer, so the binding must provably hold it — a const, or a let/var nothing in its declaring file writes)`);
6151
6946
  }
6152
6947
  const typeParams = [];
6153
- for (const tp of fnNode.typeParameters) {
6154
- const tpSym = L.checker.getSymbolAtLocation(tp.name);
6155
- if (!tpSym)
6948
+ if (fnNode.typeParameters !== undefined) {
6949
+ for (const tp of fnNode.typeParameters) {
6950
+ const tpSym = L.checker.getSymbolAtLocation(tp.name);
6951
+ if (!tpSym)
6952
+ L.unsupported("SC1090", fnNode, "this function form");
6953
+ typeParams.push(tpSym);
6954
+ }
6955
+ }
6956
+ else {
6957
+ // The CONTEXTUAL shape (bindingContextualGenericFnNodeOf): the type
6958
+ // parameters live on the annotation's one call signature, and the
6959
+ // checker types the initializer's parameters by them — the same
6960
+ // symbols the instance bodies resolve through.
6961
+ const sigs = L.checker.getCallSignatures(L.typeOf(decl.name));
6962
+ const tps = sigs.length === 1 ? sigs[0].getTypeParameters() : [];
6963
+ if (tps.length === 0)
6156
6964
  L.unsupported("SC1090", fnNode, "this function form");
6157
- typeParams.push(tpSym);
6965
+ for (const tp of tps) {
6966
+ const tpSym = tp.getSymbol();
6967
+ if (!tpSym)
6968
+ L.unsupported("SC1090", fnNode, "this function form");
6969
+ typeParams.push(tpSym);
6970
+ }
6158
6971
  }
6159
6972
  // Only NAME syntax is checkable here; optional/default/rest shapes are
6160
6973
  // computed per instantiation from the resolved signature — exactly
@@ -6181,6 +6994,53 @@ export function bindingGenericFnInfoOf(L, decl, fnNode) {
6181
6994
  }
6182
6995
  return info;
6183
6996
  }
6997
+ /** `const h = id` — a binding ALIASING a generic function (a top-level
6998
+ * declaration, a registered generic binding, or another alias — resolved
6999
+ * left to right in declaration order). The alias registers the SAME info
7000
+ * under its own symbol, so calls (`h(3)`) and pinned values (`take(h)`)
7001
+ * resolve exactly like the target's own name, and the binding itself has
7002
+ * no runtime value (a generic function value cannot materialize). Claims
7003
+ * only bindings whose OWN type still keeps type parameters — a
7004
+ * concrete-annotated alias (`const h: (x: number) => number = id`) is a
7005
+ * pinned VALUE, the existing lowerGenericFnValue story. Null when the
7006
+ * shape doesn't match or the target isn't a registered generic; fences
7007
+ * (reassignment, var redeclaration) report by name inside. */
7008
+ export function bindingGenericFnAliasInfoOf(L, decl) {
7009
+ if (!ts.isIdentifier(decl.name) || decl.initializer === undefined)
7010
+ return null;
7011
+ let init = decl.initializer;
7012
+ while (ts.isParenthesizedExpression(init))
7013
+ init = init.expression;
7014
+ if (!ts.isIdentifier(init))
7015
+ return null;
7016
+ const target = genericFnOf(L, init);
7017
+ if (!target)
7018
+ return null;
7019
+ // A concrete annotation pins one signature — that value story
7020
+ // (lowerGenericFnValue at the reference) stays untouched.
7021
+ const ownSigs = L.checker.getCallSignatures(L.typeOf(decl.name));
7022
+ if (ownSigs.length === 0 || !ownSigs.every((s) => s.getTypeParameters().length > 0))
7023
+ return null;
7024
+ const sym = L.checker.getSymbolAtLocation(decl.name);
7025
+ if (!sym)
7026
+ return null;
7027
+ const existing = L.genericFnsBySymbol.get(sym);
7028
+ if (existing)
7029
+ return existing;
7030
+ const name = decl.name.text;
7031
+ const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0;
7032
+ // The same holds-it-forever discipline as generic arrow bindings:
7033
+ // calls through the alias resolve statically against the target, so
7034
+ // nothing may ever rebind it (merged `var` redeclarations included).
7035
+ const redeclared = L.checker
7036
+ .declarationsOf(sym)
7037
+ .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined);
7038
+ if (!isConst && (redeclared || !bindingNeverReassigned(L, sym, decl))) {
7039
+ L.unsupported("SC1090", decl.name, `generic function values in reassigned bindings (calls of '${name}' resolve statically against this initializer, so the binding must provably hold it — a const, or a let/var nothing in its declaring file writes)`);
7040
+ }
7041
+ L.genericFnsBySymbol.set(sym, target);
7042
+ return target;
7043
+ }
6184
7044
  /** Static resolution stands in for the receiver's runtime value, so an
6185
7045
  * object-literal generic-method receiver must provably HOLD the defining
6186
7046
  * literal: a direct read of a binding whose initializer IS that literal
@@ -6225,10 +7085,56 @@ export function requireObjLitGenericReceiver(L, blame, recvExpr, literal, name)
6225
7085
  * direct `call` of the instance with the receiver unevaluated. Claims
6226
7086
  * every call whose member is generic-callable — lowering it or fencing
6227
7087
  * with a named message. */
7088
+ /** URL.revokeObjectURL() with NO argument: Node's ERR_MISSING_ARGS
7089
+ * throws before the registry lookup, so the zero-argument contract is
7090
+ * exact without any blob machinery. The one-argument form (Node's
7091
+ * silent no-op for unregistered ids) and createObjectURL keep their
7092
+ * fences — a compiled program has no blob registry to consult. */
7093
+ function lowerUrlStaticCall(L, call, callee) {
7094
+ if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined)
7095
+ return null;
7096
+ if (!ts.isIdentifier(callee.expression) || callee.expression.text !== "URL")
7097
+ return null;
7098
+ if (callee.name.text !== "revokeObjectURL" || call.arguments.length !== 0)
7099
+ return null;
7100
+ const sym = L.resolveValueSymbol(callee.expression);
7101
+ if (!sym || !L.isStdlibSymbol(sym))
7102
+ return null;
7103
+ return nodeThrowExpr(1, "ERR_MISSING_ARGS", 'The "url" argument must be specified', VOID, locOf(call));
7104
+ }
7105
+ const SP_BRAND_METHODS = new Set([
7106
+ "append", "delete", "get", "getAll", "has", "set", "sort",
7107
+ "forEach", "keys", "values", "entries", "toString",
7108
+ ]);
6228
7109
  export function lowerObjLitGenericMethodCall(L, call, access) {
6229
7110
  if (L.chainBlocked(access, call))
6230
7111
  return null;
6231
7112
  const name = access.name.text;
7113
+ // URLSearchParams method values through Function.prototype.call/apply
7114
+ // with a receiver that is provably NOT a URLSearchParams (the suite's
7115
+ // `params.append.call(undefined)` probes): the WHATWG brand check
7116
+ // throws ERR_INVALID_THIS before any argument conversion — the whole
7117
+ // call IS that throw. A receiver that IS searchParams-typed, or one
7118
+ // whose runtime kind is unknowable (dyn/'any'), keeps the fence.
7119
+ if ((name === "call" || name === "apply") &&
7120
+ ts.isPropertyAccessExpression(access.expression) &&
7121
+ L.mapTypeOf(L.typeOf(access.expression.expression))?.kind === "searchParams" &&
7122
+ SP_BRAND_METHODS.has(access.expression.name.text) &&
7123
+ L.isStdlibMember(access.expression)) {
7124
+ const thisArg = call.arguments[0];
7125
+ const thisT = thisArg ? L.mapTypeOf(L.typeOf(thisArg)) : { kind: "undefinedT" };
7126
+ const provablyNot = thisT !== null &&
7127
+ thisT.kind !== "searchParams" &&
7128
+ thisT.kind !== "dyn" &&
7129
+ thisT.kind !== "jsval" &&
7130
+ (!thisArg || ts.isIdentifier(thisArg) || ts.isLiteralExpression(thisArg) ||
7131
+ thisArg.kind === ts.SyntaxKind.UndefinedKeyword ||
7132
+ thisArg.kind === ts.SyntaxKind.NullKeyword ||
7133
+ isUnitType(thisT));
7134
+ if (provablyNot) {
7135
+ return nodeThrowExpr(1, "ERR_INVALID_THIS", 'Value of "this" must be of type URLSearchParams', L.mapTypeOf(L.typeOf(call)) ?? VOID, locOf(call));
7136
+ }
7137
+ }
6232
7138
  const recvT = L.typeOf(access.expression);
6233
7139
  const propSym = L.checker.getPropertyOfType(recvT, name);
6234
7140
  if (!propSym)
@@ -6241,8 +7147,36 @@ export function lowerObjLitGenericMethodCall(L, call, access) {
6241
7147
  if (L.checker.declarationsOf(propSym).some((d) => d.parent !== undefined && (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)))) {
6242
7148
  return null;
6243
7149
  }
7150
+ // An INTERFACE-typed receiver over a class instance (`const r: Repo =
7151
+ // new MemRepo(); r.get(...)` — the declaration is signature-only, but
7152
+ // the receiver's exact class is statically proven and the binding
7153
+ // kept the class representation, genericIfaceBindingKeepsClass): the
7154
+ // call is a class generic-method call on that exact class. The
7155
+ // receiver must LOWER as the class — a record-held value (a `let`, a
7156
+ // produced value, a parameter) has already dropped it, and keeps the
7157
+ // named fence below.
7158
+ {
7159
+ const exact = exactInstanceClassOf(L, access.expression);
7160
+ const gfound = exact ? findGenericMethodOn(L, exact, name) : null;
7161
+ if (gfound && ts.isIdentifier(access.expression)) {
7162
+ const recv = L.lowerExpr(access.expression); // identifier reads are pure — no double evaluation
7163
+ if (recv.type.kind === "object") {
7164
+ return lowerClassGenericMethodCall(L, call, access, exact, gfound, recv);
7165
+ }
7166
+ }
7167
+ }
6244
7168
  const found = objLitGenericFnNodeOf(L, propSym);
6245
7169
  if (!found) {
7170
+ // Interface-declared generic methods dispatch statically, so the
7171
+ // receiver's runtime class must be provable — name that discipline
7172
+ // instead of the object-literal wording when the method lives on an
7173
+ // interface.
7174
+ const onInterface = L.checker
7175
+ .declarationsOf(propSym)
7176
+ .some((d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent));
7177
+ if (onInterface) {
7178
+ L.unsupported("SC1090", call, `calls of the generic method '${name}' through this receiver (the interface declaration is signature-only and generic methods dispatch statically, so the receiver's runtime class must be provable — bind the receiver to a const initialized with its 'new' expression, e.g. 'const r: ${L.checker.typeToString(recvT)} = new C(...)')`);
7179
+ }
6246
7180
  L.unsupported("SC1090", call, `calls of the generic method '${name}' with no defining object literal (the declaration is signature-only — only methods declared with a body in an object literal monomorphize)`);
6247
7181
  }
6248
7182
  requireObjLitGenericReceiver(L, call, access.expression, found.literal, name);