@scriptc/compiler 0.0.7 → 0.0.8

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 (68) hide show
  1. package/dist/backend/emission/emit-exprs.js +47 -1
  2. package/dist/backend/emission/emit-exprs.js.map +1 -1
  3. package/dist/backend/emission/emit-island.js +9 -1
  4. package/dist/backend/emission/emit-island.js.map +1 -1
  5. package/dist/backend/emission/emit-stmts.js +3 -1
  6. package/dist/backend/emission/emit-stmts.js.map +1 -1
  7. package/dist/backend/emission/emit-walkers.js +22 -8
  8. package/dist/backend/emission/emit-walkers.js.map +1 -1
  9. package/dist/backend/emission/emitter.js +5 -1
  10. package/dist/backend/emission/emitter.js.map +1 -1
  11. package/dist/backend/emission/may-throw.js +4 -3
  12. package/dist/backend/emission/may-throw.js.map +1 -1
  13. package/dist/backend/llvm/emitter.js +94 -7
  14. package/dist/backend/llvm/emitter.js.map +1 -1
  15. package/dist/diagnostics/diagnostic.d.ts +43 -1
  16. package/dist/diagnostics/diagnostic.js +103 -5
  17. package/dist/diagnostics/diagnostic.js.map +1 -1
  18. package/dist/frontend/lib-contract.d.ts +24 -6
  19. package/dist/frontend/lib-contract.js +125 -44
  20. package/dist/frontend/lib-contract.js.map +1 -1
  21. package/dist/frontend/lowering/lower-builtins.js +16 -1
  22. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  23. package/dist/frontend/lowering/lower-calls.d.ts +47 -7
  24. package/dist/frontend/lowering/lower-calls.js +560 -36
  25. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  26. package/dist/frontend/lowering/lower-classes.d.ts +14 -1
  27. package/dist/frontend/lowering/lower-classes.js +85 -15
  28. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  29. package/dist/frontend/lowering/lower-containers.js +43 -1
  30. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  31. package/dist/frontend/lowering/lower-emitter.js +1 -1
  32. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  33. package/dist/frontend/lowering/lower-exprs.d.ts +26 -3
  34. package/dist/frontend/lowering/lower-exprs.js +455 -40
  35. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  36. package/dist/frontend/lowering/lower-island.d.ts +7 -0
  37. package/dist/frontend/lowering/lower-island.js +71 -10
  38. package/dist/frontend/lowering/lower-island.js.map +1 -1
  39. package/dist/frontend/lowering/lower-mixins.d.ts +1 -1
  40. package/dist/frontend/lowering/lower-mixins.js +1 -1
  41. package/dist/frontend/lowering/lower-modules.js +95 -4
  42. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  43. package/dist/frontend/lowering/lower-stmts.js +421 -43
  44. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  45. package/dist/frontend/lowering/lowerer.d.ts +19 -3
  46. package/dist/frontend/lowering/lowerer.js +188 -11
  47. package/dist/frontend/lowering/lowerer.js.map +1 -1
  48. package/dist/frontend/types.d.ts +28 -1
  49. package/dist/frontend/types.js +300 -8
  50. package/dist/frontend/types.js.map +1 -1
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.js +37 -7
  53. package/dist/index.js.map +1 -1
  54. package/dist/ir/nodes.d.ts +28 -2
  55. package/dist/ir/nodes.js +35 -3
  56. package/dist/ir/nodes.js.map +1 -1
  57. package/dist/ir/validate.js +35 -6
  58. package/dist/ir/validate.js.map +1 -1
  59. package/dist/library/profile.d.ts +17 -3
  60. package/dist/library/profile.js +38 -9
  61. package/dist/library/profile.js.map +1 -1
  62. package/dist/library/sidecar.js +162 -43
  63. package/dist/library/sidecar.js.map +1 -1
  64. package/dist/library/trap-teaching.d.ts +11 -0
  65. package/dist/library/trap-teaching.js +49 -0
  66. package/dist/library/trap-teaching.js.map +1 -0
  67. package/package.json +2 -2
  68. package/surface-manifest.json +37 -2
@@ -203,6 +203,45 @@ function lowerExprInner(L, expr) {
203
203
  if (ts.isArrayLiteralExpression(expr) && expr.elements.length === 0 && underConstAssertion(expr)) {
204
204
  return false;
205
205
  }
206
+ // A JS variable INITIALIZER whose BINDING registered as a
207
+ // checked-dynamic module global (the typedef-annotated
208
+ // doc-builder consts whose JSDoc types degraded — DYN slots):
209
+ // the island build could never land (no engine→DOM crossing
210
+ // exists), so the literal takes its own world's path instead —
211
+ // the DOM literal, or a static record the slot converts.
212
+ if (isJsSourceFile(expr.getSourceFile()) &&
213
+ ts.isVariableDeclaration(expr.parent) &&
214
+ expr.parent.initializer === expr &&
215
+ ts.isIdentifier(expr.parent.name)) {
216
+ const bindSym = L.checker.getSymbolAtLocation(expr.parent.name);
217
+ const g = bindSym ? L.globalsBySymbol.get(bindSym) : undefined;
218
+ if (g?.type.kind === "dyn")
219
+ return false;
220
+ }
221
+ // A PROJECT-DECLARED contextual type that only ABSORBED to the
222
+ // island (a doc-builder typedef whose field's JSDoc type
223
+ // reference degraded to checker-`any` — the tsgo multi-file
224
+ // value-as-type residue) is not an npm slot: when the literal's
225
+ // OWN type maps statically, it builds that way — the binding's
226
+ // checked-dynamic slot takes the DOM conversion. Genuinely
227
+ // island slots (npm .d.ts provenance, plain `any`) keep the
228
+ // native build.
229
+ if (ctxTs !== undefined && (ctxTs.flags & ts.TypeFlags.Any) === 0) {
230
+ // Intersection/tuple typedefs (`Line & {readonly soft: true}`)
231
+ // carry provenance on the ALIAS symbol.
232
+ const sym = ctxTs.getSymbol() ?? ctxTs.getAliasSymbol();
233
+ const decls = sym ? L.checker.declarationsOf(sym) : [];
234
+ const projectDeclared = decls.length > 0 &&
235
+ decls.every((d) => {
236
+ const sf = d.getSourceFile();
237
+ return !sf.isDeclarationFile && !sf.fileName.includes("/node_modules/");
238
+ });
239
+ if (projectDeclared) {
240
+ const own = L.mapTypeOf(L.typeOf(expr));
241
+ if (own?.kind === "record" || own?.kind === "array")
242
+ return false;
243
+ }
244
+ }
206
245
  return true;
207
246
  })()) {
208
247
  if (ts.isObjectLiteralExpression(expr)) {
@@ -217,21 +256,73 @@ function lowerExprInner(L, expr) {
217
256
  // (`"k" in o` is false), which the static record path can't say.
218
257
  const argsWithout = [];
219
258
  const argsWith = [];
259
+ const getters = [];
220
260
  let spread = null;
261
+ // A MEMBER a JS file cannot lower or marshal (a generic function
262
+ // as a value, a signature with checked-dynamic parameters — the
263
+ // doc-builder public aggregate's degraded pieces): defer like a
264
+ // statement fence — the slot holds a host closure that THROWS the
265
+ // captured diagnostic when invoked, so building the aggregate
266
+ // compiles and only a CALL through the island stops the run.
267
+ const islandMemberFence = (diagsBefore, err, valueNode) => {
268
+ if (!(err instanceof PoisonError) ||
269
+ !isJsSourceFile(expr.getSourceFile()) ||
270
+ L.diagSink !== null ||
271
+ L.diags.length <= diagsBefore ||
272
+ L.diags.slice(diagsBefore).some((d) => d.code === "SC9001")) {
273
+ throw err;
274
+ }
275
+ const captured = L.diags.splice(diagsBefore);
276
+ L.runtimeFences.push(...captured);
277
+ const first = captured[0];
278
+ const pos = ts.getLineAndCharacterOfPosition(L.program.getSourceFile(first.loc.file) ?? expr.getSourceFile(), first.loc.start);
279
+ const fnName = `%fn${L.lambdaCounter++}_islfence`;
280
+ L.liftedFns.push({
281
+ name: fnName,
282
+ params: [],
283
+ returnType: VOID,
284
+ locals: [],
285
+ captures: [],
286
+ body: [
287
+ {
288
+ kind: "runtimeFence",
289
+ code: first.code,
290
+ message: `${first.message} [${first.code} at ${first.loc.file}:${pos.line + 1}]`,
291
+ loc: locOf(valueNode),
292
+ },
293
+ ],
294
+ loc: locOf(valueNode),
295
+ });
296
+ const fence = { kind: "closure", fnName, captures: [], type: funcOf([], VOID), loc: locOf(valueNode) };
297
+ return L.jsvalIn(fence, valueNode);
298
+ };
221
299
  const pushProp = (name, value, valueNode, into) => {
300
+ const diagsBefore = L.diags.length;
301
+ let marshaled;
302
+ try {
303
+ marshaled = L.jsvalIn(value, valueNode);
304
+ }
305
+ catch (err) {
306
+ marshaled = islandMemberFence(diagsBefore, err, valueNode);
307
+ }
222
308
  for (const args of into) {
223
309
  args.push({
224
310
  kind: "jsMarshal",
225
311
  value: { kind: "strLit", value: name.text, type: STRING, loc: locOf(name) },
226
312
  type: JSVAL, loc: locOf(name),
227
313
  });
228
- args.push(L.jsvalIn(value, valueNode));
314
+ args.push(marshaled);
229
315
  }
230
316
  };
317
+ const spreadSrcs = [];
318
+ let sawPlainProp = false;
231
319
  for (const prop of expr.properties) {
232
320
  if (ts.isSpreadAssignment(prop)) {
233
321
  const cs = conditionalSpreadOf(prop.expression);
234
322
  if (cs && cs !== "unsupported" && !spread) {
323
+ if (spreadSrcs.length > 0) {
324
+ L.unsupported("SC1090", prop, "a conditional spread mixed with plain spreads in an 'any'-typed object literal");
325
+ }
235
326
  const cond = L.lowerCondition(cs.cond);
236
327
  if (!pureCondExpr(cond)) {
237
328
  L.unsupported("SC1090", prop, "conditional spreads with effectful conditions in an 'any'-typed object literal (bind the condition to a const first)");
@@ -243,8 +334,23 @@ function lowerExprInner(L, expr) {
243
334
  }
244
335
  continue;
245
336
  }
337
+ // A PLAIN spread (`{ ...options, plugins }` — the withPlugins
338
+ // argument-rebuild shape): the source copies into the fresh
339
+ // engine object through the spec's CopyDataProperties (the
340
+ // objSpread op). Spreads must precede explicit properties
341
+ // (the composition applies explicit keys AFTER the copies —
342
+ // JS's later-wins — which a spread after them would invert);
343
+ // mixing with a conditional spread keeps the fence.
344
+ if (cs === undefined || cs === null) {
345
+ if (sawPlainProp || spread) {
346
+ L.unsupported("SC1090", prop, "object spread after explicit properties (or mixed with a conditional spread) in an 'any'-typed object literal — spreads must come first");
347
+ }
348
+ spreadSrcs.push(L.jsvalIn(L.lowerExpr(prop.expression), prop.expression));
349
+ continue;
350
+ }
246
351
  L.unsupported("SC1090", prop, "this spread form in an 'any'-typed object literal (one `...(c ? { k: v } : {})` conditional spread is supported — bind other shapes to a const and set keys explicitly)");
247
352
  }
353
+ sawPlainProp = true;
248
354
  // `name: value`, the shorthand `{ name }` (the value is the
249
355
  // identifier itself, resolved through the shorthand VALUE symbol
250
356
  // like the static record path), and METHODS — `{ load() {...} }`
@@ -254,22 +360,79 @@ function lowerExprInner(L, expr) {
254
360
  // string-literal keys — engine property names have no
255
361
  // identifier restriction ("content-type").
256
362
  const name = prop.name;
257
- const value = ts.isPropertyAssignment(prop)
258
- ? L.lowerExpr(prop.initializer)
259
- : ts.isShorthandPropertyAssignment(prop)
260
- ? L.lowerShorthandValue(prop)
261
- : ts.isMethodDeclaration(prop) && prop.body
262
- ? (L.rejectThisInObjectMethod(prop.body), L.lowerLambda(prop))
263
- : null;
363
+ // A GET accessor (`get root() { return ROOT_INDENT; }` — the
364
+ // doc-printer's self-referential root-indent shape): the body
365
+ // lowers as a zero-param closure marshaled into the engine, and
366
+ // the property defines through the engine's own getter
367
+ // machinery AFTER the literal builds (the defineGetter op) —
368
+ // reads through the handle invoke it natively. `this` in the
369
+ // body keeps the object-method rejection; setters stay fenced.
370
+ if (ts.isGetAccessorDeclaration(prop) && prop.body && name && (ts.isIdentifier(name) || ts.isStringLiteral(name))) {
371
+ L.rejectThisInObjectMethod(prop.body);
372
+ getters.push({ name: name.text, fn: L.jsvalIn(L.lowerLambda(prop), prop), loc: locOf(prop) });
373
+ continue;
374
+ }
375
+ // A member VALUE that cannot lower at all (a generic function
376
+ // referenced as a value — the aggregate's `align`): the same
377
+ // call-time fence deferral pushProp applies to marshal failures.
378
+ let value = null;
379
+ const valueDiagsBefore = L.diags.length;
380
+ try {
381
+ value = ts.isPropertyAssignment(prop)
382
+ ? L.lowerExpr(prop.initializer)
383
+ : ts.isShorthandPropertyAssignment(prop)
384
+ ? L.lowerShorthandValue(prop)
385
+ : ts.isMethodDeclaration(prop) && prop.body
386
+ ? (L.rejectThisInObjectMethod(prop.body), L.lowerLambda(prop))
387
+ : null;
388
+ }
389
+ catch (err) {
390
+ value = islandMemberFence(valueDiagsBefore, err, prop);
391
+ }
264
392
  if (value && name && (ts.isIdentifier(name) || ts.isStringLiteral(name))) {
265
393
  pushProp(name, value, prop, [argsWithout, argsWith]);
266
394
  }
267
395
  else {
268
- L.unsupported("SC1090", prop, "this property form in an 'any'-typed object literal (only `name: value` and shorthand names are supported)");
396
+ L.unsupported("SC1090", prop, "this property form in an 'any'-typed object literal (only `name: value`, shorthand names, methods, and get accessors are supported)");
269
397
  }
270
398
  }
271
- if (!spread)
272
- return { kind: "jsOp", op: "objLit", args: argsWithout, type: JSVAL, loc };
399
+ const withGetters = (obj) => getters.reduce((acc, g) => ({
400
+ kind: "jsOp",
401
+ op: "defineGetter",
402
+ args: [
403
+ acc,
404
+ { kind: "jsMarshal", value: { kind: "strLit", value: g.name, type: STRING, loc: g.loc }, type: JSVAL, loc: g.loc },
405
+ g.fn,
406
+ ],
407
+ type: JSVAL,
408
+ loc: g.loc,
409
+ }), obj);
410
+ if (!spread) {
411
+ if (spreadSrcs.length === 0) {
412
+ return withGetters({ kind: "jsOp", op: "objLit", args: argsWithout, type: JSVAL, loc });
413
+ }
414
+ // Plain spreads compose left to right onto a fresh object, the
415
+ // explicit properties merging LAST (JS's later-wins): spread
416
+ // sources evaluate before later property values by nesting
417
+ // order, exactly the source order.
418
+ let acc = { kind: "jsOp", op: "objLit", args: [], type: JSVAL, loc };
419
+ for (const src of spreadSrcs) {
420
+ acc = { kind: "jsOp", op: "objSpread", args: [acc, src], type: JSVAL, loc };
421
+ }
422
+ if (argsWithout.length > 0) {
423
+ acc = {
424
+ kind: "jsOp",
425
+ op: "objSpread",
426
+ args: [acc, { kind: "jsOp", op: "objLit", args: argsWithout, type: JSVAL, loc }],
427
+ type: JSVAL,
428
+ loc,
429
+ };
430
+ }
431
+ return withGetters(acc);
432
+ }
433
+ if (getters.length > 0) {
434
+ L.unsupported("SC1090", expr, "get accessors combined with conditional spreads in an 'any'-typed object literal");
435
+ }
273
436
  const withLit = { kind: "jsOp", op: "objLit", args: argsWith, type: JSVAL, loc };
274
437
  const withoutLit = { kind: "jsOp", op: "objLit", args: argsWithout, type: JSVAL, loc };
275
438
  return {
@@ -1538,7 +1701,7 @@ export function lowerTernary(L, expr, expected) {
1538
1701
  // The ternary's IR type is normally the checker's own: it collapses
1539
1702
  // same-kind literal unions ("a" | "b" → string) and forms tagged
1540
1703
  // unions for mixed arms that map (`c ? okRec : errRec`); anything
1541
- // left unmappable gets SC2001 here. A record/union CONTEXTUAL type
1704
+ // left unmappable gets the type fence (badType) here. A record/union CONTEXTUAL type
1542
1705
  // takes over exactly when the own type can't carry the value —
1543
1706
  // unmappable, or a DIFFERENT union (branch literals omitting
1544
1707
  // DIFFERENT optional subsets make the own type a union of the
@@ -1992,7 +2155,11 @@ export function lowerNullishCoalesce(L, expr, loc) {
1992
2155
  L.unsupported("SC1100", expr, "nullish coalescing on 'unknown' values");
1993
2156
  }
1994
2157
  if (left.type.kind === "jsval") {
1995
- L.unsupported("SC1090", expr, "nullish coalescing on 'any' values");
2158
+ // `a ?? b` on an ISLAND value: the engine's own nullish test — the
2159
+ // left evaluates once, the right runs lazily in its branch and
2160
+ // marshals in (the emitter's jsval nullish arm).
2161
+ const right = L.jsvalIn(L.lowerExpr(expr.right), expr.right);
2162
+ return { kind: "nullish", left, right, type: JSVAL, loc };
1996
2163
  }
1997
2164
  if (left.type.kind !== "union")
1998
2165
  return left;
@@ -2833,7 +3000,7 @@ export function lowerIntrinsicProperty(L, expr) {
2833
3000
  /** `[a, b, c]`. The element type comes from the contextual type when tsc
2834
3001
  * has one (`const a: number[] = []`, arguments, nested literals) and from
2835
3002
  * the literal's own inferred type otherwise. A bare `[]` with no context
2836
- * is `never[]` — unmappable, rejected with SC2001. `expected` overrides
3003
+ * is `never[]` — unmappable, rejected with the component fence (SC2009). `expected` overrides
2837
3004
  * the contextual lookup where the caller knows the slot's array type and
2838
3005
  * tsc's API doesn't surface it (ternary arms under an array context —
2839
3006
  * tsc accepts the arm covariantly, but a tagged element representation
@@ -2887,6 +3054,36 @@ export function lowerArrayLiteral(L, expr, expected) {
2887
3054
  if (arrayArms.length === 1)
2888
3055
  mapped = arrayArms[0];
2889
3056
  }
3057
+ // A NON-EMPTY literal whose own type has no static home (the JS dyn
3058
+ // fallback — a null/dyn mapping, or an inference that degraded to a
3059
+ // unit-only-element array over non-unit elements) under a union
3060
+ // with exactly ONE array-family arm — an array, or an
3061
+ // arity-matching tuple (the option-table `default: [{ value: [] }]`
3062
+ // shape): build AS that arm, exactly the empty-literal rule above;
3063
+ // the slot's coercion wraps it.
3064
+ if (ctxUnion && expr.elements.length > 0) {
3065
+ // A unit-only-element array (`(null | undefined)[]`) cannot hold
3066
+ // non-unit elements — neither as the literal's own mapping nor as
3067
+ // a competing union arm.
3068
+ const nonUnitElems = expr.elements.some((el) => !ts.isOmittedExpression(el) &&
3069
+ el.kind !== ts.SyntaxKind.NullKeyword &&
3070
+ !(ts.isIdentifier(el) && el.text === "undefined"));
3071
+ const ownUnhelpful = mapped === null || mapped.kind === "dyn" ||
3072
+ // The checker echoing the context union back as the literal's
3073
+ // own type decides nothing either — nor does an island ('any')
3074
+ // residue under a STATIC union slot.
3075
+ mapped.kind === "union" || mapped.kind === "jsval" ||
3076
+ (mapped.kind === "array" && nonUnitElems && L.unitOnlyElem(mapped.elem));
3077
+ if (ownUnhelpful) {
3078
+ const def = L.unions.get(ctxUnion.unionId);
3079
+ const arms = (def?.arms ?? []).filter((a) => (a.kind === "array" && !(nonUnitElems && L.unitOnlyElem(a.elem))) ||
3080
+ (a.kind === "record" &&
3081
+ !!L.shapes.get(a.shapeId)?.tuple &&
3082
+ L.shapes.get(a.shapeId).fields.length === expr.elements.length));
3083
+ if (arms.length === 1)
3084
+ mapped = arms[0];
3085
+ }
3086
+ }
2890
3087
  }
2891
3088
  // An EMPTY literal under a CONST ASSERTION whose type queries panicked
2892
3089
  // (tsgo's readonly-[] TupleType conversion — the facade answers `any`,
@@ -3245,7 +3442,7 @@ function propNameText(L, name) {
3245
3442
  * (dynFrom's domain, functions box); a value with no dyn representation
3246
3443
  * fences per property. Spreads, accessors, and methods stay fenced —
3247
3444
  * none co-occur with the computed-key idiom in the wild. */
3248
- function lowerDynObjectLiteral(L, expr) {
3445
+ export function lowerDynObjectLiteral(L, expr) {
3249
3446
  const loc = locOf(expr);
3250
3447
  const fields = [];
3251
3448
  for (const prop of expr.properties) {
@@ -3283,11 +3480,92 @@ function lowerDynObjectLiteral(L, expr) {
3283
3480
  // Lambda values that fail to lower become trap closures (the
3284
3481
  // per-call fence granularity — fenceClosureProbe) so the object
3285
3482
  // still builds; everything else keeps the per-property fence below.
3286
- const raw = fenceClosureProbe(L, valueExpr, undefined, () => L.lowerExpr(valueExpr)) ??
3287
- L.lowerExpr(valueExpr);
3288
- const v = L.coerceToExpected(raw, DYN);
3483
+ let raw;
3484
+ const propDiagsBefore = L.diags.length;
3485
+ try {
3486
+ raw =
3487
+ fenceClosureProbe(L, valueExpr, undefined, () => L.lowerExpr(valueExpr)) ??
3488
+ L.lowerExpr(valueExpr);
3489
+ }
3490
+ catch (err) {
3491
+ // A PURE member read a JS file cannot lower (a namespace object
3492
+ // in an export aggregate): the slot takes a boxed fence closure —
3493
+ // the diagnostics defer to the runtime-fence ledger, the object
3494
+ // builds, and only USING the member stops the run.
3495
+ const pureMember = ts.isIdentifier(valueExpr);
3496
+ if (!(err instanceof PoisonError) ||
3497
+ !pureMember ||
3498
+ L.diagSink !== null ||
3499
+ L.diags.length <= propDiagsBefore ||
3500
+ L.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
3501
+ throw err;
3502
+ }
3503
+ const captured = L.diags.splice(propDiagsBefore);
3504
+ L.runtimeFences.push(...captured);
3505
+ const first = captured[0];
3506
+ const pos = ts.getLineAndCharacterOfPosition(L.program.getSourceFile(first.loc.file) ?? expr.getSourceFile(), first.loc.start);
3507
+ const fnName = `%fn${L.lambdaCounter++}_dynfence`;
3508
+ L.liftedFns.push({
3509
+ name: fnName,
3510
+ params: [],
3511
+ returnType: VOID,
3512
+ locals: [],
3513
+ captures: [],
3514
+ body: [
3515
+ {
3516
+ kind: "runtimeFence",
3517
+ code: first.code,
3518
+ message: `${first.message} [${first.code} at ${first.loc.file}:${pos.line + 1}]`,
3519
+ loc: locOf(prop),
3520
+ },
3521
+ ],
3522
+ loc: locOf(prop),
3523
+ });
3524
+ raw = { kind: "closure", fnName, captures: [], type: funcOf([], VOID), loc: locOf(prop) };
3525
+ }
3526
+ let v = L.coerceToExpected(raw, DYN);
3289
3527
  if (v.type.kind !== "dyn") {
3290
- L.unsupported("SC1101", valueExpr, `holding '${L.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
3528
+ const convDiagsBefore = L.diags.length;
3529
+ try {
3530
+ L.unsupported("SC1101", valueExpr, `holding '${L.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
3531
+ }
3532
+ catch (err) {
3533
+ // A member value the DOM cannot hold — a func whose signature
3534
+ // cannot box, an island ('any') handle, a record carrying func
3535
+ // fields (the export aggregate's typed utilities and npm-handle
3536
+ // members): the same call-time fence deferral as an unlowerable
3537
+ // member — the slot takes a boxed fence closure; only USING it
3538
+ // stops the run. Probe mode and ICEs keep the poison.
3539
+ if (!(err instanceof PoisonError) ||
3540
+ L.diagSink !== null ||
3541
+ L.diags.length <= convDiagsBefore ||
3542
+ L.diags.slice(convDiagsBefore).some((d) => d.code === "SC9001")) {
3543
+ throw err;
3544
+ }
3545
+ const captured = L.diags.splice(convDiagsBefore);
3546
+ L.runtimeFences.push(...captured);
3547
+ const first = captured[0];
3548
+ const pos = ts.getLineAndCharacterOfPosition(L.program.getSourceFile(first.loc.file) ?? expr.getSourceFile(), first.loc.start);
3549
+ const fnName = `%fn${L.lambdaCounter++}_dynfence`;
3550
+ L.liftedFns.push({
3551
+ name: fnName,
3552
+ params: [],
3553
+ returnType: VOID,
3554
+ locals: [],
3555
+ captures: [],
3556
+ body: [
3557
+ {
3558
+ kind: "runtimeFence",
3559
+ code: first.code,
3560
+ message: `${first.message} [${first.code} at ${first.loc.file}:${pos.line + 1}]`,
3561
+ loc: locOf(prop),
3562
+ },
3563
+ ],
3564
+ loc: locOf(prop),
3565
+ });
3566
+ const fence = { kind: "closure", fnName, captures: [], type: funcOf([], VOID), loc: locOf(prop) };
3567
+ v = L.coerceToExpected(fence, DYN);
3568
+ }
3291
3569
  }
3292
3570
  fields.push({ key, value: v });
3293
3571
  }
@@ -3480,7 +3758,7 @@ export function lowerObjectLiteral(L, expr) {
3480
3758
  // Syntax fence FIRST: unsupported member forms get their specific
3481
3759
  // message even when they also make the literal's type unmappable
3482
3760
  // (a getter or a `this`-returning method would otherwise surface as an
3483
- // opaque SC2001 on the whole literal).
3761
+ // opaque type fence on the whole literal).
3484
3762
  for (const prop of expr.properties) {
3485
3763
  if (ts.isSpreadAssignment(prop)) {
3486
3764
  // Supported shapes: FULL spreads of known record shapes read as
@@ -3625,7 +3903,11 @@ export function lowerObjectLiteral(L, expr) {
3625
3903
  // the slot's width coercion constructs through the trivial
3626
3904
  // parameter-property constructor (recordToClassPlan), or fences with
3627
3905
  // the record-shape story.
3628
- if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object") {
3906
+ // A jsval-mapped context reaches here only when lowerExpr's island
3907
+ // gate DECLINED it (a project-declared typedef that absorbed to the
3908
+ // island through checker-`any` field residue): the literal builds at
3909
+ // its own type like every unmappable context.
3910
+ if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object" || mapped.kind === "jsval") {
3629
3911
  const ctxUnion = mapped?.kind === "union" ? mapped : null;
3630
3912
  mapped = L.mapTypeOf(L.typeOf(expr)) ?? mapped;
3631
3913
  // A literal whose own shape re-tags into NO arm of the contextual
@@ -3885,6 +4167,9 @@ export function lowerObjectLiteral(L, expr) {
3885
4167
  // contributor overriding one would silently drop that evaluation —
3886
4168
  // collisions fence in both directions.
3887
4169
  const conditionalNames = new Set();
4170
+ // Member names DROPPED from the shape (JS deferral — unlowerable pure
4171
+ // member reads narrow away; see the catch in the property loop).
4172
+ const droppedNames = new Set();
3888
4173
  for (const prop of expr.properties) {
3889
4174
  if (ts.isSpreadAssignment(prop)) {
3890
4175
  const cs = conditionalSpreadOf(prop.expression);
@@ -4303,21 +4588,56 @@ export function lowerObjectLiteral(L, expr) {
4303
4588
  }
4304
4589
  let value;
4305
4590
  let valueNode = prop;
4306
- if (ts.isPropertyAssignment(prop)) {
4307
- valueNode = prop.initializer;
4308
- value =
4309
- fenceClosureProbe(L, prop.initializer, fieldType, () => L.lowerExpr(prop.initializer)) ??
4310
- L.lowerExpr(prop.initializer);
4311
- }
4312
- else if (ts.isShorthandPropertyAssignment(prop)) {
4313
- value = L.lowerShorthandValue(prop);
4314
- }
4315
- else if (ts.isMethodDeclaration(prop)) {
4316
- value =
4317
- fenceClosureProbe(L, prop, fieldType, () => L.lowerLambda(prop)) ?? L.lowerLambda(prop);
4318
- }
4319
- else {
4320
- L.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
4591
+ const propDiagsBefore = L.diags.length;
4592
+ try {
4593
+ if (ts.isPropertyAssignment(prop)) {
4594
+ valueNode = prop.initializer;
4595
+ // ARRAY-LITERAL initializers route through the expected-type
4596
+ // lowering: a union field with one array-family arm builds the
4597
+ // literal AS that arm (lowerExprExpecting's IR-directed rule —
4598
+ // the option-table `default: [{ value: [] }]` shape), where the
4599
+ // bare lowering would take the JS DOM fallback and fence.
4600
+ let init = prop.initializer;
4601
+ while (ts.isParenthesizedExpression(init))
4602
+ init = init.expression;
4603
+ value =
4604
+ fenceClosureProbe(L, prop.initializer, fieldType, () => L.lowerExpr(prop.initializer)) ??
4605
+ (fieldType !== undefined && ts.isArrayLiteralExpression(init)
4606
+ ? L.lowerExprExpecting(prop.initializer, fieldType)
4607
+ : L.lowerExpr(prop.initializer));
4608
+ }
4609
+ else if (ts.isShorthandPropertyAssignment(prop)) {
4610
+ value = L.lowerShorthandValue(prop);
4611
+ }
4612
+ else if (ts.isMethodDeclaration(prop)) {
4613
+ value =
4614
+ fenceClosureProbe(L, prop, fieldType, () => L.lowerLambda(prop)) ?? L.lowerLambda(prop);
4615
+ }
4616
+ else {
4617
+ L.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
4618
+ }
4619
+ }
4620
+ catch (err) {
4621
+ // A member VALUE a JS file cannot lower (a namespace object in an
4622
+ // export aggregate — the sharedWithCli `errors` member): the
4623
+ // member NARROWS AWAY like a CJS export-table accessor entry —
4624
+ // the diagnostics defer to the runtime-fence ledger, the shape
4625
+ // drops the field, and each READ of it meets its own per-site
4626
+ // fence. Pure member forms only (identifier/shorthand reads);
4627
+ // TypeScript, probe mode, and ICEs keep the poison.
4628
+ const pureMember = ts.isShorthandPropertyAssignment(prop) ||
4629
+ (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer));
4630
+ if (!(err instanceof PoisonError) ||
4631
+ !pureMember ||
4632
+ !isJsSourceFile(expr.getSourceFile()) ||
4633
+ L.diagSink !== null ||
4634
+ L.diags.length <= propDiagsBefore ||
4635
+ L.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
4636
+ throw err;
4637
+ }
4638
+ L.runtimeFences.push(...L.diags.splice(propDiagsBefore));
4639
+ droppedNames.add(name);
4640
+ continue;
4321
4641
  }
4322
4642
  if (!fieldType) {
4323
4643
  // Overflow entry: the value flows into the index signature's value
@@ -4352,6 +4672,12 @@ export function lowerObjectLiteral(L, expr) {
4352
4672
  // omission of required fields before lowering, undefined-armed or not).
4353
4673
  // A REQUIRED missing field keeps the shape-mismatch rejection (possible
4354
4674
  // through `as`: the cast smuggles a narrower literal past freshness).
4675
+ if (droppedNames.size > 0) {
4676
+ const narrowed = shape.fields.filter((f) => !droppedNames.has(f.name));
4677
+ const narrowedId = L.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
4678
+ type = { kind: "record", shapeId: narrowedId };
4679
+ shape = L.shapes.get(narrowedId);
4680
+ }
4355
4681
  if (fields.filter((f) => !f.overflow).length !== shape.fields.length) {
4356
4682
  const provided = new Set(fields.filter((f) => !f.overflow).map((f) => f.name));
4357
4683
  for (const f of shape.fields) {
@@ -4982,7 +5308,11 @@ export function lowerRecordKeyRead(L, expr, shapeId, shape) {
4982
5308
  // NUMERIC literals are their canonical string spelling — JS object keys
4983
5309
  // ARE strings (`r[1]` reads `r["1"]`), so `b[1]` hits a declared field
4984
5310
  // named "1" (a mapped type over a numeric enum) exactly like `b["1"]`.
4985
- const litKey = recordKeyLiteralText(keyNode);
5311
+ // A key IDENTIFIER whose type proves one literal (a literal-typed
5312
+ // const, a keyof-constrained type parameter bound to a literal inside
5313
+ // a generic instance) is the same static read — identifier evaluation
5314
+ // is pure, so skipping it matches JS exactly.
5315
+ const litKey = recordKeyLiteralText(keyNode) ?? recordKeyTypeLiteralText(L, keyNode);
4986
5316
  // The receiver lowers FIRST (both branches below read it, and JS
4987
5317
  // evaluates the receiver before the key).
4988
5318
  const obj = L.lowerExpr(expr.expression);
@@ -5069,6 +5399,32 @@ export function recordKeyLiteralText(node) {
5069
5399
  return String(Number(node.text));
5070
5400
  return null;
5071
5401
  }
5402
+ /** The compile-time key an IDENTIFIER's TYPE proves: a key variable whose
5403
+ * (narrowed) type is one string/number literal — `const k: "a" = …; o[k]`,
5404
+ * and the generic-body form where k's type is a keyof-constrained
5405
+ * parameter bound to a literal (`pick(o, "a")`'s instance reads `o[k]`
5406
+ * with K = "a", resolved through typeParamTsBindings). Identifier reads
5407
+ * are pure, so the static field read may skip evaluating the key exactly
5408
+ * like a syntactic literal. Null anywhere the type keeps more than one
5409
+ * key. */
5410
+ export function recordKeyTypeLiteralText(L, node) {
5411
+ if (!ts.isIdentifier(node))
5412
+ return null;
5413
+ let t = L.typeOf(node);
5414
+ if (t.flags & ts.TypeFlags.TypeParameter) {
5415
+ const bound = L.typeParamTsResolver(t);
5416
+ if (!bound)
5417
+ return null;
5418
+ t = bound;
5419
+ }
5420
+ if (t.isStringLiteralType())
5421
+ return t.value;
5422
+ if (t.isNumberLiteralType()) {
5423
+ const n = t.value;
5424
+ return Number.isFinite(n) && n >= 0 ? String(n) : null;
5425
+ }
5426
+ return null;
5427
+ }
5072
5428
  /** Can every value a dynamic key can reach (declared fields + the
5073
5429
  * overflow) surface as the read's result type? dyn results need
5074
5430
  * DOM-convertible fields (JSON-safe, with the undefined arm allowed at
@@ -5232,8 +5588,13 @@ export function lowerElementWrite(L, expr) {
5232
5588
  if (shape && !shape.tuple) {
5233
5589
  // Literal keys — string literals AND numeric literals in their
5234
5590
  // canonical string spelling (`b[1] = v` writes field/key "1", JS's
5235
- // own key derivation) — name declared fields directly.
5236
- const litKey = recordKeyLiteralText(target.argumentExpression);
5591
+ // own key derivation) — name declared fields directly. A key
5592
+ // IDENTIFIER whose TYPE proves one literal (a literal-typed const,
5593
+ // a keyof-constrained type parameter bound to a literal inside a
5594
+ // generic instance — `set(o, "a", v)`'s body writing `o[k] = v`)
5595
+ // is the same static field write.
5596
+ const litKey = recordKeyLiteralText(target.argumentExpression) ??
5597
+ recordKeyTypeLiteralText(L, target.argumentExpression);
5237
5598
  if (litKey !== null) {
5238
5599
  const field = shape.fields.find((f) => f.name === litKey);
5239
5600
  if (field) {
@@ -5243,7 +5604,40 @@ export function lowerElementWrite(L, expr) {
5243
5604
  }
5244
5605
  }
5245
5606
  if (!shape.indexValue) {
5246
- L.unsupported("SC1090", target, "dynamic keyed writes to records without an index signature (spell the field name)");
5607
+ // A SIGNATURE-FREE shape whose declared fields share ONE type
5608
+ // writes through the same per-shape dispatch (the mockable-clock
5609
+ // module shape: `mocked[functionality] = implementation` over a
5610
+ // table of same-signature closures). A key naming no declared
5611
+ // field throws the catchable TypeError — JS would ADD the
5612
+ // property, which a monomorphic struct cannot (the SEMANTICS.md
5613
+ // keyed-write-miss divergence). Mixed-type and accessor-carrying
5614
+ // shapes keep the fence.
5615
+ const common = shape.fields.length > 0 &&
5616
+ !shapeHasAccessorSlots(shape) &&
5617
+ shape.fields.every((f) => typeEquals(f.type, shape.fields[0].type))
5618
+ ? shape.fields[0].type
5619
+ : null;
5620
+ if (!common) {
5621
+ L.unsupported("SC1090", target, "dynamic keyed writes to records without an index signature (declared fields of ONE shared type dispatch by key — spell the field name otherwise)");
5622
+ }
5623
+ const obj = L.lowerExpr(target.expression);
5624
+ let key = litKey !== null
5625
+ ? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
5626
+ : L.lowerExpr(target.argumentExpression);
5627
+ // Runtime number/boolean/dyn keys stringify — ToPropertyKey,
5628
+ // exactly the index-signature path's rule.
5629
+ if (key.type.kind === "f64")
5630
+ key = L.ensureString(key, target.argumentExpression);
5631
+ if (key.type.kind === "bool" || key.type.kind === "dyn") {
5632
+ key = { kind: "toString", operand: key, type: STRING, loc: locOf(target.argumentExpression) };
5633
+ }
5634
+ if (key.type.kind !== "string") {
5635
+ L.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
5636
+ }
5637
+ const value = L.coerceInto(expr.right, L.lowerExpr(expr.right), common);
5638
+ if (!typeEquals(value.type, common))
5639
+ L.badType(expr.right, L.typeOf(expr.right));
5640
+ return { kind: "recordKeySet", obj, shapeId: receiverIr.shapeId, key, value, loc: locOf(expr) };
5247
5641
  }
5248
5642
  // A LITERAL key naming no declared field is a pure overflow insert
5249
5643
  // — no collision to validate. Runtime keys must be able to collide
@@ -7043,6 +7437,27 @@ export function lowerInstanceOf(L, expr, loc) {
7043
7437
  return { kind: "dynTest", test: "bytes", value: left, type: BOOL, loc };
7044
7438
  }
7045
7439
  }
7440
+ // `x instanceof RegExp` over a union with a regex arm (the
7441
+ // skip-utility `string | RegExp` dispatch): a union tag test — the
7442
+ // socket-narrowing shape; the checker types the branches and
7443
+ // maybeNarrow bridges the reads. A plain regex-typed LHS folds
7444
+ // true; the fold keeps the operand-purity rule of the class folds
7445
+ // (identifier reads only).
7446
+ if (ts.isIdentifier(expr.right) &&
7447
+ L.isStdlibGlobal(expr.right, "RegExp") &&
7448
+ !L.caughtLocalOf(expr.left)) {
7449
+ const left = L.lowerExpr(expr.left);
7450
+ if (left.type.kind === "union") {
7451
+ const def = L.unions.get(left.type.unionId);
7452
+ const tag = def ? def.arms.findIndex((a) => a.kind === "regex") : -1;
7453
+ if (tag >= 0) {
7454
+ return { kind: "unionIsTag", unionId: left.type.unionId, tag, negated: false, value: left, type: BOOL, loc };
7455
+ }
7456
+ }
7457
+ if (left.type.kind === "regex" && left.kind === "varRef" && ts.isIdentifier(expr.left)) {
7458
+ return { kind: "boolLit", value: true, type: BOOL, loc };
7459
+ }
7460
+ }
7046
7461
  // `x instanceof X` where X is a class VALUE (a classval-typed
7047
7462
  // binding): the target is DYNAMIC — a hierarchy target reads its
7048
7463
  // interval from the class object at runtime (instanceOfValue); a