@scriptc/compiler 0.0.8 → 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 (66) 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 +108 -5
  5. package/dist/backend/emission/emit-exprs.js.map +1 -1
  6. package/dist/backend/emission/emit-types.d.ts +3 -0
  7. package/dist/backend/emission/emit-types.js +12 -0
  8. package/dist/backend/emission/emit-types.js.map +1 -1
  9. package/dist/backend/emission/emit-walkers.d.ts +7 -5
  10. package/dist/backend/emission/emit-walkers.js +23 -10
  11. package/dist/backend/emission/emit-walkers.js.map +1 -1
  12. package/dist/backend/emission/emitter.d.ts +1 -1
  13. package/dist/backend/emission/emitter.js +30 -9
  14. package/dist/backend/emission/emitter.js.map +1 -1
  15. package/dist/backend/llvm/emitter.js +206 -11
  16. package/dist/backend/llvm/emitter.js.map +1 -1
  17. package/dist/diagnostics/diagnostic.d.ts +55 -10
  18. package/dist/diagnostics/diagnostic.js +82 -20
  19. package/dist/diagnostics/diagnostic.js.map +1 -1
  20. package/dist/frontend/lowering/lower-assert.js +16 -0
  21. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  22. package/dist/frontend/lowering/lower-builtins.d.ts +11 -0
  23. package/dist/frontend/lowering/lower-builtins.js +111 -3
  24. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  25. package/dist/frontend/lowering/lower-calls.d.ts +30 -11
  26. package/dist/frontend/lowering/lower-calls.js +418 -8
  27. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  28. package/dist/frontend/lowering/lower-classes.js +27 -1
  29. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  30. package/dist/frontend/lowering/lower-containers.d.ts +4 -2
  31. package/dist/frontend/lowering/lower-containers.js +193 -43
  32. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  33. package/dist/frontend/lowering/lower-emitter.js +54 -0
  34. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  35. package/dist/frontend/lowering/lower-exprs.js +52 -7
  36. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  37. package/dist/frontend/lowering/lower-stmts.js +445 -13
  38. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  39. package/dist/frontend/lowering/lower-stream.js +28 -2
  40. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  41. package/dist/frontend/lowering/lowerer.d.ts +66 -14
  42. package/dist/frontend/lowering/lowerer.js +273 -28
  43. package/dist/frontend/lowering/lowerer.js.map +1 -1
  44. package/dist/frontend/lowering/surfaces.js +20 -3
  45. package/dist/frontend/lowering/surfaces.js.map +1 -1
  46. package/dist/frontend/npm-static-rewrite.d.ts +7 -2
  47. package/dist/frontend/npm-static-rewrite.js +566 -138
  48. package/dist/frontend/npm-static-rewrite.js.map +1 -1
  49. package/dist/frontend/npm-static.js +11 -2
  50. package/dist/frontend/npm-static.js.map +1 -1
  51. package/dist/frontend/program.js +5 -5
  52. package/dist/frontend/program.js.map +1 -1
  53. package/dist/frontend/shared.d.ts +6 -1
  54. package/dist/frontend/shared.js +46 -1
  55. package/dist/frontend/shared.js.map +1 -1
  56. package/dist/frontend/types.js +4 -2
  57. package/dist/frontend/types.js.map +1 -1
  58. package/dist/index.js +150 -25
  59. package/dist/index.js.map +1 -1
  60. package/dist/ir/nodes.d.ts +131 -7
  61. package/dist/ir/nodes.js +41 -3
  62. package/dist/ir/nodes.js.map +1 -1
  63. package/dist/ir/validate.js +58 -8
  64. package/dist/ir/validate.js.map +1 -1
  65. package/package.json +2 -2
  66. package/surface-manifest.json +10 -3
@@ -388,8 +388,10 @@ export declare function lowerObjectFromEntriesCall(L: Lowerer, call: ts.CallExpr
388
388
  * every source value (declared fields, and the overflow slot when the
389
389
  * source carries one) must enter the target's value slot (identity, a
390
390
  * width lift — arm wrap/re-tag/nested reshape, or JSON-safe into a dyn
391
- * slot), and every target field must be optional-flavored (the fresh
392
- * record's default) and writable at runtime. */
391
+ * slot), and every target field must either be initialized directly by
392
+ * a same-named source declared field (the rule that admits REQUIRED
393
+ * declared members) or be optional-flavored (the fresh record's
394
+ * default) and writable at runtime. */
393
395
  export declare function lowerRecordOvfCaptureHelper(L: Lowerer, fromId: string, toId: string, loc: SrcLoc): string | null;
394
396
  /** `Object.assign(target, ...sources)` into an INDEX-SIGNATURE record:
395
397
  * the capture helper's keyed-write walk aimed at the EXISTING target —
@@ -5,12 +5,12 @@
5
5
  import * as ts from "../ts7/adapter.js";
6
6
  import { BOOL, BYTES_U8, CAUGHT, DYN, F64, JSVAL, STRING, UNDEFINED_T, VOID, arrayOf, bytesOf, funcOf, isRefCounted, isSupportedIndexValue, isUnitType, typeEquals } from "../../ir/nodes.js";
7
7
  import { ARRAY_METHODS, MAP_METHODS, SET_COMBINE_METHODS, SET_METHODS, STR_METHODS } from "./surfaces.js";
8
- import { isRequireMainFilename, probeLower, pureReemittable } from "./lower-exprs.js";
8
+ import { isRequireMainFilename, lowerDynObjectLiteral, probeLower, pureReemittable } from "./lower-exprs.js";
9
9
  import { forOfVarTarget, lowerDestructuringAssign } from "./lower-stmts.js";
10
10
  import { isJsSourceFile, locOf } from "../program.js";
11
11
  import { DYN_DISPATCH_METHODS, islandPrimitiveExit } from "./lower-calls.js";
12
12
  import { typeKey } from "../types.js";
13
- import { own } from "./lowerer.js";
13
+ import { dynUndefinedExpr, own } from "./lowerer.js";
14
14
  /** Ambient array method calls. `push`/`pop`/`indexOf`/`includes`/`join`
15
15
  * lower to arrIntrinsic; the HOF family — `map`/`filter`/`forEach`/
16
16
  * `find`/`some`/`every`/`flatMap`/`reduce`/`reduceRight` — desugars to a
@@ -4104,25 +4104,35 @@ export function lowerBytesNew(L, expr, symbol) {
4104
4104
  const seed = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
4105
4105
  return { kind: "bytesNew", source: seed, type, loc };
4106
4106
  }
4107
- // The SYNTACTIC `new T(new SharedArrayBuffer(n))` form — the
4108
- // Atomics.wait sleep idiom's construction. The buffer never exists
4109
- // as a value: n must be a byte-length LITERAL divisible by the
4110
- // element size (tsc would admit any number; a bad one is Node's
4111
- // RangeError — rejected at compile time instead of half-lowering),
4112
- // and the whole expression is a zero-filled typed array of
4113
- // n/elemSize elements. Sharing is unobservable without threads
4114
- // (scriptc has none), so erasing the SharedArrayBuffer is exact —
4115
- // SEMANTICS.md documents the stance.
4107
+ // The SYNTACTIC `new T(new ArrayBuffer(n))` and `new T(new
4108
+ // SharedArrayBuffer(n))` forms — fresh-buffer construction (the
4109
+ // shared spelling is the Atomics.wait sleep idiom's). The buffer
4110
+ // never exists as a value: n must be a byte-length LITERAL divisible
4111
+ // by the element size (tsc would admit any number; a bad one is
4112
+ // Node's RangeError — rejected at compile time instead of
4113
+ // half-lowering), and the whole expression is a zero-filled typed
4114
+ // array of n/elemSize elements. Erasing the buffer is exact: nothing
4115
+ // else can ever reference it, so neither sharing (scriptc has no
4116
+ // threads) nor aliasing is observable — SEMANTICS.md documents the
4117
+ // stance. The RESIZABLE form (a maxByteLength options bag) fences by
4118
+ // name: resize needs the buffer to exist as a value, and none does.
4116
4119
  if (ts.isNewExpression(argNode) &&
4117
4120
  ts.isIdentifier(argNode.expression) &&
4118
- argNode.expression.text === "SharedArrayBuffer" &&
4121
+ (argNode.expression.text === "ArrayBuffer" ||
4122
+ argNode.expression.text === "SharedArrayBuffer") &&
4119
4123
  L.isStdlibSymbol(L.resolveValueSymbol(argNode.expression) ?? undefined)) {
4124
+ const bufCtor = argNode.expression.text;
4125
+ if ((argNode.arguments?.length ?? 0) > 1) {
4126
+ L.noLowering(`new ${name} over a resizable ${bufCtor}`, argNode, "a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer " +
4127
+ "to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here " +
4128
+ "erases into the view): drop the options bag");
4129
+ }
4120
4130
  const elemSize = elem === "u8" ? 1 : 4;
4121
4131
  const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined;
4122
4132
  const lenT = lenArg ? L.typeOf(lenArg) : null;
4123
4133
  const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
4124
4134
  if (byteLen === null || byteLen % elemSize !== 0 || byteLen < 0) {
4125
- L.noLowering(`new ${name} over this SharedArrayBuffer`, argNode, `the byte length must be a number literal divisible by ${elemSize} — new ${name}(new SharedArrayBuffer(${elemSize}))`);
4135
+ L.noLowering(`new ${name} over this ${bufCtor}`, argNode, `the byte length must be a number literal divisible by ${elemSize} — new ${name}(new ${bufCtor}(${elemSize}))`);
4126
4136
  }
4127
4137
  const count = { kind: "numLit", value: byteLen / elemSize, type: F64, loc };
4128
4138
  return { kind: "bytesNew", source: count, type, loc };
@@ -4156,8 +4166,37 @@ function lowerDataViewNew(L, expr) {
4156
4166
  if (!bufNode || args.length > 3 || args.some(ts.isSpreadElement)) {
4157
4167
  L.noLowering(`new DataView with ${args.length} arguments`, expr, "supported: new DataView(x.buffer), (x.buffer, byteOffset), or (x.buffer, byteOffset, byteLength)");
4158
4168
  }
4169
+ // The FRESH-BUFFER form: `new DataView(new ArrayBuffer(n), ...)` — the
4170
+ // buffer erases into the view (nothing else can ever reference it, so
4171
+ // the aliasing a shared buffer would exhibit is unobservable): the
4172
+ // view's storage is a fresh zero-filled n-byte allocation. n must be a
4173
+ // non-negative integer LITERAL (tsc admits any number; a fractional
4174
+ // one is ToIndex truncation nothing here implements). byteOffset and
4175
+ // byteLength keep their runtime Node-RangeError story. The RESIZABLE
4176
+ // form (a maxByteLength options bag) fences by name.
4177
+ if (ts.isNewExpression(bufNode) &&
4178
+ ts.isIdentifier(bufNode.expression) &&
4179
+ bufNode.expression.text === "ArrayBuffer" &&
4180
+ L.isStdlibSymbol(L.resolveValueSymbol(bufNode.expression) ?? undefined)) {
4181
+ if ((bufNode.arguments?.length ?? 0) > 1) {
4182
+ L.noLowering("new DataView over a resizable ArrayBuffer", bufNode, "a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer " +
4183
+ "to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here " +
4184
+ "erases into the view): drop the options bag");
4185
+ }
4186
+ const lenArg = bufNode.arguments?.length === 1 ? bufNode.arguments[0] : undefined;
4187
+ const lenT = lenArg ? L.typeOf(lenArg) : null;
4188
+ const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
4189
+ if (byteLen === null || !Number.isInteger(byteLen) || byteLen < 0) {
4190
+ L.noLowering("new DataView over this ArrayBuffer", bufNode, "the byte length must be a non-negative integer literal — new DataView(new ArrayBuffer(8))");
4191
+ }
4192
+ const count = { kind: "numLit", value: byteLen, type: F64, loc };
4193
+ const receiver = { kind: "bytesNew", source: count, type: BYTES_U8, loc };
4194
+ const idxArgs = args.slice(1).map((a) => L.lowerExprExpecting(a, F64));
4195
+ return { kind: "bytesIntrinsic", method: "dataViewNew", receiver, args: idxArgs, type: BYTES_U8, loc };
4196
+ }
4159
4197
  const hint = "views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) " +
4160
- "where x is a Uint8Array/Uint32Array/Float32Array/Buffer value; free-standing ArrayBuffers have no representation";
4198
+ "where x is a Uint8Array/Uint32Array/Float32Array/Buffer value — or a fresh buffer erased into " +
4199
+ "the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation";
4161
4200
  if (!ts.isPropertyAccessExpression(bufNode) || bufNode.name.text !== "buffer" || bufNode.questionDotToken) {
4162
4201
  L.noLowering("new DataView over this buffer expression", bufNode, hint);
4163
4202
  }
@@ -4329,6 +4368,27 @@ export function lowerBytesMethodCall(L, call, access) {
4329
4368
  args.push(L.lowerExprExpecting(call.arguments[1], BOOL));
4330
4369
  return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc };
4331
4370
  }
4371
+ // DataView setters — the getters' mirror: (byteOffset, value) plus the
4372
+ // optional littleEndian bool on the multi-byte kinds. Void results;
4373
+ // the same constant Node RangeError on a bad offset (may-throw seeds).
4374
+ // setBigUint64/setBigInt64 never lower (bigint ARGUMENTS have no
4375
+ // representation and no composed form exists) — they keep the member
4376
+ // fence, as does setFloat16.
4377
+ const dvSetter = own(DV_SETTERS, name);
4378
+ if (dvSetter !== undefined && receiverIr.elem === "u8") {
4379
+ const maxArgs = dvSetter.le ? 3 : 2;
4380
+ if (nArgs < 2 || nArgs > maxArgs) {
4381
+ L.noLowering(`.${name} with ${nArgs} arguments`, call);
4382
+ }
4383
+ const receiver = L.lowerExpr(access.expression);
4384
+ const args = [
4385
+ L.lowerExprExpecting(call.arguments[0], F64),
4386
+ L.lowerExprExpecting(call.arguments[1], F64),
4387
+ ];
4388
+ if (nArgs === 3)
4389
+ args.push(L.lowerExprExpecting(call.arguments[2], BOOL));
4390
+ return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc };
4391
+ }
4332
4392
  return null;
4333
4393
  }
4334
4394
  /** Node's Buffer encoding names → the runtime's canonical spelling (every
@@ -4418,20 +4478,59 @@ function lowerBufferInstanceMethod(L, call, access, name, loc) {
4418
4478
  }
4419
4479
  return v;
4420
4480
  };
4481
+ // The checked-dynamic crossing for a compare/equals argument slot: dyn
4482
+ // passes through, convertible statics (the invalid-input probes'
4483
+ // literals) wrap in dynFrom, `undefined` literals ride the same wrap.
4484
+ // Fences (never returns) when the value cannot cross — an island jsval.
4485
+ const chkArg = (i) => {
4486
+ // Object literals take the DOM literal path directly (method members
4487
+ // box as dyn functions — the typed record fence never applies).
4488
+ if (ts.isObjectLiteralExpression(args[i])) {
4489
+ return lowerDynObjectLiteral(L, args[i]);
4490
+ }
4491
+ const v = L.lowerExpr(args[i]);
4492
+ if (v.type.kind === "dyn")
4493
+ return v;
4494
+ if (v.kind === "unitLit" || L.dynConvertible(v.type)) {
4495
+ return { kind: "dynFrom", value: v, type: DYN, loc: v.loc };
4496
+ }
4497
+ L.noLowering(`.${name} of '${L.fmt(v.type)}' values`, args[i], "a Buffer/Uint8Array argument is the lowered shape (narrow unions first)");
4498
+ };
4499
+ const argIrKind = (i) => L.mapTypeOf(L.typeOf(args[i]));
4421
4500
  if (name === "equals") {
4422
4501
  if (nArgs !== 1)
4423
4502
  L.noLowering(`.equals with ${nArgs} arguments`, call);
4424
- const other = u8Arg(0);
4503
+ if (argIrKind(0)?.kind === "bytes") {
4504
+ const other = u8Arg(0);
4505
+ const receiver = L.lowerExpr(access.expression);
4506
+ return { kind: "bytesIntrinsic", method: "equals", receiver, args: [other], type: BOOL, loc };
4507
+ }
4508
+ // Not statically bytes (the invalid-input probes, untyped JS
4509
+ // helpers): Node's "otherBuffer" argument ladder runs at runtime.
4425
4510
  const receiver = L.lowerExpr(access.expression);
4426
- return { kind: "bytesIntrinsic", method: "equals", receiver, args: [other], type: BOOL, loc };
4511
+ return { kind: "libCall", fn: "bytes.equalsChk", args: [receiver, chkArg(0)], type: BOOL, loc };
4427
4512
  }
4428
4513
  if (name === "compare") {
4429
- if (nArgs < 1 || nArgs > 5)
4514
+ if (nArgs > 5)
4430
4515
  L.noLowering(`.compare with ${nArgs} arguments`, call);
4431
- const target = u8Arg(0);
4432
- const idx = call.arguments.slice(1).map((a) => L.lowerExprExpecting(a, F64));
4516
+ const fast = nArgs >= 1 &&
4517
+ argIrKind(0)?.kind === "bytes" &&
4518
+ call.arguments.slice(1).every((a) => L.mapTypeOf(L.typeOf(a))?.kind === "f64");
4519
+ if (fast) {
4520
+ const target = u8Arg(0);
4521
+ const idx = call.arguments.slice(1).map((a) => L.lowerExprExpecting(a, F64));
4522
+ const receiver = L.lowerExpr(access.expression);
4523
+ return { kind: "bytesIntrinsic", method: "compareBuf", receiver, args: [target, ...idx], type: F64, loc };
4524
+ }
4525
+ // An absent/ill-typed target or offset (`a.compare()`, string
4526
+ // offsets, explicit undefined): Node's target/targetStart/targetEnd/
4527
+ // sourceStart/sourceEnd ladder runs at runtime; absent slots pass
4528
+ // the undefined dyn (Node defaults apply there).
4433
4529
  const receiver = L.lowerExpr(access.expression);
4434
- return { kind: "bytesIntrinsic", method: "compareBuf", receiver, args: [target, ...idx], type: F64, loc };
4530
+ const slots = [];
4531
+ for (let i = 0; i < 5; i++)
4532
+ slots.push(i < nArgs ? chkArg(i) : dynUndefinedExpr(loc));
4533
+ return { kind: "libCall", fn: "bytes.compareChk", args: [receiver, ...slots], type: F64, loc };
4435
4534
  }
4436
4535
  if (name === "indexOf" || name === "lastIndexOf" || name === "includes") {
4437
4536
  if (nArgs < 1 || nArgs > 3)
@@ -4568,6 +4667,16 @@ const DV_GETTERS = {
4568
4667
  getBigUint64: { method: "dvGetBigUint64Number", le: true },
4569
4668
  getBigInt64: { method: "dvGetBigInt64Number", le: true },
4570
4669
  };
4670
+ const DV_SETTERS = {
4671
+ setUint8: { method: "dvSetUint8", le: false },
4672
+ setInt8: { method: "dvSetInt8", le: false },
4673
+ setUint16: { method: "dvSetUint16", le: true },
4674
+ setInt16: { method: "dvSetInt16", le: true },
4675
+ setUint32: { method: "dvSetUint32", le: true },
4676
+ setInt32: { method: "dvSetInt32", le: true },
4677
+ setFloat32: { method: "dvSetFloat32", le: true },
4678
+ setFloat64: { method: "dvSetFloat64", le: true },
4679
+ };
4571
4680
  /** The Buffer statics — `Buffer.from(...)`, `Buffer.alloc(n)`,
4572
4681
  * `Buffer.concat(list)`, `Buffer.isBuffer(x)` — on THE stdlib Buffer
4573
4682
  * global (name + provenance; fallback and @types/node alike). Null when
@@ -4676,14 +4785,22 @@ export function lowerBufferStaticCall(L, call, access) {
4676
4785
  if (args.length !== 2 || args.some(ts.isSpreadElement)) {
4677
4786
  L.noLowering(`Buffer.compare with ${args.length} arguments`, call);
4678
4787
  }
4679
- const sides = args.map((a) => {
4680
- const v = L.lowerExpr(a);
4681
- if (!(v.type.kind === "bytes" && v.type.elem === "u8")) {
4682
- L.noLowering(`Buffer.compare of '${L.fmt(v.type)}' values`, a, "Buffer/Uint8Array values compare (narrow unions first)");
4788
+ const sides = args.map((a) => L.lowerExpr(a));
4789
+ if (sides.every((v) => v.type.kind === "bytes" && v.type.elem === "u8")) {
4790
+ return { kind: "bytesIntrinsic", method: "compareBuf", receiver: sides[0], args: [sides[1]], type: F64, loc };
4791
+ }
4792
+ // A side that is not statically bytes (the invalid-input probes,
4793
+ // untyped JS helpers): Node's "buf1"/"buf2" argument ladder runs
4794
+ // at runtime — a well-typed dyn still compares.
4795
+ const dyns = sides.map((v, i) => {
4796
+ if (v.type.kind === "dyn")
4797
+ return v;
4798
+ if (v.kind === "unitLit" || L.dynConvertible(v.type)) {
4799
+ return { kind: "dynFrom", value: v, type: DYN, loc: v.loc };
4683
4800
  }
4684
- return v;
4801
+ L.noLowering(`Buffer.compare of '${L.fmt(v.type)}' values`, args[i], "Buffer/Uint8Array values compare (narrow unions first)");
4685
4802
  });
4686
- return { kind: "bytesIntrinsic", method: "compareBuf", receiver: sides[0], args: [sides[1]], type: F64, loc };
4803
+ return { kind: "libCall", fn: "buffer.compareChk", args: dyns, type: F64, loc };
4687
4804
  }
4688
4805
  if (member === "isBuffer") {
4689
4806
  // The type-predicate narrowing test. Lowered where it DECIDES
@@ -5192,8 +5309,10 @@ function lowerFromEntriesStringRows(L, call, argNode, argIr) {
5192
5309
  * every source value (declared fields, and the overflow slot when the
5193
5310
  * source carries one) must enter the target's value slot (identity, a
5194
5311
  * width lift — arm wrap/re-tag/nested reshape, or JSON-safe into a dyn
5195
- * slot), and every target field must be optional-flavored (the fresh
5196
- * record's default) and writable at runtime. */
5312
+ * slot), and every target field must either be initialized directly by
5313
+ * a same-named source declared field (the rule that admits REQUIRED
5314
+ * declared members) or be optional-flavored (the fresh record's
5315
+ * default) and writable at runtime. */
5197
5316
  export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5198
5317
  const from = L.shapes.get(fromId);
5199
5318
  const to = L.shapes.get(toId);
@@ -5214,11 +5333,16 @@ export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5214
5333
  // signature have no overflow to carry over).
5215
5334
  if (fIv && slotLift(fIv) === null)
5216
5335
  return null;
5217
- // Every target field defaults to its undefined arm (fresh record) and
5218
- // must accept a runtime collision (dyn slots validate via dynCheck —
5219
- // fields must be DOM-convertible; typed slots write through directly).
5220
- const defaults = [];
5336
+ const inits = [];
5337
+ const consumed = new Set();
5221
5338
  for (const tf of to.fields) {
5339
+ const sf = from.fields.find((f) => f.name === tf.name);
5340
+ const directLift = sf ? L.widthLiftPlan(sf.type, tf.type) : null;
5341
+ if (sf && directLift) {
5342
+ inits.push({ name: tf.name, kind: "direct", src: sf.type, lift: directLift });
5343
+ consumed.add(tf.name);
5344
+ continue;
5345
+ }
5222
5346
  if (tf.type.kind !== "union")
5223
5347
  return null;
5224
5348
  const utag = L.armTag(tf.type.unionId, UNDEFINED_T);
@@ -5226,7 +5350,7 @@ export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5226
5350
  return null;
5227
5351
  if (tIv.kind === "dyn" ? !L.dynConvertible(tf.type) : !typeEquals(tf.type, tIv))
5228
5352
  return null;
5229
- defaults.push({ name: tf.name, unionId: tf.type.unionId, utag });
5353
+ inits.push({ name: tf.name, kind: "undef", unionId: tf.type.unionId, utag });
5230
5354
  }
5231
5355
  // Every source declared field must flow into the keyed-write slot —
5232
5356
  // in DECLARATION order (insertion order below; declaredOrder omits
@@ -5239,11 +5363,30 @@ export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5239
5363
  : from.fields;
5240
5364
  const fieldLifts = new Map();
5241
5365
  for (const ff of orderedFields) {
5366
+ if (consumed.has(ff.name))
5367
+ continue; // direct-initialized above
5242
5368
  const lift = slotLift(ff.type);
5243
5369
  if (lift === null)
5244
5370
  return null;
5245
5371
  fieldLifts.set(ff.name, lift);
5246
5372
  }
5373
+ // DISPATCH writes — keys that can hit a declared target slot: the
5374
+ // overflow loop's runtime keys, and a literal source-field name the
5375
+ // target also declares. Non-dyn slots store THROUGH on a collision, so
5376
+ // such writes need every declared field to BE the slot type (the
5377
+ // validator's recordKeySet rule); dyn slots validate per field
5378
+ // (dynCheck), so each field must be DOM-convertible. Writes to
5379
+ // literal names the target does NOT declare are overflowOnly and
5380
+ // exempt — which is what lets a direct-initialized required field
5381
+ // (`id: number` beside a `number | undefined` slot) coexist with a
5382
+ // declared-only source, while an index-signature source declines.
5383
+ const dispatchWrites = fIv !== undefined ||
5384
+ orderedFields.some((ff) => !consumed.has(ff.name) && to.fields.some((f) => f.name === ff.name));
5385
+ if (dispatchWrites) {
5386
+ if (tIv.kind === "dyn" ? !to.fields.every((f) => L.dynConvertible(f.type)) : !to.fields.every((f) => typeEquals(f.type, tIv))) {
5387
+ return null;
5388
+ }
5389
+ }
5247
5390
  const key = `ovf:${fromId}:${toId}`;
5248
5391
  const existing = L.widthHelpers.get(key);
5249
5392
  if (existing)
@@ -5265,16 +5408,18 @@ export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5265
5408
  localId: "out.0",
5266
5409
  init: {
5267
5410
  kind: "recordLit",
5268
- fields: defaults.map((d) => ({
5411
+ fields: inits.map((d) => ({
5269
5412
  name: d.name,
5270
- value: {
5271
- kind: "unionWrap",
5272
- unionId: d.unionId,
5273
- tag: d.utag,
5274
- value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
5275
- type: to.fields.find((f) => f.name === d.name).type,
5276
- loc,
5277
- },
5413
+ value: d.kind === "direct"
5414
+ ? L.applyWidthLift(d.lift, { kind: "recordGet", obj: sRef, shapeId: fromId, field: d.name, type: d.src, loc }, to.fields.find((f) => f.name === d.name).type, loc)
5415
+ : {
5416
+ kind: "unionWrap",
5417
+ unionId: d.unionId,
5418
+ tag: d.utag,
5419
+ value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
5420
+ type: to.fields.find((f) => f.name === d.name).type,
5421
+ loc,
5422
+ },
5278
5423
  })),
5279
5424
  type: toT,
5280
5425
  loc,
@@ -5283,8 +5428,10 @@ export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5283
5428
  },
5284
5429
  ];
5285
5430
  // Source declared fields in declaration order, skipping unset
5286
- // optionals (stance 37).
5431
+ // optionals (stance 37) and the direct-initialized (consumed) names.
5287
5432
  for (const ff of orderedFields) {
5433
+ if (consumed.has(ff.name))
5434
+ continue;
5288
5435
  const raw = { kind: "recordGet", obj: sRef, shapeId: fromId, field: ff.name, type: ff.type, loc };
5289
5436
  const utag = ff.type.kind === "union" ? L.armTag(ff.type.unionId, UNDEFINED_T) : -1;
5290
5437
  const write = {
@@ -5293,6 +5440,9 @@ export function lowerRecordOvfCaptureHelper(L, fromId, toId, loc) {
5293
5440
  shapeId: toId,
5294
5441
  key: { kind: "strLit", value: ff.name, type: STRING, loc },
5295
5442
  value: intoSlot(raw, fieldLifts.get(ff.name)),
5443
+ // A literal name the target does not declare can only land in the
5444
+ // overflow — skip the declared dispatch (and its validator gate).
5445
+ ...(to.fields.some((f) => f.name === ff.name) ? {} : { overflowOnly: true }),
5296
5446
  loc,
5297
5447
  };
5298
5448
  body.push(utag >= 0 && ff.type.kind === "union"