@scriptc/compiler 0.0.0 → 0.0.2

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 (206) hide show
  1. package/LICENSE +202 -0
  2. package/ambient/package.json +3 -0
  3. package/ambient/scriptc-node-fallback.d.ts +3001 -0
  4. package/ambient/scriptc-overrides.d.ts +95 -0
  5. package/ambient/scriptc.d.ts +74 -0
  6. package/dist/backend/cc.d.ts +169 -0
  7. package/dist/backend/cc.js +897 -0
  8. package/dist/backend/cc.js.map +1 -0
  9. package/dist/backend/emission/emit-async.d.ts +145 -0
  10. package/dist/backend/emission/emit-async.js +996 -0
  11. package/dist/backend/emission/emit-async.js.map +1 -0
  12. package/dist/backend/emission/emit-exprs.d.ts +3 -0
  13. package/dist/backend/emission/emit-exprs.js +5949 -0
  14. package/dist/backend/emission/emit-exprs.js.map +1 -0
  15. package/dist/backend/emission/emit-island.d.ts +45 -0
  16. package/dist/backend/emission/emit-island.js +271 -0
  17. package/dist/backend/emission/emit-island.js.map +1 -0
  18. package/dist/backend/emission/emit-shapes.d.ts +142 -0
  19. package/dist/backend/emission/emit-shapes.js +575 -0
  20. package/dist/backend/emission/emit-shapes.js.map +1 -0
  21. package/dist/backend/emission/emit-stmts.d.ts +78 -0
  22. package/dist/backend/emission/emit-stmts.js +960 -0
  23. package/dist/backend/emission/emit-stmts.js.map +1 -0
  24. package/dist/backend/emission/emit-types.d.ts +65 -0
  25. package/dist/backend/emission/emit-types.js +652 -0
  26. package/dist/backend/emission/emit-types.js.map +1 -0
  27. package/dist/backend/emission/emit-walkers.d.ts +154 -0
  28. package/dist/backend/emission/emit-walkers.js +1587 -0
  29. package/dist/backend/emission/emit-walkers.js.map +1 -0
  30. package/dist/backend/emission/emitter.d.ts +452 -0
  31. package/dist/backend/emission/emitter.js +1249 -0
  32. package/dist/backend/emission/emitter.js.map +1 -0
  33. package/dist/backend/emission/may-throw.d.ts +15 -0
  34. package/dist/backend/emission/may-throw.js +250 -0
  35. package/dist/backend/emission/may-throw.js.map +1 -0
  36. package/dist/backend/llvm/blocks.d.ts +21 -0
  37. package/dist/backend/llvm/blocks.js +68 -0
  38. package/dist/backend/llvm/blocks.js.map +1 -0
  39. package/dist/backend/llvm/classes.d.ts +84 -0
  40. package/dist/backend/llvm/classes.js +438 -0
  41. package/dist/backend/llvm/classes.js.map +1 -0
  42. package/dist/backend/llvm/dyn.d.ts +123 -0
  43. package/dist/backend/llvm/dyn.js +2624 -0
  44. package/dist/backend/llvm/dyn.js.map +1 -0
  45. package/dist/backend/llvm/emitter.d.ts +3 -0
  46. package/dist/backend/llvm/emitter.js +10102 -0
  47. package/dist/backend/llvm/emitter.js.map +1 -0
  48. package/dist/backend/llvm/shapes.d.ts +78 -0
  49. package/dist/backend/llvm/shapes.js +754 -0
  50. package/dist/backend/llvm/shapes.js.map +1 -0
  51. package/dist/backend/llvm/unsupported.d.ts +6 -0
  52. package/dist/backend/llvm/unsupported.js +12 -0
  53. package/dist/backend/llvm/unsupported.js.map +1 -0
  54. package/dist/backend/llvm/walkers.d.ts +58 -0
  55. package/dist/backend/llvm/walkers.js +793 -0
  56. package/dist/backend/llvm/walkers.js.map +1 -0
  57. package/dist/backend/mangle.d.ts +124 -0
  58. package/dist/backend/mangle.js +232 -0
  59. package/dist/backend/mangle.js.map +1 -0
  60. package/dist/coverage/report.d.ts +65 -0
  61. package/dist/coverage/report.js +238 -0
  62. package/dist/coverage/report.js.map +1 -0
  63. package/dist/diagnostics/diagnostic.d.ts +140 -0
  64. package/dist/diagnostics/diagnostic.js +458 -0
  65. package/dist/diagnostics/diagnostic.js.map +1 -0
  66. package/dist/diagnostics/render.d.ts +11 -0
  67. package/dist/diagnostics/render.js +58 -0
  68. package/dist/diagnostics/render.js.map +1 -0
  69. package/dist/frontend/cjs-lexer.d.ts +20 -0
  70. package/dist/frontend/cjs-lexer.js +813 -0
  71. package/dist/frontend/cjs-lexer.js.map +1 -0
  72. package/dist/frontend/lowering/http2-constants.d.ts +1 -0
  73. package/dist/frontend/lowering/http2-constants.js +251 -0
  74. package/dist/frontend/lowering/http2-constants.js.map +1 -0
  75. package/dist/frontend/lowering/lib-boundary.d.ts +6 -0
  76. package/dist/frontend/lowering/lib-boundary.js +143 -0
  77. package/dist/frontend/lowering/lib-boundary.js.map +1 -0
  78. package/dist/frontend/lowering/lower-assert.d.ts +18 -0
  79. package/dist/frontend/lowering/lower-assert.js +1269 -0
  80. package/dist/frontend/lowering/lower-assert.js.map +1 -0
  81. package/dist/frontend/lowering/lower-builtins.d.ts +511 -0
  82. package/dist/frontend/lowering/lower-builtins.js +5249 -0
  83. package/dist/frontend/lowering/lower-builtins.js.map +1 -0
  84. package/dist/frontend/lowering/lower-calls.d.ts +581 -0
  85. package/dist/frontend/lowering/lower-calls.js +6286 -0
  86. package/dist/frontend/lowering/lower-calls.js.map +1 -0
  87. package/dist/frontend/lowering/lower-classes.d.ts +727 -0
  88. package/dist/frontend/lowering/lower-classes.js +4701 -0
  89. package/dist/frontend/lowering/lower-classes.js.map +1 -0
  90. package/dist/frontend/lowering/lower-comptime.d.ts +53 -0
  91. package/dist/frontend/lowering/lower-comptime.js +244 -0
  92. package/dist/frontend/lowering/lower-comptime.js.map +1 -0
  93. package/dist/frontend/lowering/lower-containers.d.ts +486 -0
  94. package/dist/frontend/lowering/lower-containers.js +6345 -0
  95. package/dist/frontend/lowering/lower-containers.js.map +1 -0
  96. package/dist/frontend/lowering/lower-dgram.d.ts +20 -0
  97. package/dist/frontend/lowering/lower-dgram.js +319 -0
  98. package/dist/frontend/lowering/lower-dgram.js.map +1 -0
  99. package/dist/frontend/lowering/lower-emitter.d.ts +10 -0
  100. package/dist/frontend/lowering/lower-emitter.js +688 -0
  101. package/dist/frontend/lowering/lower-emitter.js.map +1 -0
  102. package/dist/frontend/lowering/lower-enums.d.ts +22 -0
  103. package/dist/frontend/lowering/lower-enums.js +235 -0
  104. package/dist/frontend/lowering/lower-enums.js.map +1 -0
  105. package/dist/frontend/lowering/lower-expando.d.ts +35 -0
  106. package/dist/frontend/lowering/lower-expando.js +276 -0
  107. package/dist/frontend/lowering/lower-expando.js.map +1 -0
  108. package/dist/frontend/lowering/lower-exprs.d.ts +524 -0
  109. package/dist/frontend/lowering/lower-exprs.js +8111 -0
  110. package/dist/frontend/lowering/lower-exprs.js.map +1 -0
  111. package/dist/frontend/lowering/lower-generators.d.ts +50 -0
  112. package/dist/frontend/lowering/lower-generators.js +421 -0
  113. package/dist/frontend/lowering/lower-generators.js.map +1 -0
  114. package/dist/frontend/lowering/lower-inspect.d.ts +20 -0
  115. package/dist/frontend/lowering/lower-inspect.js +1074 -0
  116. package/dist/frontend/lowering/lower-inspect.js.map +1 -0
  117. package/dist/frontend/lowering/lower-island.d.ts +108 -0
  118. package/dist/frontend/lowering/lower-island.js +650 -0
  119. package/dist/frontend/lowering/lower-island.js.map +1 -0
  120. package/dist/frontend/lowering/lower-mixins.d.ts +96 -0
  121. package/dist/frontend/lowering/lower-mixins.js +534 -0
  122. package/dist/frontend/lowering/lower-mixins.js.map +1 -0
  123. package/dist/frontend/lowering/lower-modules.d.ts +135 -0
  124. package/dist/frontend/lowering/lower-modules.js +1613 -0
  125. package/dist/frontend/lowering/lower-modules.js.map +1 -0
  126. package/dist/frontend/lowering/lower-namespaces.d.ts +168 -0
  127. package/dist/frontend/lowering/lower-namespaces.js +706 -0
  128. package/dist/frontend/lowering/lower-namespaces.js.map +1 -0
  129. package/dist/frontend/lowering/lower-server.d.ts +90 -0
  130. package/dist/frontend/lowering/lower-server.js +3471 -0
  131. package/dist/frontend/lowering/lower-server.js.map +1 -0
  132. package/dist/frontend/lowering/lower-stmts.d.ts +361 -0
  133. package/dist/frontend/lowering/lower-stmts.js +5341 -0
  134. package/dist/frontend/lowering/lower-stmts.js.map +1 -0
  135. package/dist/frontend/lowering/lower-stream.d.ts +88 -0
  136. package/dist/frontend/lowering/lower-stream.js +1533 -0
  137. package/dist/frontend/lowering/lower-stream.js.map +1 -0
  138. package/dist/frontend/lowering/lower-test.d.ts +27 -0
  139. package/dist/frontend/lowering/lower-test.js +353 -0
  140. package/dist/frontend/lowering/lower-test.js.map +1 -0
  141. package/dist/frontend/lowering/lowerer.d.ts +1748 -0
  142. package/dist/frontend/lowering/lowerer.js +6257 -0
  143. package/dist/frontend/lowering/lowerer.js.map +1 -0
  144. package/dist/frontend/lowering/surfaces.d.ts +272 -0
  145. package/dist/frontend/lowering/surfaces.js +1096 -0
  146. package/dist/frontend/lowering/surfaces.js.map +1 -0
  147. package/dist/frontend/npm-static.d.ts +55 -0
  148. package/dist/frontend/npm-static.js +300 -0
  149. package/dist/frontend/npm-static.js.map +1 -0
  150. package/dist/frontend/npm.d.ts +330 -0
  151. package/dist/frontend/npm.js +1259 -0
  152. package/dist/frontend/npm.js.map +1 -0
  153. package/dist/frontend/program.d.ts +186 -0
  154. package/dist/frontend/program.js +2255 -0
  155. package/dist/frontend/program.js.map +1 -0
  156. package/dist/frontend/provenance-registry.d.ts +48 -0
  157. package/dist/frontend/provenance-registry.js +87 -0
  158. package/dist/frontend/provenance-registry.js.map +1 -0
  159. package/dist/frontend/provenance.d.ts +6 -0
  160. package/dist/frontend/provenance.js +459 -0
  161. package/dist/frontend/provenance.js.map +1 -0
  162. package/dist/frontend/resolve.d.ts +56 -0
  163. package/dist/frontend/resolve.js +682 -0
  164. package/dist/frontend/resolve.js.map +1 -0
  165. package/dist/frontend/shared.d.ts +67 -0
  166. package/dist/frontend/shared.js +241 -0
  167. package/dist/frontend/shared.js.map +1 -0
  168. package/dist/frontend/ts7/adapter.d.ts +7 -0
  169. package/dist/frontend/ts7/adapter.js +54 -0
  170. package/dist/frontend/ts7/adapter.js.map +1 -0
  171. package/dist/frontend/ts7/ast.d.ts +50 -0
  172. package/dist/frontend/ts7/ast.js +211 -0
  173. package/dist/frontend/ts7/ast.js.map +1 -0
  174. package/dist/frontend/ts7/census-check.d.ts +1 -0
  175. package/dist/frontend/ts7/census-check.js +12 -0
  176. package/dist/frontend/ts7/census-check.js.map +1 -0
  177. package/dist/frontend/ts7/checker.d.ts +140 -0
  178. package/dist/frontend/ts7/checker.js +544 -0
  179. package/dist/frontend/ts7/checker.js.map +1 -0
  180. package/dist/frontend/ts7/enums.d.ts +18 -0
  181. package/dist/frontend/ts7/enums.js +43 -0
  182. package/dist/frontend/ts7/enums.js.map +1 -0
  183. package/dist/frontend/ts7/program.d.ts +99 -0
  184. package/dist/frontend/ts7/program.js +278 -0
  185. package/dist/frontend/ts7/program.js.map +1 -0
  186. package/dist/frontend/ts7/world-check.d.ts +3 -0
  187. package/dist/frontend/ts7/world-check.js +34 -0
  188. package/dist/frontend/ts7/world-check.js.map +1 -0
  189. package/dist/frontend/types.d.ts +205 -0
  190. package/dist/frontend/types.js +2485 -0
  191. package/dist/frontend/types.js.map +1 -0
  192. package/dist/index.d.ts +85 -0
  193. package/dist/index.js +429 -0
  194. package/dist/index.js.map +1 -0
  195. package/dist/ir/nodes.d.ts +4334 -0
  196. package/dist/ir/nodes.js +1906 -0
  197. package/dist/ir/nodes.js.map +1 -0
  198. package/dist/ir/serialize.d.ts +4 -0
  199. package/dist/ir/serialize.js +34 -0
  200. package/dist/ir/serialize.js.map +1 -0
  201. package/dist/ir/validate.d.ts +36 -0
  202. package/dist/ir/validate.js +4885 -0
  203. package/dist/ir/validate.js.map +1 -0
  204. package/package.json +30 -6
  205. package/README.md +0 -3
  206. package/index.js +0 -2
@@ -0,0 +1,1587 @@
1
+ import { DYN_HANDLE_KINDS, isRefCounted, typeEquals, typeKey } from "../../ir/nodes.js";
2
+ import { cDecl, cStringLiteral, cType, elemAccess, releaseCallC, retainCallC, vAdapters } from "./emit-types.js";
3
+ import { mangleField, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
4
+ import { OVERFLOW_MEMBER } from "./emit-shapes.js";
5
+ /** Short human description of a dynCheck target for error messages
6
+ * ("expected number | string at $.items[2]"). Records read as "object" —
7
+ * the path already says where; the full shape would bloat messages. */
8
+ export function dynDesc(E, t) {
9
+ switch (t.kind) {
10
+ case "f64":
11
+ return "number";
12
+ case "string":
13
+ return "string";
14
+ case "bool":
15
+ return "boolean";
16
+ case "record":
17
+ // Tuples read as "array": that IS the JSON representation the
18
+ // caller must supply (arity failures carry their own message).
19
+ return E.recordsById.get(t.shapeId)?.tuple ? "array" : "object";
20
+ case "array":
21
+ return "array";
22
+ case "nullT":
23
+ return "null";
24
+ // Reachable through optional-flavored record fields ("expected
25
+ // string | undefined at $.a" — honest: the fix is a string value OR
26
+ // omitting the key).
27
+ case "undefinedT":
28
+ return "undefined";
29
+ // dyn fields accept any DOM value — only reachable in messages as a
30
+ // sibling of a failing field, never as the failure itself.
31
+ case "dyn":
32
+ return "unknown";
33
+ case "bytes":
34
+ return "Uint8Array";
35
+ // The %Error extraction (an instanceof-Error narrow / cast on an
36
+ // unknown value): the failure names the class, like caughtCheck's.
37
+ case "object":
38
+ return t.className.replace(/^%/, "");
39
+ case "union": {
40
+ const def = E.unionsById.get(t.unionId);
41
+ if (!def)
42
+ throw new Error(`emitter bug: dynDesc of unknown union ${t.unionId}`);
43
+ return def.arms.map((a) => E.dynDesc(a)).join(" | ");
44
+ }
45
+ // Function targets (the checked-dynamic function boundary): the
46
+ // failure names the expected callability, not the full signature —
47
+ // the path already says where.
48
+ case "func":
49
+ return "function";
50
+ default: {
51
+ // Runtime HANDLE targets name the class ("expected
52
+ // IncomingMessage at $, got string").
53
+ const h = DYN_HANDLE_KINDS.get(t.kind);
54
+ if (h)
55
+ return h.cls;
56
+ throw new Error(`emitter bug: dynDesc of non-JSON type ${t.kind}`);
57
+ }
58
+ }
59
+ }
60
+ /** The per-union ToBoolean helper (interned per unionId): switch on the
61
+ * runtime tag — unit arms false, f64 arms 0/NaN-falsy, string arms
62
+ * empty-falsy, bool arms by payload, ref arms always true (JS objects),
63
+ * jsval arms answered by the engine. Borrows the operand. */
64
+ export function unionTruthyHelper(E, unionId) {
65
+ const existing = E.unionTruthyFns.get(unionId);
66
+ if (existing)
67
+ return existing;
68
+ const def = E.unionsById.get(unionId);
69
+ if (!def)
70
+ throw new Error(`emitter bug: truthiness of unknown union ${unionId}`);
71
+ const name = `sc_ut_${E.unionTruthyFns.size}`;
72
+ E.unionTruthyFns.set(unionId, name);
73
+ const sig = `static bool ${name}(ScrUnion *v)`;
74
+ E.walkerProtos.push(`${sig}; /* ToBoolean ${unionId} */`);
75
+ const d = [`${sig} { /* ToBoolean ${unionId} */`, ` switch (v->tag) {`];
76
+ def.arms.forEach((arm, i) => {
77
+ switch (arm.kind) {
78
+ case "undefinedT":
79
+ case "nullT":
80
+ d.push(` case ${i}: return false;`);
81
+ break;
82
+ case "f64":
83
+ d.push(` case ${i}: { double x = scr_union_get_f64(v); return x == x && x != 0; }`);
84
+ break;
85
+ case "bool":
86
+ d.push(` case ${i}: return scr_union_get_bool(v);`);
87
+ break;
88
+ case "string":
89
+ d.push(` case ${i}: return ((ScrStr *)scr_union_peek(v))->len != 0;`);
90
+ break;
91
+ case "jsval":
92
+ d.push(` case ${i}: return scr_jsval_truthy((ScrJsval *)scr_union_peek(v)) != 0;`);
93
+ break;
94
+ default:
95
+ // Arrays, maps, sets, records, objects, functions, promises,
96
+ // regexes, ...: JS objects are always truthy — [] and {} included.
97
+ d.push(` case ${i}: return true; /* ${arm.kind} */`);
98
+ }
99
+ });
100
+ d.push(` default: fputs("scriptc: internal error: invalid union tag\\n", stderr); abort();`, ` }`, `}`, ``);
101
+ E.walkerDefs.push(...d);
102
+ return name;
103
+ }
104
+ /** The per-union strict-equality helper (interned per unionId):
105
+ * different tags are never equal; equal tags compare the arm values —
106
+ * unit arms equal, f64 with C == (NaN !== NaN, +0 === -0), strings by
107
+ * bytes, bools by value, ref arms by POINTER identity (JS object
108
+ * equality). Borrows both operands. */
109
+ export function unionEqHelper(E, unionId) {
110
+ const existing = E.unionEqFns.get(unionId);
111
+ if (existing)
112
+ return existing;
113
+ const def = E.unionsById.get(unionId);
114
+ if (!def)
115
+ throw new Error(`emitter bug: equality of unknown union ${unionId}`);
116
+ const name = `sc_ue_${E.unionEqFns.size}`;
117
+ E.unionEqFns.set(unionId, name);
118
+ const sig = `static bool ${name}(ScrUnion *a, ScrUnion *b)`;
119
+ E.walkerProtos.push(`${sig}; /* strict equality ${unionId} */`);
120
+ const d = [
121
+ `${sig} { /* strict equality ${unionId} */`,
122
+ ` if (a->tag != b->tag) return false;`,
123
+ ` switch (a->tag) {`,
124
+ ];
125
+ def.arms.forEach((arm, i) => {
126
+ switch (arm.kind) {
127
+ case "undefinedT":
128
+ case "nullT":
129
+ d.push(` case ${i}: return true;`);
130
+ break;
131
+ case "f64":
132
+ d.push(` case ${i}: return scr_union_get_f64(a) == scr_union_get_f64(b);`);
133
+ break;
134
+ case "bool":
135
+ d.push(` case ${i}: return scr_union_get_bool(a) == scr_union_get_bool(b);`);
136
+ break;
137
+ case "string":
138
+ d.push(` case ${i}: return scr_str_eq((ScrStr *)scr_union_peek(a), (ScrStr *)scr_union_peek(b));`);
139
+ break;
140
+ default:
141
+ // Ref arms: pointer identity, exactly JS object equality.
142
+ d.push(` case ${i}: return scr_union_peek(a) == scr_union_peek(b); /* ${arm.kind} */`);
143
+ }
144
+ });
145
+ d.push(` default: fputs("scriptc: internal error: invalid union tag\\n", stderr); abort();`, ` }`, `}`, ``);
146
+ E.walkerDefs.push(...d);
147
+ return name;
148
+ }
149
+ /** The per-union ToString helper (interned per unionId): switch on the
150
+ * runtime tag — unit arms return the interned "undefined"/"null" texts,
151
+ * string arms retain the payload, f64/bool arms go through the JS-exact
152
+ * formatters (String(x) semantics). Ref arms never arrive (the frontend
153
+ * fences unions with object arms from string conversion — JS would print
154
+ * "[object Object]"). Borrows the operand; the result is owned (+1). */
155
+ export function unionToStrHelper(E, unionId) {
156
+ const existing = E.unionToStrFns.get(unionId);
157
+ if (existing)
158
+ return existing;
159
+ const def = E.unionsById.get(unionId);
160
+ if (!def)
161
+ throw new Error(`emitter bug: ToString of unknown union ${unionId}`);
162
+ const name = `sc_us_${E.unionToStrFns.size}`;
163
+ E.unionToStrFns.set(unionId, name);
164
+ const sig = `static ScrStr *${name}(ScrUnion *v)`;
165
+ E.walkerProtos.push(`${sig}; /* ToString ${unionId} */`);
166
+ const d = [`${sig} { /* ToString ${unionId} */`, ` switch (v->tag) {`];
167
+ def.arms.forEach((arm, i) => {
168
+ switch (arm.kind) {
169
+ case "undefinedT":
170
+ case "nullT": {
171
+ const lit = E.internLiteral(arm.kind === "undefinedT" ? "undefined" : "null");
172
+ d.push(` case ${i}: return scr_str_retain((ScrStr *)&${lit});`);
173
+ break;
174
+ }
175
+ case "string":
176
+ d.push(` case ${i}: return scr_str_retain((ScrStr *)scr_union_peek(v));`);
177
+ break;
178
+ case "f64":
179
+ d.push(` case ${i}: return scr_f64_to_scrstr(scr_union_get_f64(v));`);
180
+ break;
181
+ case "bool":
182
+ d.push(` case ${i}: return scr_bool_to_scrstr(scr_union_get_bool(v));`);
183
+ break;
184
+ case "bytes": {
185
+ // Buffer.toString() IS the utf8 decode (Node's default
186
+ // encoding) — the `Buffer | string` chunk idiom.
187
+ const enc = E.internLiteral("utf8");
188
+ d.push(` case ${i}: return scr_bytes_to_str((ScrBytes *)scr_union_peek(v), (ScrStr *)&${enc});`);
189
+ break;
190
+ }
191
+ default:
192
+ throw new Error(`emitter bug: ToString of union with a ${arm.kind} arm (frontend fences these)`);
193
+ }
194
+ });
195
+ d.push(` default: fputs("scriptc: internal error: invalid union tag\\n", stderr); abort();`, ` }`, `}`, ``);
196
+ E.walkerDefs.push(...d);
197
+ return name;
198
+ }
199
+ /** The per-union array-join helper (interned per unionId): Array.prototype
200
+ * .join over union elements — undefined/null arms print EMPTY (exactly
201
+ * JS's join, which silences only the nullish elements), every other arm
202
+ * goes through the per-union ToString walker (string arms verbatim,
203
+ * f64/bool via the JS-exact formatters). The frontend admits only unions
204
+ * of f64/string/bool/unit arms here. Borrows the array and separator;
205
+ * the result is owned (+1). */
206
+ export function unionJoinHelper(E, unionId) {
207
+ const existing = E.unionJoinFns.get(unionId);
208
+ if (existing)
209
+ return existing;
210
+ const def = E.unionsById.get(unionId);
211
+ if (!def)
212
+ throw new Error(`emitter bug: join of unknown union ${unionId}`);
213
+ const name = `sc_uj_${E.unionJoinFns.size}`;
214
+ E.unionJoinFns.set(unionId, name);
215
+ const toStr = unionToStrHelper(E, unionId);
216
+ const unitTags = def.arms.flatMap((a, i) => a.kind === "undefinedT" || a.kind === "nullT" ? [i] : []);
217
+ const sig = `static ScrStr *${name}(ScrArr *a, ScrStr *sep)`;
218
+ E.walkerProtos.push(`${sig}; /* join ${unionId} */`);
219
+ E.walkerDefs.push(`${sig} { /* Array#join over ${unionId}: nullish arms print empty */`, ` ScrJsonBuf b;`, ` scr_jb_init(&b);`, ` size_t n = (size_t)scr_arr_len(a);`, ` for (size_t i = 0; i < n; i++) {`, ` if (i) for (size_t j = 0; j < sep->len; j++) scr_jb_putc(&b, sep->data[j]);`, ` ScrUnion *u = (ScrUnion *)scr_arr_get_ref(a, (double)i);`, ...(unitTags.length > 0
220
+ ? [` if (${unitTags.map((t) => `u->tag == ${t}`).join(" || ")}) { scr_union_release(u); continue; }`]
221
+ : []), ` ScrStr *s = ${toStr}(u);`, ` for (size_t j = 0; j < s->len; j++) scr_jb_putc(&b, s->data[j]);`, ` scr_str_release(s);`, ` scr_union_release(u);`, ` }`, ` return scr_jb_finish(&b);`, `}`, ``);
222
+ return name;
223
+ }
224
+ /** The dyn ToString helper pair (interned once, type-independent):
225
+ * Node's String() over a DOM value — "undefined"/"null" texts, bools,
226
+ * JS-exact number formatting (NaN/Infinity spelled out — NOT the JSON
227
+ * null), strings verbatim, arrays via Array.prototype.toString (join
228
+ * with ","; null/undefined ELEMENTS print empty, nested arrays flatten
229
+ * through the recursion — JS-exact), plain objects as
230
+ * "[object Object]". The operand is borrowed; the result is owned (+1). */
231
+ export function dynToStrHelper(E) {
232
+ if (E.dynToStrFn)
233
+ return E.dynToStrFn;
234
+ const name = "sc_ds";
235
+ E.dynToStrFn = name;
236
+ E.walkerProtos.push(`static void ${name}_buf(ScrJsonBuf *b, const ScrDyn *d); /* String(unknown) walker */`, `static ScrStr *${name}(const ScrDyn *d); /* String(unknown) */`);
237
+ E.walkerDefs.push(`static void ${name}_buf(ScrJsonBuf *b, const ScrDyn *d) { /* String(unknown), recursive */`, ` switch (d->kind) {`, ` case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); break;`, ` case SCR_DYN_NULL: scr_jb_puts(b, "null"); break;`, ` case SCR_DYN_BOOL: scr_jb_puts(b, d->v.b ? "true" : "false"); break;`, ` case SCR_DYN_NUM: {`, ` ScrStr *s = scr_f64_to_scrstr(d->v.num); /* String(n): NaN/Infinity spelled out, not JSON null */`, ` for (size_t i = 0; i < s->len; i++) scr_jb_putc(b, s->data[i]);`, ` scr_str_release(s);`, ` break;`, ` }`, ` case SCR_DYN_STR:`, ` for (size_t i = 0; i < d->v.str->len; i++) scr_jb_putc(b, d->v.str->data[i]);`, ` break;`, ` case SCR_DYN_ARR:`, ` /* Array.prototype.toString: join(",") — null/undefined ELEMENTS`, ` * print empty (unlike top level), nested arrays flatten. */`, ` for (size_t i = 0; i < d->v.arr.len; i++) {`, ` if (i > 0) scr_jb_putc(b, ',');`, ` const ScrDyn *e = d->v.arr.items[i];`, ` if (e->kind == SCR_DYN_UNDEF || e->kind == SCR_DYN_NULL) continue;`, ` ${name}_buf(b, e);`, ` }`, ` break;`, ` case SCR_DYN_OBJ:`, ` if (scr_dyn_obj_get(d, "%error", 6)) {`, ` /* The DOM's error encoding (caughtToDyn): Error.prototype`, ` * .toString over the encoded name/message — Node's String(err),`, ` * which carries no stack either. */`, ` const ScrDyn *en = scr_dyn_obj_get(d, "name", 4);`, ` const ScrDyn *em = scr_dyn_obj_get(d, "message", 7);`, ` const ScrStr *ens = (en && en->kind == SCR_DYN_STR) ? en->v.str : NULL;`, ` const ScrStr *ems = (em && em->kind == SCR_DYN_STR) ? em->v.str : NULL;`, ` if (ens) for (size_t i = 0; i < ens->len; i++) scr_jb_putc(b, ens->data[i]);`, ` if (ens && ens->len && ems && ems->len) scr_jb_puts(b, ": ");`, ` if (ems) for (size_t i = 0; i < ems->len; i++) scr_jb_putc(b, ems->data[i]);`, ` break;`, ` }`, ` scr_jb_puts(b, "[object Object]");`, ` break;`, ` case SCR_DYN_BYTES: {`, ` /* Buffer-flavored values (stream chunks) coerce utf8 (Node's`, ` * Buffer.toString); plain Uint8Array joins its elements. */`, ` if (d->buffer) {`, ` ScrStr *enc = scr_str_new("utf8", 4);`, ` ScrStr *txt = scr_bytes_to_str(d->v.bytes, enc);`, ` scr_str_release(enc);`, ` for (size_t i = 0; i < txt->len; i++) scr_jb_putc(b, txt->data[i]);`, ` scr_str_release(txt);`, ` break;`, ` }`, ` for (size_t i = 0; i < d->v.bytes->len; i++) {`, ` if (i > 0) scr_jb_putc(b, ',');`, ` char n[16];`, ` snprintf(n, sizeof n, "%u", (unsigned)d->v.bytes->data[i]);`, ` scr_jb_puts(b, n);`, ` }`, ` break;`, ` }`, ` case SCR_DYN_FUNC:`, ` /* Function.prototype.toString: the SOURCE is gone in a compiled`, ` * program, so the native-code form — exactly what JS engines`, ` * print for their own non-JS functions. */`, ` scr_jb_puts(b, "function ");`, ` if (d->v.fn.name) scr_jb_puts(b, d->v.fn.name);`, ` scr_jb_puts(b, "() { [native code] }");`, ` break;`, ` case SCR_DYN_HANDLE:`, ` /* Object.prototype.toString — Node's String() over these`, ` * classes (IncomingMessage/ServerResponse/Socket). */`, ` scr_jb_puts(b, "[object Object]");`, ` break;`, ` case SCR_DYN_PROMISE:`, ` /* Object.prototype.toString with the Promise @@toStringTag. */`, ` scr_jb_puts(b, "[object Promise]");`, ` break;`, ` }`, `}`, `static ScrStr *${name}(const ScrDyn *d) { /* String(unknown) -> owned (+1) */`, ` if (d->kind == SCR_DYN_STR) return scr_str_retain(d->v.str);`, ` ScrJsonBuf b;`, ` scr_jb_init(&b);`, ` ${name}_buf(&b, d);`, ` return scr_jb_finish(&b);`, `}`, ``);
238
+ return name;
239
+ }
240
+ /** The caught→dyn converter (interned once, type-independent): a catch
241
+ * binding flowing into an `unknown` slot — the typed→unknown deep-copy
242
+ * stance over the exception snapshot's runtime kind. Scalar payloads
243
+ * convert exactly; an Error-family OBJ payload becomes the DOM's error
244
+ * encoding — the reserved "%error" marker plus name/message (and code
245
+ * when stamped), so `instanceof Error`, the %Error extraction, and
246
+ * String() answer like Node; every other payload (REF — records, arrays,
247
+ * closures, unions — and non-Error hierarchy objects, type-erased at
248
+ * runtime) becomes an EMPTY DOM object (SEMANTICS.md 67). Borrows the
249
+ * snapshot; the result is a fresh tree (+1). Never throws. */
250
+ export function caughtToDynHelper(E) {
251
+ if (E.caughtToDynFn)
252
+ return E.caughtToDynFn;
253
+ const name = "sc_cd";
254
+ E.caughtToDynFn = name;
255
+ const sig = `static ScrDyn *${name}(const ScrCaught *c)`;
256
+ E.walkerProtos.push(`${sig}; /* caught -> unknown */`);
257
+ E.walkerDefs.push(`${sig} { /* caught -> unknown (+1, fresh tree) */`, ` switch (c->kind) {`, ` case SCR_EXC_F64: return scr_dyn_new_num(c->f64);`, ` case SCR_EXC_BOOL: return scr_dyn_new_bool(c->b);`, ` case SCR_EXC_STR: return scr_dyn_new_str((ScrStr *)c->payload); /* _new_str retains */`, ` case SCR_EXC_OBJ:`, ` if (scr_error_is(c->payload)) {`, ` /* The identity-cached crossing (scr_json.c): the SAME error`, ` * instance boxes to ONE DOM node however it crosses, so a`, ` * caught dyn compares reference-equal to the thrown error's`, ` * other boxings, like Node. */`, ` return scr_dyn_from_error((const ScrError *)c->payload);`, ` }`, ` /* FALLTHROUGH: a non-Error hierarchy object is type-erased */`, ` case SCR_EXC_REF:`, ` /* A thrown DOM value passes back BY REFERENCE (identity with`, ` * every other holder of the node — the traced-throw shape). */`, ` if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_retain((ScrDyn *)c->payload);`, ` /* FALLTHROUGH */`, ` default:`, ` /* non-dyn REF and non-Error objects: the "[object Object]"`, ` * approximation — truthy, typeof "object", fields unreadable. */`, ` return scr_dyn_new_obj();`, ` }`, `}`, ``);
258
+ return name;
259
+ }
260
+ /** The pretty-print re-indenter (interned once, type-independent):
261
+ * `JSON.stringify(v, null, space)` output as a REWRITE of the compact
262
+ * serializer text — Node's gap algorithm exactly. Structural '{'/'[' open
263
+ * a newline + one-deeper indent (unless immediately closed: `{}` and `[]`
264
+ * stay inline, like Node), '}'/']' close onto their own line at the outer
265
+ * depth, ',' breaks the line at the current depth, and the key ':' gains
266
+ * one space. String state (with escape skipping) keeps literal braces,
267
+ * commas, and colons inside JSON strings untouched. Compact input is
268
+ * BORROWED; the result is a fresh string (+1). An empty indent (space 0,
269
+ * "", null) never reaches here — the frontend drops the property. */
270
+ export function jsonIndentHelper(E) {
271
+ if (E.jsonIndentFn)
272
+ return E.jsonIndentFn;
273
+ const name = "sc_ji";
274
+ E.jsonIndentFn = name;
275
+ const sig = `static ScrStr *${name}(ScrStr *compact, const char *indent, size_t ilen)`;
276
+ E.walkerProtos.push(`${sig}; /* stringify space re-indent */`);
277
+ E.walkerDefs.push(`${sig} { /* stringify space re-indent (Node's gap algorithm) */`, ` ScrJsonBuf b;`, ` scr_jb_init(&b);`, ` const char *s = compact->data;`, ` size_t n = compact->len;`, ` size_t depth = 0;`, ` bool instr = false;`, `#define SCR_JI_INDENT() do { \\`, ` scr_jb_putc(&b, '\\n'); \\`, ` for (size_t td = 0; td < depth; td++) \\`, ` for (size_t tk = 0; tk < ilen; tk++) scr_jb_putc(&b, indent[tk]); \\`, ` } while (0)`, ` for (size_t i = 0; i < n; i++) {`, ` char c = s[i];`, ` if (instr) {`, ` scr_jb_putc(&b, c);`, ` if (c == '\\\\' && i + 1 < n) { scr_jb_putc(&b, s[++i]); }`, ` else if (c == '"') instr = false;`, ` continue;`, ` }`, ` switch (c) {`, ` case '"': instr = true; scr_jb_putc(&b, c); break;`, ` case '{': case '[':`, ` scr_jb_putc(&b, c);`, ` if (i + 1 < n && s[i + 1] == (c == '{' ? '}' : ']')) {`, ` scr_jb_putc(&b, s[++i]); /* empty {} / [] stay inline, like Node */`, ` break;`, ` }`, ` depth++;`, ` SCR_JI_INDENT();`, ` break;`, ` case '}': case ']':`, ` depth--;`, ` SCR_JI_INDENT();`, ` scr_jb_putc(&b, c);`, ` break;`, ` case ',':`, ` scr_jb_putc(&b, ',');`, ` SCR_JI_INDENT();`, ` break;`, ` case ':':`, ` scr_jb_putc(&b, ':');`, ` scr_jb_putc(&b, ' ');`, ` break;`, ` default:`, ` scr_jb_putc(&b, c);`, ` }`, ` }`, `#undef SCR_JI_INDENT`, ` return scr_jb_finish(&b);`, `}`, ``);
278
+ return name;
279
+ }
280
+ export function jsonWriteHelper(E, t) {
281
+ const key = typeKey(t);
282
+ const existing = E.jsonWriters.get(key);
283
+ if (existing)
284
+ return existing;
285
+ const name = `sc_jw_${E.jsonWriters.size}`;
286
+ E.jsonWriters.set(key, name);
287
+ const sig = `static void ${name}(ScrJsonBuf *b, ${cDecl(t, "v")})`;
288
+ E.walkerProtos.push(`${sig}; /* stringify ${key} */`);
289
+ const d = [`${sig} { /* stringify ${key} */`];
290
+ switch (t.kind) {
291
+ case "f64":
292
+ d.push(` scr_jb_put_f64(b, v); /* NaN/Infinity -> null, -0 -> 0, like JS */`);
293
+ break;
294
+ case "bool":
295
+ d.push(` scr_jb_puts(b, v ? "true" : "false");`);
296
+ break;
297
+ case "string":
298
+ d.push(` scr_jb_put_json_str(b, v);`);
299
+ break;
300
+ case "record": {
301
+ const shape = E.recordsById.get(t.shapeId);
302
+ if (!shape)
303
+ throw new Error(`emitter bug: jsonStringify of unknown shape ${t.shapeId}`);
304
+ // A tuple serializes as a JSON ARRAY in index order — JS-exact
305
+ // (JSON.stringify(["a", 1]) is `["a",1]`). Every position is
306
+ // required, so labels/commas are static like all-required records.
307
+ if (shape.tuple) {
308
+ const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
309
+ d.push(` scr_jb_putc(b, '[');`);
310
+ byIndex.forEach((f, i) => {
311
+ if (i > 0)
312
+ d.push(` scr_jb_putc(b, ',');`);
313
+ d.push(` ${E.jsonWriteHelper(f.type)}(b, v->${mangleField(f.name)});`);
314
+ });
315
+ d.push(` scr_jb_putc(b, ']');`);
316
+ break;
317
+ }
318
+ // Fields serialize in DECLARED order — JS insertion order, exactly
319
+ // Node for objects constructed in declaration order (SEMANTICS.md
320
+ // 36 documents the divergence when they are not) — never the
321
+ // canonical (sorted) struct order. declaredOrder may omit internal
322
+ // '%'-fields (Dirent's %dtype): those are hidden from JSON exactly
323
+ // like every other key-order surface. Shapes interned WITHOUT an
324
+ // order (the CJS export-table literal, accessor-narrowed JS
325
+ // shapes) keep the canonical order — their construction paths
326
+ // document the divergence.
327
+ const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
328
+ const inOrder = new Set(order);
329
+ if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
330
+ throw new Error(`emitter bug: declaredOrder of shape ${t.shapeId} omits a non-internal field`);
331
+ }
332
+ const byName = new Map(shape.fields.map((f) => [f.name, f]));
333
+ const emitFields = order.map((n) => byName.get(n)).filter((f) => f !== undefined);
334
+ // Optional-flavored fields (undefined-armed unions) DROP from the
335
+ // output while they hold the undefined arm — exactly Node — so as
336
+ // soon as one exists, comma placement turns dynamic (a `first`
337
+ // flag); all-required shapes keep the static prefix labels. An
338
+ // overflow portion forces the dynamic path too (entry count is
339
+ // runtime state).
340
+ const droppable = emitFields.some((f) => E.undefinedArmTag(f.type) >= 0) || !!shape.indexValue;
341
+ d.push(` scr_jb_putc(b, '{');`);
342
+ if (!droppable) {
343
+ emitFields.forEach((f, i) => {
344
+ const label = cStringLiteral(Buffer.from(`${i > 0 ? "," : ""}"${f.name}":`, "utf8"));
345
+ d.push(` scr_jb_puts(b, ${label});`);
346
+ // Refcounted fields are BORROWED straight off the struct (the
347
+ // record itself is borrowed for the whole call).
348
+ d.push(` ${E.jsonWriteHelper(f.type)}(b, v->${mangleField(f.name)});`);
349
+ });
350
+ }
351
+ else {
352
+ d.push(` bool first = true;`);
353
+ for (const f of emitFields) {
354
+ const label = cStringLiteral(Buffer.from(`"${f.name}":`, "utf8"));
355
+ const utag = E.undefinedArmTag(f.type);
356
+ const pad = utag >= 0 ? " " : " ";
357
+ if (utag >= 0) {
358
+ d.push(` if (v->${mangleField(f.name)}->tag != ${utag}) { /* undefined-valued field: dropped, like Node */`);
359
+ }
360
+ d.push(`${pad}if (!first) scr_jb_putc(b, ',');`);
361
+ d.push(`${pad}first = false;`);
362
+ d.push(`${pad}scr_jb_puts(b, ${label});`);
363
+ d.push(`${pad}${E.jsonWriteHelper(f.type)}(b, v->${mangleField(f.name)});`);
364
+ if (utag >= 0)
365
+ d.push(` }`);
366
+ }
367
+ // Overflow entries follow the declared fields, themselves in JS
368
+ // OWN-KEY order — integer-like keys ascending first, then the
369
+ // rest in insertion order (scr_map_keys_js_order, the same
370
+ // enumeration Object.keys answers; number-keyed signatures make
371
+ // integer-like overflow keys ordinary). SEMANTICS.md documents
372
+ // the declared-canonical-then-overflow divergence from JS's one
373
+ // interleaved key order. Keys escape like any JSON string;
374
+ // undefined-valued entries drop, exactly the optional-field rule.
375
+ if (shape.indexValue) {
376
+ const iv = shape.indexValue;
377
+ const m = `v->${OVERFLOW_MEMBER}`;
378
+ d.push(` {`);
379
+ d.push(` ScrArr *ks = scr_map_keys_js_order(${m});`);
380
+ d.push(` for (size_t i = 0; i < ks->len; i++) {`);
381
+ d.push(` ScrStr *k = (ScrStr *)scr_arr_get_ref(ks, (double)i);`);
382
+ if (iv.kind === "f64" || iv.kind === "bool") {
383
+ d.push(` ${cDecl(iv, "e")} = 0;`);
384
+ d.push(` scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(${m}, k, &e);`);
385
+ }
386
+ else {
387
+ d.push(` ${cDecl(iv, "e")} = (${cType(iv).trim()})scr_map_get_str_ref(${m}, k);`);
388
+ }
389
+ const skipUndef = iv.kind === "dyn"
390
+ ? `e->kind == SCR_DYN_UNDEF`
391
+ : E.undefinedArmTag(iv) >= 0
392
+ ? `e->tag == ${E.undefinedArmTag(iv)}`
393
+ : null;
394
+ if (skipUndef) {
395
+ d.push(` if (${skipUndef}) { /* undefined-valued entry: dropped, like Node */`);
396
+ d.push(` ${releaseCallC(iv, "e")};`);
397
+ d.push(` scr_str_release(k);`);
398
+ d.push(` continue;`);
399
+ d.push(` }`);
400
+ }
401
+ d.push(` if (!first) scr_jb_putc(b, ',');`);
402
+ d.push(` first = false;`);
403
+ d.push(` scr_jb_put_json_str(b, k);`);
404
+ d.push(` scr_jb_putc(b, ':');`);
405
+ d.push(` ${E.jsonWriteHelper(iv)}(b, e);`);
406
+ d.push(` scr_str_release(k);`);
407
+ if (isRefCounted(iv))
408
+ d.push(` ${releaseCallC(iv, "e")};`);
409
+ d.push(` }`);
410
+ d.push(` scr_arr_release(ks);`);
411
+ d.push(` }`);
412
+ }
413
+ }
414
+ d.push(` scr_jb_putc(b, '}');`);
415
+ break;
416
+ }
417
+ case "dyn":
418
+ // Overflow values under an `unknown` index signature: the DOM
419
+ // serializes itself (runtime walker) — object members holding
420
+ // undefined drop, array slots holding undefined print null,
421
+ // exactly Node. Bare JSON.stringify of dyn stays frontend-fenced;
422
+ // this writer is reachable only through overflow entries.
423
+ d.push(` scr_jb_put_dyn(b, v);`);
424
+ break;
425
+ case "array": {
426
+ const elem = t.elem;
427
+ const w = E.jsonWriteHelper(elem);
428
+ d.push(` scr_jb_putc(b, '[');`);
429
+ d.push(` for (size_t i = 0; i < v->len; i++) {`);
430
+ d.push(` if (i > 0) scr_jb_putc(b, ',');`);
431
+ if (elem.kind === "f64") {
432
+ d.push(` ${w}(b, scr_arr_get_f64(v, (double)i));`);
433
+ }
434
+ else if (elem.kind === "bool") {
435
+ d.push(` ${w}(b, scr_arr_get_bool(v, (double)i));`);
436
+ }
437
+ else {
438
+ // _get_ref returns +1; release after writing.
439
+ d.push(` ${cDecl(elem, "e")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)i);`);
440
+ d.push(` ${w}(b, e);`);
441
+ d.push(` ${releaseCallC(elem, "e")};`);
442
+ }
443
+ d.push(` }`);
444
+ d.push(` scr_jb_putc(b, ']');`);
445
+ break;
446
+ }
447
+ case "union": {
448
+ const def = E.unionsById.get(t.unionId);
449
+ if (!def)
450
+ throw new Error(`emitter bug: jsonStringify of unknown union ${t.unionId}`);
451
+ d.push(` switch (v->tag) {`);
452
+ def.arms.forEach((arm, i) => {
453
+ if (arm.kind === "nullT") {
454
+ // Payload-less arm: JSON.stringify(null) is the text `null`,
455
+ // exactly like Node.
456
+ d.push(` case ${i}: scr_jb_puts(b, "null"); break;`);
457
+ return;
458
+ }
459
+ if (arm.kind === "undefinedT") {
460
+ // Reachable only as a record FIELD's serializer (bare
461
+ // undefined-armed unions are fenced from stringify), and the
462
+ // record writer drops the field while it holds this tag before
463
+ // calling — so the tag can never arrive here.
464
+ d.push(` case ${i}: /* undefined arm: the field dropped at the record level */`);
465
+ d.push(` fputs("scriptc: internal error: stringify reached an undefined arm\\n", stderr); abort();`);
466
+ return;
467
+ }
468
+ const w = E.jsonWriteHelper(arm);
469
+ if (arm.kind === "f64") {
470
+ d.push(` case ${i}: ${w}(b, scr_union_get_f64(v)); break;`);
471
+ }
472
+ else if (arm.kind === "bool") {
473
+ d.push(` case ${i}: ${w}(b, scr_union_get_bool(v)); break;`);
474
+ }
475
+ else {
476
+ // Payload is BORROWED out of the box for the write.
477
+ d.push(` case ${i}: ${w}(b, (${cType(arm).trim()})scr_union_peek(v)); break;`);
478
+ }
479
+ });
480
+ d.push(` default: fputs("scriptc: internal error: invalid union tag\\n", stderr); abort();`);
481
+ d.push(` }`);
482
+ break;
483
+ }
484
+ default:
485
+ throw new Error(`emitter bug: jsonStringify of non-JSON type ${t.kind}`);
486
+ }
487
+ d.push(`}`, ``);
488
+ E.walkerDefs.push(...d);
489
+ return name;
490
+ }
491
+ /** The emitted match predicate for one dynCheck target type:
492
+ * `static bool sc_dm_<n>(const ScrDyn *d)` — does this DOM fit T?
493
+ * Never throws, builds nothing; union builders use it to try arms in
494
+ * canonical order (first FULL match wins). Records are width-tolerant
495
+ * here too: extra keys are ignored, only declared fields are examined. */
496
+ export function dynMatchHelper(E, t) {
497
+ const key = typeKey(t);
498
+ const existing = E.dynMatchers.get(key);
499
+ if (existing)
500
+ return existing;
501
+ const name = `sc_dm_${E.dynMatchers.size}`;
502
+ E.dynMatchers.set(key, name);
503
+ const sig = `static bool ${name}(const ScrDyn *d)`;
504
+ E.walkerProtos.push(`${sig}; /* matches ${key} */`);
505
+ const d = [`${sig} { /* matches ${key} */`];
506
+ switch (t.kind) {
507
+ case "f64":
508
+ d.push(` return d->kind == SCR_DYN_NUM;`);
509
+ break;
510
+ case "string":
511
+ d.push(` return d->kind == SCR_DYN_STR;`);
512
+ break;
513
+ case "bool":
514
+ d.push(` return d->kind == SCR_DYN_BOOL;`);
515
+ break;
516
+ case "nullT":
517
+ // JSON null matches exactly the nullT arm (reachable only as a
518
+ // union-arm matcher — bare null is not a valid dynCheck target).
519
+ d.push(` return d->kind == SCR_DYN_NULL;`);
520
+ break;
521
+ case "undefinedT":
522
+ // JSON text never parses to undefined, but the DOM can HOLD it now
523
+ // (overflow reads/conversions under `unknown` index signatures):
524
+ // the undefined DOM value matches exactly the undefined arm.
525
+ d.push(` return d->kind == SCR_DYN_UNDEF;`);
526
+ break;
527
+ case "dyn":
528
+ // An `unknown` target: every DOM value fits, undefined included.
529
+ d.push(` (void)d;`);
530
+ d.push(` return true;`);
531
+ break;
532
+ case "bytes":
533
+ // A Uint8Array target (the DOM carries u8 payloads only).
534
+ if (t.elem !== "u8")
535
+ throw new Error(`emitter bug: dynMatch of bytes<${t.elem}>`);
536
+ d.push(` return d->kind == SCR_DYN_BYTES;`);
537
+ break;
538
+ case "record": {
539
+ const shape = E.recordsById.get(t.shapeId);
540
+ if (!shape)
541
+ throw new Error(`emitter bug: dynCheck of unknown shape ${t.shapeId}`);
542
+ // A tuple matches a JSON ARRAY of EXACTLY its arity whose elements
543
+ // match positionally (arity is part of the type — width tolerance
544
+ // is an object-key concept and does not apply).
545
+ if (shape.tuple) {
546
+ const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
547
+ d.push(` if (d->kind != SCR_DYN_ARR || d->v.arr.len != ${byIndex.length}) return false;`);
548
+ byIndex.forEach((f, i) => {
549
+ d.push(` if (!${E.dynMatchHelper(f.type)}(d->v.arr.items[${i}])) return false;`);
550
+ });
551
+ d.push(` return true;`);
552
+ break;
553
+ }
554
+ d.push(` if (d->kind != SCR_DYN_OBJ) return false;`);
555
+ // dyn ('unknown') fields match ANY value, present or missing (a
556
+ // missing key builds the undefined DOM value) — no test to emit.
557
+ if (shape.fields.some((f) => f.type.kind !== "dyn"))
558
+ d.push(` const ScrDyn *m;`);
559
+ for (const f of shape.fields) {
560
+ if (f.type.kind === "dyn")
561
+ continue;
562
+ const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
563
+ const keyLen = Buffer.byteLength(f.name, "utf8");
564
+ d.push(` m = scr_dyn_obj_get(d, ${keyLit}, ${keyLen});`);
565
+ if (E.undefinedArmTag(f.type) >= 0) {
566
+ // Optional-flavored field: a MISSING key is the undefined arm
567
+ // (a match); a PRESENT key must fit the union as usual.
568
+ d.push(` if (m && !${E.dynMatchHelper(f.type)}(m)) return false;`);
569
+ }
570
+ else {
571
+ d.push(` if (!m || !${E.dynMatchHelper(f.type)}(m)) return false;`);
572
+ }
573
+ }
574
+ // Index-signature shapes: UNDECLARED keys must fit the overflow
575
+ // value type (the builder CAPTURES them — width tolerance became
576
+ // width capture). A dyn value type accepts anything; concrete
577
+ // types check every extra entry.
578
+ if (shape.indexValue && shape.indexValue.kind !== "dyn") {
579
+ const skip = shape.fields
580
+ .map((f) => {
581
+ const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
582
+ const keyLen = Buffer.byteLength(f.name, "utf8");
583
+ return `(e->key_len == ${keyLen} && memcmp(e->key, ${keyLit}, ${keyLen}) == 0)`;
584
+ })
585
+ .join(" || ");
586
+ d.push(` for (size_t i = 0; i < d->v.obj.len; i++) {`);
587
+ d.push(` const ScrDynEntry *e = &d->v.obj.entries[i];`);
588
+ if (shape.fields.length > 0)
589
+ d.push(` if (${skip}) continue;`);
590
+ d.push(` if (!${E.dynMatchHelper(shape.indexValue)}(e->value)) return false;`);
591
+ d.push(` }`);
592
+ }
593
+ d.push(` return true;`);
594
+ break;
595
+ }
596
+ case "array": {
597
+ const m = E.dynMatchHelper(t.elem);
598
+ d.push(` if (d->kind != SCR_DYN_ARR) return false;`);
599
+ d.push(` for (size_t i = 0; i < d->v.arr.len; i++) {`);
600
+ d.push(` if (!${m}(d->v.arr.items[i])) return false;`);
601
+ d.push(` }`);
602
+ d.push(` return true;`);
603
+ break;
604
+ }
605
+ case "union": {
606
+ const def = E.unionsById.get(t.unionId);
607
+ if (!def)
608
+ throw new Error(`emitter bug: dynCheck of unknown union ${t.unionId}`);
609
+ // The undefined arm participates: parsed JSON never contains
610
+ // undefined (a MISSING record key was the arm's only source), but
611
+ // the DOM can hold the undefined value now — overflow entries
612
+ // under `unknown` index signatures — and it matches exactly the
613
+ // undefined arm.
614
+ const arms = def.arms.map((a) => `${E.dynMatchHelper(a)}(d)`);
615
+ d.push(` return ${arms.join(" || ")};`);
616
+ break;
617
+ }
618
+ default:
619
+ throw new Error(`emitter bug: dynMatch of non-JSON type ${t.kind}`);
620
+ }
621
+ d.push(`}`, ``);
622
+ E.walkerDefs.push(...d);
623
+ return name;
624
+ }
625
+ /** The emitted keyed read on a dyn DOM value:
626
+ * `static ScrDyn *sc_dyn_key_get(ScrDyn *d, ScrStr *k, bool opt)` —
627
+ * OBJ answers the member (+1) or the undefined singleton (own-property
628
+ * answer); ARR answers `length` and canonical in-range indices, STR
629
+ * answers `length` (UTF-16-exact via scr_str_utf16_len), both
630
+ * undefined otherwise; NUM/BOOL/BYTES answer undefined. undefined/null
631
+ * receivers THROW the catchable Node-shaped TypeError, or answer the
632
+ * undefined singleton when `opt` is set (a `?.` step). Borrows d and k;
633
+ * the result is +1. Interned once (the memo key can never collide with
634
+ * a typeKey — no IR type spells "%"). */
635
+ /** RequireObjectCoercible with V8's destructuring TypeError (the
636
+ * dynDestrCheck node): nullish throws "Cannot destructure 'SPELL' as it
637
+ * is undefined." (or "…null."), the property form "Cannot destructure
638
+ * property 'PROP' of 'SPELL' …" when firstProp is non-NULL — V8's exact
639
+ * strings, source spelling included. Non-nullish values pass silently. */
640
+ export function dynDestrCheckHelper(E) {
641
+ const memoKey = "%dynDestrCheck";
642
+ const existing = E.dynBuilders.get(memoKey);
643
+ if (existing)
644
+ return existing;
645
+ const name = "sc_dyn_destr_check";
646
+ E.dynBuilders.set(memoKey, name);
647
+ const sig = `static void ${name}(const ScrDyn *d, const char *spell, const char *firstProp)`;
648
+ E.walkerProtos.push(`${sig}; /* destructuring RequireObjectCoercible */`);
649
+ E.walkerDefs.push(`${sig} { /* destructuring RequireObjectCoercible */`, ` if (d->kind != SCR_DYN_UNDEF && d->kind != SCR_DYN_NULL) return;`, ` ScrJsonBuf b;`, ` scr_jb_init(&b);`, ` if (firstProp) {`, ` scr_jb_puts(&b, "Cannot destructure property '");`, ` scr_jb_puts(&b, firstProp);`, ` scr_jb_puts(&b, "' of '");`, ` scr_jb_puts(&b, spell);`, ` scr_jb_puts(&b, "' as it is ");`, ` } else {`, ` scr_jb_puts(&b, "Cannot destructure '");`, ` scr_jb_puts(&b, spell);`, ` scr_jb_puts(&b, "' as it is ");`, ` }`, ` scr_jb_puts(&b, d->kind == SCR_DYN_UNDEF ? "undefined." : "null.");`, ` scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));`, `}`, ``);
650
+ return name;
651
+ }
652
+ /** GetIterator + first-N steps over a DOM value (the dynIterN node), as
653
+ * array destructuring sees it: arrays step by index, strings by CODE
654
+ * POINT (the string iterator — astral chars arrive unsplit), Buffers by
655
+ * byte; everything else throws V8's exact not-iterable TypeError
656
+ * ("undefined is not iterable …", "object null is not iterable …",
657
+ * "number 1 is not iterable …", "boolean true …", "object …",
658
+ * "function …" — each with the "(cannot read property
659
+ * Symbol(Symbol.iterator))" tail). Returns a fresh +1 DOM array of
660
+ * exactly n elements, undefined-padded past the end. */
661
+ export function dynIterNHelper(E) {
662
+ const memoKey = "%dynIterN";
663
+ const existing = E.dynBuilders.get(memoKey);
664
+ if (existing)
665
+ return existing;
666
+ const name = "sc_dyn_iter_n";
667
+ E.dynBuilders.set(memoKey, name);
668
+ const sig = `static ScrDyn *${name}(const ScrDyn *d, size_t n)`;
669
+ E.walkerProtos.push(`${sig}; /* destructuring GetIterator + N steps */`);
670
+ E.walkerDefs.push(`${sig} { /* destructuring GetIterator + N steps */`, ` if (d->kind != SCR_DYN_ARR && d->kind != SCR_DYN_STR && d->kind != SCR_DYN_BYTES) {`, ` ScrJsonBuf b;`, ` scr_jb_init(&b);`, ` switch (d->kind) {`, ` case SCR_DYN_UNDEF: scr_jb_puts(&b, "undefined"); break;`, ` case SCR_DYN_NULL: scr_jb_puts(&b, "object null"); break;`, ` case SCR_DYN_BOOL: scr_jb_puts(&b, d->v.b ? "boolean true" : "boolean false"); break;`, ` case SCR_DYN_NUM: {`, ` scr_jb_puts(&b, "number ");`, ` ScrStr *s = scr_f64_to_scrstr(d->v.num);`, ` for (size_t i = 0; i < s->len; i++) scr_jb_putc(&b, s->data[i]);`, ` scr_str_release(s);`, ` break;`, ` }`, ` case SCR_DYN_FUNC: scr_jb_puts(&b, "function"); break;`, ` default: scr_jb_puts(&b, "object"); break;`, ` }`, ` scr_jb_puts(&b, " is not iterable (cannot read property Symbol(Symbol.iterator))");`, ` scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));`, ` return NULL;`, ` }`, ` ScrDyn *out = scr_dyn_new_arr();`, ` for (size_t i = 0; i < n; i++) {`, ` ScrDyn *item = NULL;`, ` if (d->kind == SCR_DYN_ARR) {`, ` item = i < d->v.arr.len ? scr_dyn_retain(d->v.arr.items[i]) : scr_dyn_retain(scr_dyn_undefined());`, ` } else if (d->kind == SCR_DYN_BYTES) {`, ` item = i < d->v.bytes->len ? scr_dyn_new_num((double)d->v.bytes->data[i]) : scr_dyn_retain(scr_dyn_undefined());`, ` } else {`, ` /* String iteration: whole code POINTS (astral chars arrive`, ` * unsplit — the string iterator, not charAt). */`, ` double len = scr_str_utf16_len(d->v.str);`, ` double at = 0;`, ` for (size_t step = 0; step < i && at < len; step++) {`, ` ScrStr *cp = scr_str_cp_at(d->v.str, at);`, ` at += scr_str_utf16_len(cp);`, ` scr_str_release(cp);`, ` }`, ` if (at < len) {`, ` ScrStr *cp = scr_str_cp_at(d->v.str, at);`, ` item = scr_dyn_new_str(cp);`, ` scr_str_release(cp);`, ` } else {`, ` item = scr_dyn_retain(scr_dyn_undefined());`, ` }`, ` }`, ` scr_dyn_arr_push(out, item); /* push takes ownership */`, ` }`, ` return out;`, `}`, ``);
671
+ return name;
672
+ }
673
+ export function dynKeyGetHelper(E) {
674
+ const memoKey = "%dynKeyGet";
675
+ const existing = E.dynBuilders.get(memoKey);
676
+ if (existing)
677
+ return existing;
678
+ const name = "sc_dyn_key_get";
679
+ E.dynBuilders.set(memoKey, name);
680
+ const sig = `static ScrDyn *${name}(ScrDyn *d, ScrStr *k, bool opt)`;
681
+ E.walkerProtos.push(`${sig}; /* d[k] on dyn */`);
682
+ const d = [`${sig} { /* d[k] on dyn */`];
683
+ d.push(` if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) {`);
684
+ d.push(` if (opt) return scr_dyn_retain(scr_dyn_undefined());`);
685
+ d.push(` const char *base = d->kind == SCR_DYN_UNDEF`);
686
+ d.push(` ? "Cannot read properties of undefined (reading '"`);
687
+ d.push(` : "Cannot read properties of null (reading '";`);
688
+ d.push(` ScrStr *head = scr_str_new(base, strlen(base));`);
689
+ d.push(` ScrStr *withKey = scr_str_concat(head, k);`);
690
+ d.push(` scr_str_release(head);`);
691
+ d.push(` ScrStr *tail = scr_str_new("')", 2);`);
692
+ d.push(` ScrStr *msg = scr_str_concat(withKey, tail);`);
693
+ d.push(` scr_str_release(withKey);`);
694
+ d.push(` scr_str_release(tail);`);
695
+ d.push(` scr_throw_error(SCR_ERR_TYPE, msg); /* takes ownership */`);
696
+ d.push(` return NULL;`);
697
+ d.push(` }`);
698
+ d.push(` if (d->kind == SCR_DYN_OBJ) {`);
699
+ d.push(` ScrDyn *m = scr_dyn_obj_get(d, k->data, k->len);`);
700
+ d.push(` return scr_dyn_retain(m ? m : scr_dyn_undefined());`);
701
+ d.push(` }`);
702
+ d.push(` if (d->kind == SCR_DYN_HANDLE) {`);
703
+ d.push(` /* Native handles: the tag's modeled properties (req.url,`);
704
+ d.push(` * res.statusCode, ...) answer boxed values through the`);
705
+ d.push(` * installed ops; the rest is the undefined singleton or the`);
706
+ d.push(` * loud not-supported ladder (scr_json.c). */`);
707
+ d.push(` return scr_dyn_handle_key_get(d, k);`);
708
+ d.push(` }`);
709
+ d.push(` if (d->kind == SCR_DYN_BYTES) {`);
710
+ d.push(` /* Buffer-shaped dyn (a stream's 'data' chunk in the JS lane):`);
711
+ d.push(` * .length and canonical-index byte reads answer like Node. */`);
712
+ d.push(` if (k->len == 6 && memcmp(k->data, "length", 6) == 0) {`);
713
+ d.push(` return scr_dyn_new_num((double)d->v.bytes->len);`);
714
+ d.push(` }`);
715
+ d.push(` if (k->len > 0 && !(k->len > 1 && k->data[0] == '0')) {`);
716
+ d.push(` size_t idx = 0; bool digits = true;`);
717
+ d.push(` for (size_t i = 0; i < k->len; i++) {`);
718
+ d.push(` if (k->data[i] < '0' || k->data[i] > '9' || idx > (SIZE_MAX - 9) / 10) { digits = false; break; }`);
719
+ d.push(` idx = idx * 10 + (size_t)(k->data[i] - '0');`);
720
+ d.push(` }`);
721
+ d.push(` if (digits && idx < d->v.bytes->len) return scr_dyn_new_num((double)d->v.bytes->data[idx]);`);
722
+ d.push(` }`);
723
+ d.push(` }`);
724
+ d.push(` if (d->kind == SCR_DYN_FUNC) {`);
725
+ d.push(` /* own props (defineProperties writes), then name/length —`);
726
+ d.push(` * the function-instance members test/common copies. */`);
727
+ d.push(` ScrDyn *m = scr_dyn_fn_get(d, k->data, k->len);`);
728
+ d.push(` if (m) return m;`);
729
+ d.push(` }`);
730
+ d.push(` if (d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_STR) {`);
731
+ d.push(` if (k->len == 6 && memcmp(k->data, "length", 6) == 0) {`);
732
+ d.push(` return scr_dyn_new_num(d->kind == SCR_DYN_ARR ? (double)d->v.arr.len : scr_str_utf16_len(d->v.str));`);
733
+ d.push(` }`);
734
+ d.push(` if (k->len > 0 && !(k->len > 1 && k->data[0] == '0')) {`);
735
+ d.push(` /* canonical index: digits only, no leading zero — array`);
736
+ d.push(` * elements, and the string's 1-code-unit char ("abc"[1] is`);
737
+ d.push(` * "b" in JS; out of range reads as an absent key). */`);
738
+ d.push(` size_t idx = 0; bool digits = true;`);
739
+ d.push(` for (size_t i = 0; i < k->len; i++) {`);
740
+ d.push(` if (k->data[i] < '0' || k->data[i] > '9' || idx > (SIZE_MAX - 9) / 10) { digits = false; break; }`);
741
+ d.push(` idx = idx * 10 + (size_t)(k->data[i] - '0');`);
742
+ d.push(` }`);
743
+ d.push(` if (digits && d->kind == SCR_DYN_ARR && idx < d->v.arr.len) return scr_dyn_retain(d->v.arr.items[idx]);`);
744
+ d.push(` if (digits && d->kind == SCR_DYN_STR && (double)idx < scr_str_utf16_len(d->v.str)) {`);
745
+ d.push(` ScrStr *c = scr_str_char_at(d->v.str, (double)idx);`);
746
+ d.push(` ScrDyn *r = scr_dyn_new_str(c);`);
747
+ d.push(` scr_str_release(c);`);
748
+ d.push(` return r;`);
749
+ d.push(` }`);
750
+ d.push(` }`);
751
+ d.push(` }`);
752
+ d.push(` return scr_dyn_retain(scr_dyn_undefined());`);
753
+ d.push(`}`, ``);
754
+ E.walkerDefs.push(...d);
755
+ return name;
756
+ }
757
+ /** The emitted checked builder for one dynCheck target type:
758
+ * `static T sc_dc_<n>(const ScrDyn *d, const ScrDynPath *path)` —
759
+ * validate the DOM against T and BUILD the typed value (+1), or throw a
760
+ * catchable, path-annotated TypeError-shaped string through the exception
761
+ * cell and return a dummy (0/false/NULL) with the pending flag set.
762
+ * Recursive calls propagate a pending exception by releasing the
763
+ * partially-built value and returning — the same pending-check discipline
764
+ * emitted function bodies follow. */
765
+ export function dynCheckHelper(E, t) {
766
+ const key = typeKey(t);
767
+ const existing = E.dynBuilders.get(key);
768
+ if (existing)
769
+ return existing;
770
+ const name = `sc_dc_${E.dynBuilders.size}`;
771
+ E.dynBuilders.set(key, name);
772
+ const sig = `static ${cType(t)}${cType(t).endsWith("*") ? "" : " "}${name}(const ScrDyn *d, const ScrDynPath *path)`;
773
+ E.walkerProtos.push(`${sig}; /* check ${key} */`);
774
+ const want = cStringLiteral(Buffer.from(E.dynDesc(t), "utf8"));
775
+ const d = [`${sig} { /* check ${key} */`];
776
+ switch (t.kind) {
777
+ case "f64":
778
+ d.push(` if (d->kind != SCR_DYN_NUM) { scr_dyn_check_fail(path, ${want}, d); return 0; }`);
779
+ d.push(` return d->v.num;`);
780
+ break;
781
+ case "bool":
782
+ d.push(` if (d->kind != SCR_DYN_BOOL) { scr_dyn_check_fail(path, ${want}, d); return false; }`);
783
+ d.push(` return d->v.b;`);
784
+ break;
785
+ case "string":
786
+ d.push(` if (d->kind != SCR_DYN_STR) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
787
+ d.push(` return scr_str_retain(d->v.str);`);
788
+ break;
789
+ case "dyn":
790
+ // An `unknown` slot (a dyn record field): the DOM subtree passes
791
+ // through as-is — nothing to validate, nothing to build.
792
+ d.push(` (void)path;`);
793
+ d.push(` return scr_dyn_retain((ScrDyn *)d);`);
794
+ break;
795
+ case "bytes":
796
+ // `u as Uint8Array`: kind check, then a fresh COPY out (the
797
+ // boundary's aliasing stance in both directions).
798
+ if (t.elem !== "u8")
799
+ throw new Error(`emitter bug: dynCheck of bytes<${t.elem}>`);
800
+ d.push(` if (d->kind != SCR_DYN_BYTES) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
801
+ d.push(` return scr_dyn_bytes_copy_out(d);`);
802
+ break;
803
+ case "object":
804
+ // The %Error extraction (an instanceof-Error narrow on unknown):
805
+ // validate the DOM's error encoding — the reserved "%error" marker
806
+ // caughtToDyn builds — and extract through the runtime's IDENTITY
807
+ // CACHE (scr_error_from_dyn): a DOM error that came from a runtime
808
+ // ScrError answers that very instance, so out-and-back crossings
809
+ // compare reference-equal (the tracing suite's shape); alien
810
+ // %error objects rebuild once and cache the pair.
811
+ if (t.className !== "%Error") {
812
+ throw new Error(`emitter bug: dynCheck of class ${t.className} (only %Error extracts from the DOM)`);
813
+ }
814
+ d.push(` if (d->kind != SCR_DYN_OBJ || !scr_dyn_obj_get(d, "%error", 6)) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
815
+ d.push(` return scr_error_from_dyn(d);`);
816
+ break;
817
+ case "record": {
818
+ const shape = E.recordsById.get(t.shapeId);
819
+ if (!shape)
820
+ throw new Error(`emitter bug: dynCheck of unknown shape ${t.shapeId}`);
821
+ const rel = (v) => releaseCallC(t, v);
822
+ // Tuple targets: a JSON ARRAY of exactly the arity, validated and
823
+ // extracted positionally with index paths ("$.pairs[3][1]").
824
+ if (shape.tuple) {
825
+ const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
826
+ const arityWant = cStringLiteral(Buffer.from(`array of length ${byIndex.length}`, "utf8"));
827
+ d.push(` if (d->kind != SCR_DYN_ARR) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
828
+ d.push(` if (d->v.arr.len != ${byIndex.length}) { scr_dyn_check_fail(path, ${arityWant}, d); return NULL; }`);
829
+ d.push(` ${cDecl(t, "r")} = ${mangleRecordNew(t.shapeId)}();`);
830
+ byIndex.forEach((f, i) => {
831
+ d.push(` {`);
832
+ d.push(` ScrDynPath p = { path, NULL, ${i} };`);
833
+ d.push(` r->${mangleField(f.name)} = ${E.dynCheckHelper(f.type)}(d->v.arr.items[${i}], &p);`);
834
+ d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`);
835
+ d.push(` }`);
836
+ });
837
+ d.push(` return r;`);
838
+ break;
839
+ }
840
+ d.push(` if (d->kind != SCR_DYN_OBJ) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
841
+ d.push(` ${cDecl(t, "r")} = ${mangleRecordNew(t.shapeId)}();`);
842
+ for (const f of shape.fields) {
843
+ const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
844
+ const keyLen = Buffer.byteLength(f.name, "utf8");
845
+ const fieldWant = cStringLiteral(Buffer.from(E.dynDesc(f.type), "utf8"));
846
+ // Width tolerance: only declared fields are looked up — extra JSON
847
+ // keys are simply never examined (check-and-extract, not shape
848
+ // equality). A missing field fails as "got undefined" — except
849
+ // optional-flavored fields (undefined-armed unions), where the
850
+ // missing key IS the undefined arm: the interned immortal
851
+ // instance, no retain owed (rc == SIZE_MAX).
852
+ const utag = f.type.kind === "union" ? E.undefinedArmTag(f.type) : -1;
853
+ d.push(` {`);
854
+ d.push(` ScrDynPath p = { path, ${keyLit}, 0 };`);
855
+ d.push(` const ScrDyn *m = scr_dyn_obj_get(d, ${keyLit}, ${keyLen});`);
856
+ if (f.type.kind === "dyn") {
857
+ // An `unknown` field: a present key passes through, a missing
858
+ // one IS the undefined DOM value (JS's missing-property read).
859
+ d.push(` (void)p;`);
860
+ d.push(` r->${mangleField(f.name)} = scr_dyn_retain(m ? (ScrDyn *)m : scr_dyn_undefined());`);
861
+ }
862
+ else if (utag >= 0 && f.type.kind === "union") {
863
+ const unit = E.unitInstanceRef(f.type.unionId, utag);
864
+ d.push(` if (!m) {`);
865
+ d.push(` r->${mangleField(f.name)} = ${unit}; /* absent key -> the undefined arm */`);
866
+ d.push(` } else {`);
867
+ d.push(` r->${mangleField(f.name)} = ${E.dynCheckHelper(f.type)}(m, &p);`);
868
+ d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`);
869
+ d.push(` }`);
870
+ }
871
+ else {
872
+ d.push(` if (!m) { scr_dyn_check_fail(&p, ${fieldWant}, NULL); ${rel("r")}; return NULL; }`);
873
+ d.push(` r->${mangleField(f.name)} = ${E.dynCheckHelper(f.type)}(m, &p);`);
874
+ d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`);
875
+ }
876
+ d.push(` }`);
877
+ }
878
+ // Index-signature shapes CAPTURE undeclared keys into the overflow
879
+ // map (width tolerance became width capture — plain shapes above
880
+ // keep ignoring extras). dyn value types retain the DOM subtree
881
+ // as-is; concrete types validate each entry with its key path.
882
+ if (shape.indexValue) {
883
+ const iv = shape.indexValue;
884
+ const skip = shape.fields
885
+ .map((f) => {
886
+ const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
887
+ const keyLen = Buffer.byteLength(f.name, "utf8");
888
+ return `(e->key_len == ${keyLen} && memcmp(e->key, ${keyLit}, ${keyLen}) == 0)`;
889
+ })
890
+ .join(" || ");
891
+ d.push(` for (size_t i = 0; i < d->v.obj.len; i++) {`);
892
+ d.push(` const ScrDynEntry *e = &d->v.obj.entries[i];`);
893
+ if (shape.fields.length > 0)
894
+ d.push(` if (${skip}) continue;`);
895
+ if (iv.kind === "dyn") {
896
+ d.push(` ${cDecl(iv, "ev")} = scr_dyn_retain(e->value);`);
897
+ }
898
+ else {
899
+ d.push(` ScrDynPath p = { path, e->key, 0 };`);
900
+ d.push(` ${cDecl(iv, "ev")} = ${E.dynCheckHelper(iv)}(e->value, &p);`);
901
+ d.push(` if (scr_exc_pending()) { ${rel("r")}; return NULL; }`);
902
+ }
903
+ d.push(` ScrStr *ek = scr_str_new(e->key, e->key_len);`);
904
+ if (iv.kind === "f64" || iv.kind === "bool") {
905
+ d.push(` scr_map_set_str_${iv.kind === "f64" ? "f64" : "bool"}(r->${OVERFLOW_MEMBER}, ek, ev);`);
906
+ }
907
+ else {
908
+ d.push(` scr_map_set_str_ref(r->${OVERFLOW_MEMBER}, ek, ev);`);
909
+ }
910
+ d.push(` scr_str_release(ek);`);
911
+ d.push(` }`);
912
+ }
913
+ d.push(` return r;`);
914
+ break;
915
+ }
916
+ case "array": {
917
+ const elem = t.elem;
918
+ const c = E.dynCheckHelper(elem);
919
+ d.push(` if (d->kind != SCR_DYN_ARR) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
920
+ d.push(` ScrArr *a = ${E.arrNewC(elem, "d->v.arr.len")};`);
921
+ d.push(` for (size_t i = 0; i < d->v.arr.len; i++) {`);
922
+ d.push(` ScrDynPath p = { path, NULL, i };`);
923
+ d.push(` ${cDecl(elem, "e")} = ${c}(d->v.arr.items[i], &p);`);
924
+ d.push(` if (scr_exc_pending()) { scr_arr_release(a); return NULL; }`);
925
+ d.push(` scr_arr_push_${elemAccess(elem)}(a, e);`);
926
+ d.push(` }`);
927
+ d.push(` return a;`);
928
+ break;
929
+ }
930
+ case "union": {
931
+ const def = E.unionsById.get(t.unionId);
932
+ if (!def)
933
+ throw new Error(`emitter bug: dynCheck of unknown union ${t.unionId}`);
934
+ // Arms in CANONICAL order, first FULL match wins (discriminated
935
+ // unions disambiguate naturally: the arm whose declared fields all
936
+ // fit). The matched arm's builder can no longer fail.
937
+ def.arms.forEach((arm, i) => {
938
+ const m = E.dynMatchHelper(arm);
939
+ if (arm.kind === "undefinedT") {
940
+ // Parsed JSON never matches here (no undefined in JSON text —
941
+ // a MISSING record key builds this arm in the record builder
942
+ // above), but the undefined DOM value can arrive from
943
+ // `unknown` index-signature overflows and builds the interned
944
+ // unit instance exactly like null.
945
+ d.push(` if (${m}(d)) {`);
946
+ d.push(` return ${E.unitInstanceRef(t.unionId, i)};`);
947
+ d.push(` }`);
948
+ return;
949
+ }
950
+ if (arm.kind === "nullT") {
951
+ // A matched unit arm builds nothing: the result is THE interned
952
+ // immortal instance (rc == SIZE_MAX — RC entry points and the
953
+ // collector both skip it, so no retain is owed).
954
+ d.push(` if (${m}(d)) {`);
955
+ d.push(` return ${E.unitInstanceRef(t.unionId, i)};`);
956
+ d.push(` }`);
957
+ return;
958
+ }
959
+ const c = E.dynCheckHelper(arm);
960
+ d.push(` if (${m}(d)) {`);
961
+ if (arm.kind === "f64") {
962
+ d.push(` return scr_union_new_f64(${i}, ${c}(d, path));`);
963
+ }
964
+ else if (arm.kind === "bool") {
965
+ d.push(` return scr_union_new_bool(${i}, ${c}(d, path));`);
966
+ }
967
+ else {
968
+ const rc = vAdapters(arm);
969
+ d.push(` return scr_union_new_ref(${i}, ${c}(d, path), &${rc.retain}, &${rc.release}, ${E.traceArgC(arm)});`);
970
+ }
971
+ d.push(` }`);
972
+ });
973
+ d.push(` scr_dyn_check_fail(path, ${want}, d);`);
974
+ d.push(` return NULL;`);
975
+ break;
976
+ }
977
+ case "func": {
978
+ // The checked-dynamic function boundary, OUT direction: a
979
+ // non-function kind fails like any dynCheck; an IDENTICAL boxed
980
+ // signature (the interned typeKey — same key ⇔ same IR type ⇔
981
+ // same ABI) unwraps the closure directly (identity preserved:
982
+ // `mustCall(fn)` handed back to a slot of fn's own type IS fn's
983
+ // wrapper, retained); anything else wraps in the per-target
984
+ // adapter closure, whose caps[0] obj-box owns the dyn value
985
+ // (untraced — cycles through dyn never collect, SEMANTICS.md).
986
+ const adapter = dynFuncAdapterHelper(E, t);
987
+ const sigLit = cStringLiteral(Buffer.from(key, "utf8"));
988
+ d.push(` if (d->kind != SCR_DYN_FUNC) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
989
+ d.push(` if (strcmp(d->v.fn.sig, ${sigLit}) == 0) return scr_closure_retain(d->v.fn.clo);`);
990
+ d.push(` {`);
991
+ d.push(` ScrClosure *a = scr_closure_new((void *)&${adapter}, 1);`);
992
+ d.push(` a->caps[0] = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);`);
993
+ d.push(` scr_box_set_ref(a->caps[0], scr_dyn_retain((ScrDyn *)d));`);
994
+ d.push(` return a;`);
995
+ d.push(` }`);
996
+ break;
997
+ }
998
+ default: {
999
+ // Runtime HANDLE targets: a tag-checked reference unwrap (+1 —
1000
+ // identity, no copy; the runtime throws the path-annotated
1001
+ // TypeError on any other kind or tag).
1002
+ const h = DYN_HANDLE_KINDS.get(t.kind);
1003
+ if (h) {
1004
+ d.push(` return (${cType(t).trim()})scr_dyn_handle_unbox(d, ${h.tag}, path, ${want});`);
1005
+ break;
1006
+ }
1007
+ throw new Error(`emitter bug: dynCheck of non-JSON type ${t.kind}`);
1008
+ }
1009
+ }
1010
+ d.push(`}`, ``);
1011
+ E.walkerDefs.push(...d);
1012
+ return name;
1013
+ }
1014
+ /** The emitted static→DOM converter for one type:
1015
+ * `static ScrDyn *sc_td_<n>(<T> v)` — build a fresh dyn DOM value from a
1016
+ * static one (+1), DEEP-COPYING composites (a dyn value never aliases
1017
+ * static storage — the jsMarshal stance). Domain: the JSON-safe kinds
1018
+ * plus undefined-armed unions (the undefined arm becomes the undefined
1019
+ * DOM singleton) and index-signature records (whose overflow entries
1020
+ * copy over). The operand is borrowed. Never throws. */
1021
+ export function toDynHelper(E, t) {
1022
+ const key = typeKey(t);
1023
+ const existing = E.toDynFns.get(key);
1024
+ if (existing)
1025
+ return existing;
1026
+ const name = `sc_td_${E.toDynFns.size}`;
1027
+ E.toDynFns.set(key, name);
1028
+ const sig = `static ScrDyn *${name}(${cDecl(t, "v")})`;
1029
+ E.walkerProtos.push(`${sig}; /* to-dyn ${key} */`);
1030
+ const d = [`${sig} { /* to-dyn ${key} */`];
1031
+ switch (t.kind) {
1032
+ case "f64":
1033
+ d.push(` return scr_dyn_new_num(v);`);
1034
+ break;
1035
+ case "bool":
1036
+ d.push(` return scr_dyn_new_bool(v);`);
1037
+ break;
1038
+ case "string":
1039
+ d.push(` return scr_dyn_new_str(v); /* retains v */`);
1040
+ break;
1041
+ case "undefinedT":
1042
+ // A bare unit field (`x: undefined` in a record — the tls
1043
+ // options-record's explicit-absent member): the DOM unit value.
1044
+ d.push(` (void)v; return scr_dyn_retain(scr_dyn_undefined());`);
1045
+ break;
1046
+ case "nullT":
1047
+ d.push(` (void)v; return scr_dyn_new_null();`);
1048
+ break;
1049
+ case "object":
1050
+ // %Error only (canConvertToDyn's gate): the DOM's error encoding.
1051
+ if (t.className !== "%Error") {
1052
+ throw new Error(`emitter bug: to-dyn of class ${t.className}`);
1053
+ }
1054
+ d.push(` return scr_dyn_from_error(v);`);
1055
+ break;
1056
+ case "dyn":
1057
+ // A dyn member of a converting composite (a dyn record field): the
1058
+ // DOM value passes through by reference — already a DOM, already
1059
+ // immutable-through-copies.
1060
+ d.push(` return scr_dyn_retain(v);`);
1061
+ break;
1062
+ case "bytes":
1063
+ // bytes<u8> → the DOM's bytes kind, payload COPIED (the boundary
1064
+ // stance; stdin chunks into unknown-typed helpers).
1065
+ if (t.elem !== "u8")
1066
+ throw new Error(`emitter bug: to-dyn of bytes<${t.elem}>`);
1067
+ d.push(` return scr_dyn_new_bytes_copy(v);`);
1068
+ break;
1069
+ case "record": {
1070
+ const shape = E.recordsById.get(t.shapeId);
1071
+ if (!shape)
1072
+ throw new Error(`emitter bug: to-dyn of unknown shape ${t.shapeId}`);
1073
+ if (shape.tuple) {
1074
+ // A tuple converts as the JSON ARRAY it is everywhere else.
1075
+ const byIndex = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
1076
+ d.push(` ScrDyn *d = scr_dyn_new_arr();`);
1077
+ for (const f of byIndex) {
1078
+ d.push(` scr_dyn_arr_push(d, ${E.toDynHelper(f.type)}(v->${mangleField(f.name)}));`);
1079
+ }
1080
+ d.push(` return d;`);
1081
+ break;
1082
+ }
1083
+ d.push(` ScrDyn *d = scr_dyn_new_obj();`);
1084
+ // Keys insert in DECLARED order — the DOM object's insertion order
1085
+ // is observable (Object.keys/for-in over checked-dynamic values,
1086
+ // dyn JSON), so it must be JS's (SEMANTICS.md 36's stance, same as
1087
+ // the JSON writer above). Internal '%'-fields declaredOrder omits
1088
+ // still enter the DOM (after the visible keys) so a
1089
+ // record→dyn→record round trip keeps their data.
1090
+ {
1091
+ const byName = new Map(shape.fields.map((f) => [f.name, f]));
1092
+ const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
1093
+ const inOrder = new Set(order);
1094
+ const dynFields = [
1095
+ ...order.map((n) => byName.get(n)).filter((f) => f !== undefined),
1096
+ ...shape.fields.filter((f) => !inOrder.has(f.name)),
1097
+ ];
1098
+ for (const f of dynFields) {
1099
+ const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
1100
+ const keyLen = Buffer.byteLength(f.name, "utf8");
1101
+ d.push(` scr_dyn_obj_set(d, ${keyLit}, ${keyLen}, ${E.toDynHelper(f.type)}(v->${mangleField(f.name)}));`);
1102
+ }
1103
+ }
1104
+ if (shape.indexValue) {
1105
+ const iv = shape.indexValue;
1106
+ const m = `v->${OVERFLOW_MEMBER}`;
1107
+ // JS OWN-KEY order (integer-like keys ascending first) — the same
1108
+ // enumeration Object.keys and the JSON writer answer.
1109
+ d.push(` {`);
1110
+ d.push(` ScrArr *ks = scr_map_keys_js_order(${m});`);
1111
+ d.push(` for (size_t i = 0; i < ks->len; i++) {`);
1112
+ d.push(` ScrStr *k = (ScrStr *)scr_arr_get_ref(ks, (double)i);`);
1113
+ if (iv.kind === "f64" || iv.kind === "bool") {
1114
+ d.push(` ${cDecl(iv, "sv")} = 0;`);
1115
+ d.push(` scr_map_get_str_${iv.kind === "f64" ? "f64" : "bool"}(${m}, k, &sv);`);
1116
+ d.push(` scr_dyn_obj_set(d, k->data, k->len, scr_dyn_new_${iv.kind === "f64" ? "num" : "bool"}(sv));`);
1117
+ }
1118
+ else if (iv.kind === "dyn") {
1119
+ // get_str_ref returns +1 — exactly the ownership obj_set takes.
1120
+ d.push(` scr_dyn_obj_set(d, k->data, k->len, (ScrDyn *)scr_map_get_str_ref(${m}, k));`);
1121
+ }
1122
+ else {
1123
+ d.push(` ${cDecl(iv, "e")} = (${cType(iv).trim()})scr_map_get_str_ref(${m}, k);`);
1124
+ d.push(` scr_dyn_obj_set(d, k->data, k->len, ${E.toDynHelper(iv)}(e));`);
1125
+ d.push(` ${releaseCallC(iv, "e")};`);
1126
+ }
1127
+ d.push(` scr_str_release(k);`);
1128
+ d.push(` }`);
1129
+ d.push(` scr_arr_release(ks);`);
1130
+ d.push(` }`);
1131
+ }
1132
+ d.push(` return d;`);
1133
+ break;
1134
+ }
1135
+ case "array": {
1136
+ const elem = t.elem;
1137
+ d.push(` ScrDyn *d = scr_dyn_new_arr();`);
1138
+ d.push(` for (size_t i = 0; i < v->len; i++) {`);
1139
+ if (elem.kind === "f64") {
1140
+ d.push(` scr_dyn_arr_push(d, scr_dyn_new_num(scr_arr_get_f64(v, (double)i)));`);
1141
+ }
1142
+ else if (elem.kind === "bool") {
1143
+ d.push(` scr_dyn_arr_push(d, scr_dyn_new_bool(scr_arr_get_bool(v, (double)i)));`);
1144
+ }
1145
+ else {
1146
+ d.push(` ${cDecl(elem, "e")} = (${cType(elem).trim()})scr_arr_get_ref(v, (double)i);`);
1147
+ d.push(` scr_dyn_arr_push(d, ${E.toDynHelper(elem)}(e));`);
1148
+ d.push(` ${releaseCallC(elem, "e")};`);
1149
+ }
1150
+ d.push(` }`);
1151
+ d.push(` return d;`);
1152
+ break;
1153
+ }
1154
+ case "union": {
1155
+ const def = E.unionsById.get(t.unionId);
1156
+ if (!def)
1157
+ throw new Error(`emitter bug: to-dyn of unknown union ${t.unionId}`);
1158
+ d.push(` switch (v->tag) {`);
1159
+ def.arms.forEach((arm, i) => {
1160
+ if (arm.kind === "undefinedT") {
1161
+ d.push(` case ${i}: return scr_dyn_retain(scr_dyn_undefined());`);
1162
+ }
1163
+ else if (arm.kind === "nullT") {
1164
+ d.push(` case ${i}: return scr_dyn_new_null();`);
1165
+ }
1166
+ else if (arm.kind === "f64") {
1167
+ d.push(` case ${i}: return scr_dyn_new_num(scr_union_get_f64(v));`);
1168
+ }
1169
+ else if (arm.kind === "bool") {
1170
+ d.push(` case ${i}: return scr_dyn_new_bool(scr_union_get_bool(v));`);
1171
+ }
1172
+ else {
1173
+ d.push(` case ${i}: return ${E.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(v));`);
1174
+ }
1175
+ });
1176
+ d.push(` default: fputs("scriptc: internal error: invalid union tag\\n", stderr); abort();`);
1177
+ d.push(` }`);
1178
+ break;
1179
+ }
1180
+ case "promise": {
1181
+ // Promises box by REFERENCE (SCR_DYN_PROMISE — identity is the
1182
+ // promise): promise<dyn> carries its ScrPromise directly (the
1183
+ // payload is already a DOM value); any other inner boxes an
1184
+ // ADAPTER promise whose settle callback converts the payload
1185
+ // (rejections copy raw inside the runtime's cb-waiter machinery).
1186
+ if (t.inner.kind === "dyn") {
1187
+ d.push(` return scr_dyn_new_promise(v);`);
1188
+ break;
1189
+ }
1190
+ d.push(` return scr_dyn_new_promise_adapting(v, &${promiseDynAdapterHelper(E, t.inner)});`);
1191
+ break;
1192
+ }
1193
+ default: {
1194
+ // Runtime HANDLE kinds box by REFERENCE (identity — no copy):
1195
+ // scr_dyn_new_handle retains the borrowed operand through the
1196
+ // tag's installed ops.
1197
+ const h = DYN_HANDLE_KINDS.get(t.kind);
1198
+ if (h) {
1199
+ d.push(` return scr_dyn_new_handle(v, ${h.tag});`);
1200
+ break;
1201
+ }
1202
+ throw new Error(`emitter bug: to-dyn of unsupported type ${t.kind}`);
1203
+ }
1204
+ }
1205
+ d.push(`}`, ``);
1206
+ E.walkerDefs.push(...d);
1207
+ return name;
1208
+ }
1209
+ /** The DOM-promise settle adapter for one fulfillment payload type:
1210
+ * `static void sc_pda_<n>(ScrPromise *dst, ScrPromise *src)` — read
1211
+ * src's fulfilled payload by its compile-time kind, convert it to a DOM
1212
+ * value, and fulfill dst with it (scr_dyn_new_promise_adapting's
1213
+ * callback; rejections never reach an adapter — the runtime copies them
1214
+ * raw). Interned per inner typeKey. */
1215
+ export function promiseDynAdapterHelper(E, inner) {
1216
+ const key = typeKey(inner);
1217
+ const existing = E.promiseDynAdapters.get(key);
1218
+ if (existing)
1219
+ return existing;
1220
+ const name = `sc_pda_${E.promiseDynAdapters.size}`;
1221
+ E.promiseDynAdapters.set(key, name);
1222
+ const sig = `static void ${name}(ScrPromise *dst, ScrPromise *src)`;
1223
+ E.walkerProtos.push(`${sig}; /* dyn-box settle adapter for promise<${key}> */`);
1224
+ const d = [`${sig} { /* dyn-box settle adapter for promise<${key}> */`];
1225
+ const fulfill = (expr) => ` scr_promise_fulfill_ref(dst, ${expr}, scr_dyn_retain_v, scr_dyn_release_v, NULL);`;
1226
+ switch (inner.kind) {
1227
+ case "void":
1228
+ case "undefinedT":
1229
+ d.push(` (void)src;`);
1230
+ d.push(fulfill(`scr_dyn_retain(scr_dyn_undefined())`));
1231
+ break;
1232
+ case "nullT":
1233
+ d.push(` (void)src;`);
1234
+ d.push(fulfill(`scr_dyn_new_null()`));
1235
+ break;
1236
+ case "f64":
1237
+ d.push(fulfill(`scr_dyn_new_num(scr_promise_payload_f64(src))`));
1238
+ break;
1239
+ case "bool":
1240
+ d.push(fulfill(`scr_dyn_new_bool(scr_promise_payload_bool(src))`));
1241
+ break;
1242
+ case "string":
1243
+ d.push(` ScrStr *s = scr_promise_payload_str(src);`);
1244
+ d.push(` ScrDyn *dv = scr_dyn_new_str(s); /* retains s */`);
1245
+ d.push(` scr_str_release(s);`);
1246
+ d.push(fulfill(`dv`));
1247
+ break;
1248
+ default: {
1249
+ // Ref-payload inners (records, arrays, bytes, %Error, unions,
1250
+ // nested promises, handles): extract (+1 via the stored retain),
1251
+ // convert through the shared to-dyn spelling, release the extract.
1252
+ d.push(` ${cDecl(inner, "pv")} = (${cType(inner).trim()})scr_promise_payload_ref(src);`);
1253
+ d.push(` ScrDyn *dv = ${toDynExprC(E, inner, "pv")};`);
1254
+ if (isRefCounted(inner))
1255
+ d.push(` ${releaseCallC(inner, "pv")};`);
1256
+ d.push(fulfill(`dv`));
1257
+ break;
1258
+ }
1259
+ }
1260
+ d.push(`}`, ``);
1261
+ E.walkerDefs.push(...d);
1262
+ return name;
1263
+ }
1264
+ /* ── the checked-dynamic function boundary (nodes.ts) ─────────────────
1265
+ * Three interned per-signature helpers:
1266
+ *
1267
+ * sc_dfk_<n> — the CALL THUNK a dyn-boxed closure carries: validate
1268
+ * each dyn argument into the declared param type (JS arity: extras
1269
+ * ignored, missing args are the undefined DOM value — a param type
1270
+ * the undefined value fails checks throws the path-annotated
1271
+ * TypeError), call through the closure, convert the result back to a
1272
+ * DOM value (+1).
1273
+ *
1274
+ * sc_dfb_<n> — the BOX builder dynFrom emits: a fresh SCR_DYN_FUNC
1275
+ * node carrying the retained closure, the thunk, the arity, the
1276
+ * interned signature key (typeKey — dynCheck's exact-unwrap fast
1277
+ * path), and the best-effort name.
1278
+ *
1279
+ * sc_dfa_<n> — the ADAPTER a func-targeted dynCheck wraps a
1280
+ * NON-identical boxed signature in: a closure of the TARGET type
1281
+ * whose body converts each typed argument INTO dyn, calls the boxed
1282
+ * thunk, and validates the dyn result into the target's return type.
1283
+ */
1284
+ /** The dyn argument spelling of one static value (thunk results, adapter
1285
+ * arguments): dyn passes through (+1 via retain — callers own the
1286
+ * result uniformly), funcs box (anonymous — the static name is gone by
1287
+ * the time a value flows through an adapter; SEMANTICS.md), everything
1288
+ * else rides the toDyn converter. `expr` is BORROWED in every arm. */
1289
+ function toDynExprC(E, t, expr) {
1290
+ if (t.kind === "dyn")
1291
+ return `scr_dyn_retain(${expr})`;
1292
+ if (t.kind === "func")
1293
+ return `${E.dynFuncBoxHelper(t)}(${expr}, NULL)`;
1294
+ return `${E.toDynHelper(t)}(${expr})`;
1295
+ }
1296
+ /** The call thunk for one closure signature (see the block comment). */
1297
+ export function dynFuncThunkHelper(E, t) {
1298
+ const key = typeKey(t);
1299
+ const existing = E.dynFuncThunks.get(key);
1300
+ if (existing)
1301
+ return existing;
1302
+ const name = `sc_dfk_${E.dynFuncThunks.size}`;
1303
+ E.dynFuncThunks.set(key, name);
1304
+ const sig = `static ScrDyn *${name}(ScrClosure *c, ScrDyn *const *args, size_t argc)`;
1305
+ E.walkerProtos.push(`${sig}; /* dyn call thunk for ${key} */`);
1306
+ const d = [`${sig} { /* dyn call thunk for ${key} */`];
1307
+ if (t.params.length === 0)
1308
+ d.push(` (void)args;`);
1309
+ d.push(` (void)argc;`);
1310
+ t.params.forEach((p, i) => {
1311
+ // JS arity: a missing argument IS the undefined DOM value; the
1312
+ // param's own check decides whether that flies (dyn params take
1313
+ // anything; a number param throws the catchable TypeError).
1314
+ d.push(` ${cDecl(p, `a${i}`)};`);
1315
+ d.push(` {`);
1316
+ d.push(` const ScrDyn *ad = ${i} < argc ? args[${i}] : scr_dyn_undefined();`);
1317
+ if (p.kind === "dyn") {
1318
+ d.push(` a${i} = scr_dyn_retain((ScrDyn *)ad);`);
1319
+ }
1320
+ else {
1321
+ d.push(` ScrDynPath pp = { NULL, NULL, ${i} };`);
1322
+ d.push(` a${i} = ${E.dynCheckHelper(p)}(ad, &pp);`);
1323
+ const undo = t.params
1324
+ .slice(0, i)
1325
+ .flatMap((q, j) => (isRefCounted(q) ? [`${releaseCallC(q, `a${j}`)};`] : []));
1326
+ d.push(` if (scr_exc_pending()) { ${undo.join(" ")}${undo.length > 0 ? " " : ""}return NULL; }`);
1327
+ }
1328
+ d.push(` }`);
1329
+ });
1330
+ // VARIADIC (rest-marked) signatures: one extra trailing DOM-array
1331
+ // param carries the call's arguments from index params.length on —
1332
+ // the mustCall wrapper's `arguments`, a JS `...args`. Built fresh per
1333
+ // call (+1, moved into the callee like every param).
1334
+ if (t.rest) {
1335
+ d.push(` ScrDyn *rest = scr_dyn_new_arr();`);
1336
+ d.push(` for (size_t ri = ${t.params.length}; ri < argc; ri++) {`);
1337
+ d.push(` scr_dyn_arr_push(rest, scr_dyn_retain((ScrDyn *)args[ri]));`);
1338
+ d.push(` }`);
1339
+ }
1340
+ // The closure CONSUMES its params (+1 each moved in — exactly what the
1341
+ // builders above returned).
1342
+ const castParams = ["ScrClosure *", ...t.params.map((p) => cType(p).trim()), ...(t.rest ? ["ScrDyn *"] : [])].join(", ");
1343
+ const call = `((${cType(t.ret).trim()} (*)(${castParams}))c->fn)(${["c", ...t.params.map((_, i) => `a${i}`), ...(t.rest ? ["rest"] : [])].join(", ")})`;
1344
+ if (t.ret.kind === "void") {
1345
+ d.push(` ${call};`);
1346
+ d.push(` if (scr_exc_pending()) return NULL;`);
1347
+ d.push(` return scr_dyn_retain(scr_dyn_undefined());`);
1348
+ }
1349
+ else if (t.ret.kind === "dyn") {
1350
+ d.push(` ScrDyn *r = ${call};`);
1351
+ d.push(` if (scr_exc_pending()) return NULL;`);
1352
+ d.push(` return r;`);
1353
+ }
1354
+ else {
1355
+ d.push(` ${cDecl(t.ret, "r")} = ${call};`);
1356
+ d.push(` if (scr_exc_pending()) return NULL;`);
1357
+ d.push(` ScrDyn *out = ${toDynExprC(E, t.ret, "r")};`);
1358
+ if (isRefCounted(t.ret))
1359
+ d.push(` ${releaseCallC(t.ret, "r")};`);
1360
+ d.push(` return out;`);
1361
+ }
1362
+ d.push(`}`, ``);
1363
+ E.walkerDefs.push(...d);
1364
+ return name;
1365
+ }
1366
+ /** The box builder dynFrom emits for one closure signature. */
1367
+ export function dynFuncBoxHelper(E, t) {
1368
+ const key = typeKey(t);
1369
+ const existing = E.dynFuncBoxes.get(key);
1370
+ if (existing)
1371
+ return existing;
1372
+ const name = `sc_dfb_${E.dynFuncBoxes.size}`;
1373
+ E.dynFuncBoxes.set(key, name);
1374
+ const sig = `static ScrDyn *${name}(ScrClosure *v, const char *fname)`;
1375
+ E.walkerProtos.push(`${sig}; /* box ${key} into dyn */`);
1376
+ const thunk = dynFuncThunkHelper(E, t);
1377
+ const sigLit = cStringLiteral(Buffer.from(key, "utf8"));
1378
+ E.walkerDefs.push(`${sig} { /* box ${key} into dyn */`, ` return scr_dyn_new_func(scr_closure_retain(v), &${thunk}, ${t.params.length}, ${sigLit}, fname);`, `}`, ``);
1379
+ return name;
1380
+ }
1381
+ /** The adapter closure body for one TARGET signature: the emitted C
1382
+ * function a func-targeted dynCheck wraps a non-identical boxed
1383
+ * signature in. caps[0] is an untraced obj-box owning the dyn function
1384
+ * value. */
1385
+ export function dynFuncAdapterHelper(E, t) {
1386
+ const key = typeKey(t);
1387
+ const existing = E.dynFuncAdapters.get(key);
1388
+ if (existing)
1389
+ return existing;
1390
+ const name = `sc_dfa_${E.dynFuncAdapters.size}`;
1391
+ E.dynFuncAdapters.set(key, name);
1392
+ const params = ["ScrClosure *sc_env", ...t.params.map((p, i) => cDecl(p, `a${i}`))].join(", ");
1393
+ const sig = `static ${cType(t.ret)}${cType(t.ret).endsWith("*") ? "" : " "}${name}(${params})`;
1394
+ E.walkerProtos.push(`${sig}; /* dyn fn adapter to ${key} */`);
1395
+ const dummy = t.ret.kind === "void" ? "" : t.ret.kind === "f64" ? "0" : t.ret.kind === "bool" ? "false" : "NULL";
1396
+ const d = [`${sig} { /* dyn fn adapter to ${key} */`];
1397
+ d.push(` ScrDyn *sc_fn = (ScrDyn *)scr_box_get_ref(sc_env->caps[0]); /* +1 */`);
1398
+ // The adapter OWNS its params (closure ABI); each converts to a dyn
1399
+ // argument (toDynExprC borrows) and releases.
1400
+ if (t.params.length > 0) {
1401
+ d.push(` ScrDyn *sc_args[${t.params.length}];`);
1402
+ t.params.forEach((p, i) => {
1403
+ d.push(` sc_args[${i}] = ${toDynExprC(E, p, `a${i}`)};`);
1404
+ if (isRefCounted(p))
1405
+ d.push(` ${releaseCallC(p, `a${i}`)};`);
1406
+ });
1407
+ }
1408
+ // The kind is FUNC by construction (the dynCheck that built this
1409
+ // adapter tested it), so `what` is unreachable — spelled anyway.
1410
+ d.push(` ScrDyn *sc_r = scr_dyn_call(sc_fn, ${t.params.length > 0 ? "sc_args" : "NULL"}, ${t.params.length}, "value");`);
1411
+ d.push(` scr_dyn_release(sc_fn);`);
1412
+ t.params.forEach((_, i) => d.push(` scr_dyn_release(sc_args[${i}]);`));
1413
+ d.push(` if (scr_exc_pending()) return ${dummy};`.replace("return ;", "return;"));
1414
+ if (t.ret.kind === "void") {
1415
+ d.push(` scr_dyn_release(sc_r);`);
1416
+ d.push(` return;`);
1417
+ }
1418
+ else if (t.ret.kind === "dyn") {
1419
+ d.push(` return sc_r;`);
1420
+ }
1421
+ else {
1422
+ // Validate the dyn result into the target's return type — a lying
1423
+ // wrapper throws the catchable TypeError here (path "$"), exactly
1424
+ // the dynCheck stance; the dummy result rides the pending unwind.
1425
+ d.push(` ${cDecl(t.ret, "out")} = ${E.dynCheckHelper(t.ret)}(sc_r, NULL);`);
1426
+ d.push(` scr_dyn_release(sc_r);`);
1427
+ d.push(` return out;`);
1428
+ }
1429
+ d.push(`}`, ``);
1430
+ E.walkerDefs.push(...d);
1431
+ return name;
1432
+ }
1433
+ /** The emitted dynamic-keyed READ helper for one (shape, result type):
1434
+ * `static <T> sc_rkg_<n>(<struct> *r, ScrStr *k)` — declared fields
1435
+ * answer first (string comparisons in canonical order; the frontend
1436
+ * proved each surfaces as T: identity, a to-dyn conversion, or an
1437
+ * arm-into-union wrap), then the overflow map on index-signature shapes.
1438
+ * A MISSING key yields the undefined DOM value (T dyn), the undefined
1439
+ * arm (T undefined-armed union), or TRAPS with the key text (the array
1440
+ * OOB policy — the checker claimed T and no undefined is representable).
1441
+ * Borrows both; refcounted results are owned (+1). */
1442
+ export function recordKeyGetHelper(E, shapeId, t, overflowOnly = false) {
1443
+ const key = `${shapeId}|${typeKey(t)}${overflowOnly ? "|ovf" : ""}`;
1444
+ const existing = E.recordKeyGetFns.get(key);
1445
+ if (existing)
1446
+ return existing;
1447
+ const shape = E.recordsById.get(shapeId);
1448
+ if (!shape)
1449
+ throw new Error(`emitter bug: keyed read of unknown shape ${shapeId}`);
1450
+ const name = `sc_rkg_${E.recordKeyGetFns.size}`;
1451
+ E.recordKeyGetFns.set(key, name);
1452
+ const struct = mangleRecordStruct(shapeId);
1453
+ const sig = `static ${cType(t)}${cType(t).endsWith("*") ? "" : " "}${name}(${struct} *r, ScrStr *k)`;
1454
+ E.walkerProtos.push(`${sig}; /* r[k] on ${shapeId} as ${typeKey(t)} */`);
1455
+ const d = [`${sig} { /* r[k] on ${shapeId} as ${typeKey(t)} */`];
1456
+ // How a value of type `vt` (a field, or the overflow hit) surfaces as T.
1457
+ const surface = (vt, expr, owned) => {
1458
+ if (typeEquals(vt, t)) {
1459
+ return owned || !isRefCounted(vt) ? expr : retainCallC(vt, expr);
1460
+ }
1461
+ if (t.kind === "dyn") {
1462
+ // toDyn borrows — an owned operand would leak; callers pass borrowed.
1463
+ return `${E.toDynHelper(vt)}(${expr})`;
1464
+ }
1465
+ if (t.kind === "union") {
1466
+ const def = E.unionsById.get(t.unionId);
1467
+ const tag = def?.arms.findIndex((a) => typeEquals(a, vt)) ?? -1;
1468
+ if (tag < 0)
1469
+ throw new Error(`emitter bug: keyed read arm for ${typeKey(vt)} in ${typeKey(t)}`);
1470
+ if (vt.kind === "f64")
1471
+ return `scr_union_new_f64(${tag}, ${expr})`;
1472
+ if (vt.kind === "bool")
1473
+ return `scr_union_new_bool(${tag}, ${expr})`;
1474
+ const rc = vAdapters(vt);
1475
+ const payload = owned ? expr : retainCallC(vt, expr);
1476
+ return `scr_union_new_ref(${tag}, ${payload}, &${rc.retain}, &${rc.release}, ${E.traceArgC(vt)})`;
1477
+ }
1478
+ throw new Error(`emitter bug: keyed read cannot surface ${typeKey(vt)} as ${typeKey(t)}`);
1479
+ };
1480
+ // overflowOnly (a literal key naming no declared field): the declared
1481
+ // switch can never hit — only the overflow answers.
1482
+ for (const f of overflowOnly ? [] : shape.fields) {
1483
+ const lit = E.internLiteral(f.name);
1484
+ d.push(` if (scr_str_eq(k, (ScrStr *)&${lit})) { /* ${f.name} */`);
1485
+ d.push(` return ${surface(f.type, `r->${mangleField(f.name)}`, false)};`);
1486
+ d.push(` }`);
1487
+ }
1488
+ const iv = shape.indexValue;
1489
+ if (iv) {
1490
+ const m = `r->${OVERFLOW_MEMBER}`;
1491
+ if (iv.kind === "f64" || iv.kind === "bool") {
1492
+ const acc = iv.kind === "f64" ? "f64" : "bool";
1493
+ d.push(` ${cDecl(iv, "hit")};`);
1494
+ d.push(` if (scr_map_get_str_${acc}(${m}, k, &hit)) {`);
1495
+ d.push(` return ${surface(iv, "hit", true)};`);
1496
+ d.push(` }`);
1497
+ }
1498
+ else {
1499
+ d.push(` ${cDecl(iv, "hit")} = (${cType(iv).trim()})scr_map_get_str_ref(${m}, k);`);
1500
+ d.push(` if (hit) {`);
1501
+ if (typeEquals(iv, t)) {
1502
+ d.push(` return hit; /* get returned +1 */`);
1503
+ }
1504
+ else if (t.kind === "union") {
1505
+ d.push(` return ${surface(iv, "hit", true)};`);
1506
+ }
1507
+ else {
1508
+ throw new Error(`emitter bug: keyed read overflow ${typeKey(iv)} as ${typeKey(t)}`);
1509
+ }
1510
+ d.push(` }`);
1511
+ }
1512
+ }
1513
+ // The miss path.
1514
+ if (t.kind === "dyn") {
1515
+ d.push(` return scr_dyn_retain(scr_dyn_undefined());`);
1516
+ }
1517
+ else if (t.kind === "union" && E.undefinedArmTag(t) >= 0) {
1518
+ d.push(` return ${E.unitInstanceRef(t.unionId, E.undefinedArmTag(t))};`);
1519
+ }
1520
+ else {
1521
+ // JS would answer undefined; T has no way to say it (the checker
1522
+ // claimed T without noUncheckedIndexedAccess) — trap like an array
1523
+ // OOB read instead of corrupting a typed slot (SEMANTICS.md).
1524
+ d.push(` fprintf(stderr, "scriptc: TypeError: record has no key '%.*s' (typed '${E.dynDesc(t)}' — no undefined is representable)\\n", (int)k->len, k->data);`);
1525
+ d.push(` abort();`);
1526
+ }
1527
+ d.push(`}`, ``);
1528
+ E.walkerDefs.push(...d);
1529
+ return name;
1530
+ }
1531
+ /** The emitted dynamic-keyed WRITE helper for one index-signature shape:
1532
+ * `static void sc_rks_<n>(<struct> *r, ScrStr *k, <V> v)` — a declared
1533
+ * key writes THROUGH to the struct slot (dyn values validate against the
1534
+ * field's type first via the dynCheck walker: a mismatch throws the
1535
+ * catchable TypeError and leaves the field untouched — JS would store
1536
+ * anything, the documented divergence; typed values store directly),
1537
+ * an undeclared key inserts/replaces in the overflow map. Borrows r and
1538
+ * k; OWNS v (+1 moves in — released on the validation-failure path). */
1539
+ export function recordKeySetHelper(E, shapeId) {
1540
+ const existing = E.recordKeySetFns.get(shapeId);
1541
+ if (existing)
1542
+ return existing;
1543
+ const shape = E.recordsById.get(shapeId);
1544
+ if (!shape?.indexValue)
1545
+ throw new Error(`emitter bug: keyed write on non-overflow shape ${shapeId}`);
1546
+ const iv = shape.indexValue;
1547
+ const name = `sc_rks_${E.recordKeySetFns.size}`;
1548
+ E.recordKeySetFns.set(shapeId, name);
1549
+ const struct = mangleRecordStruct(shapeId);
1550
+ const sig = `static void ${name}(${struct} *r, ScrStr *k, ${cDecl(iv, "v")})`;
1551
+ E.walkerProtos.push(`${sig}; /* r[k] = v on ${shapeId} */`);
1552
+ const d = [`${sig} { /* r[k] = v on ${shapeId} */`];
1553
+ for (const f of shape.fields) {
1554
+ const lit = E.internLiteral(f.name);
1555
+ const member = `r->${mangleField(f.name)}`;
1556
+ d.push(` if (scr_str_eq(k, (ScrStr *)&${lit})) { /* ${f.name} */`);
1557
+ if (iv.kind === "dyn") {
1558
+ const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
1559
+ d.push(` ScrDynPath p = { NULL, ${keyLit}, 0 };`);
1560
+ d.push(` ${cDecl(f.type, "nv")} = ${E.dynCheckHelper(f.type)}(v, &p);`);
1561
+ d.push(` scr_dyn_release(v);`);
1562
+ d.push(` if (scr_exc_pending()) return; /* mismatched write: TypeError, field untouched */`);
1563
+ d.push(` ${isRefCounted(f.type) ? `if (${member}) ${releaseCallC(f.type, member)};` : `(void)0;`}`);
1564
+ d.push(` ${member} = nv;`);
1565
+ }
1566
+ else {
1567
+ // typeEquals(f.type, iv) — the frontend fences everything else.
1568
+ if (isRefCounted(f.type)) {
1569
+ d.push(` if (${member}) ${releaseCallC(f.type, member)};`);
1570
+ }
1571
+ d.push(` ${member} = v;`);
1572
+ }
1573
+ d.push(` return;`);
1574
+ d.push(` }`);
1575
+ }
1576
+ const m = `r->${OVERFLOW_MEMBER}`;
1577
+ if (iv.kind === "f64" || iv.kind === "bool") {
1578
+ d.push(` scr_map_set_str_${iv.kind === "f64" ? "f64" : "bool"}(${m}, k, v);`);
1579
+ }
1580
+ else {
1581
+ d.push(` scr_map_set_str_ref(${m}, k, v); /* v moves in */`);
1582
+ }
1583
+ d.push(`}`, ``);
1584
+ E.walkerDefs.push(...d);
1585
+ return name;
1586
+ }
1587
+ //# sourceMappingURL=emit-walkers.js.map