@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,960 @@
1
+ import { mangleField, mangleGlobal, mangleLocal, mangleRawParam } from "../mangle.js";
2
+ import { BOOL, CAUGHT, RUNTIME_ERROR_CLASSES, isRefCounted } from "../../ir/nodes.js";
3
+ import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./emit-types.js";
4
+ import { OVERFLOW_MEMBER } from "./emit-shapes.js";
5
+ export function emitFunction(E, fn) {
6
+ E.tempCounter = 0;
7
+ E.frames = [];
8
+ E.scopes = [];
9
+ E.jumpTargets = [];
10
+ E.tryStack = [];
11
+ E.finallyStack = [];
12
+ E.currentReturnType = fn.returnType;
13
+ E.currentGenerator = fn.generator ?? null;
14
+ E.labelCounter = 0;
15
+ E.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
16
+ E.captureIds = new Set((fn.captures ?? []).map((c) => c.localId));
17
+ E.line(`${E.signature(fn)} {${E.srcComment(fn.loc)}`);
18
+ E.indent++;
19
+ // The pending-return slot: a `return` crossing a finally computes its
20
+ // value FIRST (before the finally runs — snapshotting it here is what
21
+ // makes finally mutations of returned locals invisible, Node-exact),
22
+ // then jumps through every crossed finally; the last dispatch returns
23
+ // the slot. Declared only when some return actually crosses a finally.
24
+ if (fn.returnType.kind !== "void" && returnCrossesFinally(fn.body)) {
25
+ const init = isRefCounted(fn.returnType) ? "NULL" : "0";
26
+ E.line(`${cDecl(fn.returnType, "sc_pret")} = ${init}; /* pending return (through finally) */`);
27
+ }
28
+ // Captured bindings come in through the environment — borrowed for the
29
+ // whole call (the closure owns them): bound here, never released here.
30
+ (fn.captures ?? []).forEach((c, i) => {
31
+ E.line(`ScrBox *${mangleLocal(c.localId)} = sc_env->caps[${i}]; /* captured ${c.name} */`);
32
+ });
33
+ const paramIds = new Set(fn.params.map((p) => p.localId));
34
+ for (const local of fn.locals) {
35
+ if (paramIds.has(local.id) || E.captureIds.has(local.id))
36
+ continue;
37
+ if (local.boxed) {
38
+ E.line(`ScrBox *${mangleLocal(local.id)} = NULL; /* ${local.name} (boxed) */`);
39
+ }
40
+ else {
41
+ const init = isRefCounted(local.type) ? " = NULL" : "";
42
+ E.line(`${cDecl(local.type, mangleLocal(local.id))}${init}; /* ${local.name} */`);
43
+ }
44
+ }
45
+ // Function scope owns refcounted params (callees own their params).
46
+ // Boxed params: allocate the shared binding and move the raw value in.
47
+ const fnScope = [];
48
+ for (const p of fn.params) {
49
+ const local = E.currentLocals.get(p.localId);
50
+ if (local.boxed) {
51
+ const box = mangleLocal(p.localId);
52
+ E.line(`ScrBox *${box} = ${E.boxNewC(p.type)}; /* ${p.name} (boxed param) */`);
53
+ E.line(`scr_box_set_${boxAccess(p.type)}(${box}, ${mangleRawParam(p.localId)});`);
54
+ fnScope.push({ name: box, type: p.type, boxed: true });
55
+ }
56
+ else if (isRefCounted(p.type)) {
57
+ fnScope.push({ name: mangleLocal(p.localId), type: p.type });
58
+ }
59
+ }
60
+ E.scopes.push(fnScope);
61
+ E.emitStmts(fn.body);
62
+ // Implicit exit of a void function: release function-scope refcounted
63
+ // locals (unless the body already ended in an explicit return or a
64
+ // throw, whose unwind released everything down to depth 0).
65
+ const last = fn.body[fn.body.length - 1]?.kind;
66
+ const endedWithReturn = last === "return" || last === "throw" || last === "rethrow" || last === "runtimeFence";
67
+ if (fn.returnType.kind === "void" && !endedWithReturn) {
68
+ E.releaseFrame(E.scopes[0]);
69
+ }
70
+ E.scopes.pop();
71
+ E.indent--;
72
+ E.line(`}`);
73
+ E.line(``);
74
+ }
75
+ /** The statement bodies nested directly under a statement — the shared
76
+ * walk for returnCrossesFinally. */
77
+ function childBodies(s) {
78
+ switch (s.kind) {
79
+ case "if":
80
+ return s.else_ ? [s.then, s.else_] : [s.then];
81
+ case "while":
82
+ case "doWhile":
83
+ case "forOf":
84
+ case "block":
85
+ return [s.body];
86
+ case "for":
87
+ return [s.body];
88
+ case "switch":
89
+ return s.cases.map((c) => c.body);
90
+ case "tryCatch":
91
+ return [
92
+ s.tryBody,
93
+ ...(s.catchBody ? [s.catchBody] : []),
94
+ ...(s.finallyBody ? [s.finallyBody] : []),
95
+ ];
96
+ default:
97
+ return [];
98
+ }
99
+ }
100
+ /** True when some `return` sits inside the tryBody/catchBody of a
101
+ * try-with-finally, at any nesting depth — exactly the returns the
102
+ * pending-return path routes, and so exactly when emitFunction must
103
+ * declare the sc_pret slot. */
104
+ function returnCrossesFinally(stmts) {
105
+ const hasReturn = (body) => body.some((s) => s.kind === "return" || childBodies(s).some(hasReturn));
106
+ const walk = (body) => body.some((s) => {
107
+ if (s.kind === "tryCatch" &&
108
+ s.finallyBody !== null &&
109
+ (hasReturn(s.tryBody) || (s.catchBody !== null && hasReturn(s.catchBody)))) {
110
+ return true;
111
+ }
112
+ return childBodies(s).some(walk);
113
+ });
114
+ return walk(stmts);
115
+ }
116
+ /** Emits a block in its own lexical scope (refcounted locals released at
117
+ * end). `setup` runs after the scope opens, before the statements — the
118
+ * catch-binding hook: it may emit prelude lines and register entries the
119
+ * scope owns (released on every exit, jumps and unwinds included). */
120
+ export function emitBlock(E, stmts, setup) {
121
+ E.line(`{`);
122
+ E.indent++;
123
+ const scope = [];
124
+ E.scopes.push(scope);
125
+ setup?.(scope);
126
+ E.emitStmts(stmts);
127
+ const endedWithJump = E.endsWithJump(stmts);
128
+ E.scopes.pop();
129
+ if (!endedWithJump)
130
+ E.releaseFrame(scope);
131
+ E.indent--;
132
+ E.line(`}`);
133
+ }
134
+ export function emitStmts(E, stmts) {
135
+ for (const s of stmts)
136
+ E.emitStmt(s);
137
+ }
138
+ export function emitStmt(E, s) {
139
+ E.frames.push([]);
140
+ switch (s.kind) {
141
+ case "varDecl": {
142
+ const local = E.currentLocals.get(s.localId);
143
+ const target = mangleLocal(s.localId);
144
+ if (s.init === null) {
145
+ // Declared, uninitialized (`let x: number;`). tsc's TS2454 rejects
146
+ // any read before assignment, so no runtime check is needed — but
147
+ // the declaration must still RESET the C local: inside a loop the
148
+ // previous iteration's scope exit released the old value and left
149
+ // a stale pointer, and the scope-exit release below runs whether
150
+ // or not an `assign` ever did (runtime releases are NULL-tolerant).
151
+ if (local.boxed) {
152
+ // The box must exist immediately: closures created before the
153
+ // first assignment capture it. (tsc's TS2454 can't see through
154
+ // closures — a call before the first assignment reads 0/false
155
+ // for scalars and traps for refcounted kinds, where JS has
156
+ // `undefined`.)
157
+ // A SCALAR TDZ box rides an ARR-kind box: the value lives in a
158
+ // one-element array cell, so the empty (NULL) slot stays the
159
+ // not-yet-initialized sentinel — a raw scalar slot has no spare
160
+ // bit pattern to spend on it.
161
+ const boxNew = local.tdz && boxAccess(local.type) !== "ref"
162
+ ? "scr_box_new(SCR_BOX_ARR)"
163
+ : E.boxNewC(local.type);
164
+ E.line(`${target} = ${boxNew};${E.srcComment(s.loc)} /* let ${local.name}; */`);
165
+ E.scopes[E.scopes.length - 1].push({ name: target, type: local.type, boxed: true });
166
+ }
167
+ else if (isRefCounted(local.type)) {
168
+ E.line(`${target} = NULL;${E.srcComment(s.loc)} /* let ${local.name}; */`);
169
+ E.scopes[E.scopes.length - 1].push({ name: target, type: local.type });
170
+ }
171
+ // Scalars need nothing: the C local exists from the prologue and
172
+ // no read happens before an assign writes it.
173
+ break;
174
+ }
175
+ if (local.boxed) {
176
+ // Box FIRST, then evaluate the initializer: a named function
177
+ // expression's closure captures this box during init evaluation.
178
+ E.line(`${target} = ${E.boxNewC(local.type)};${E.srcComment(s.loc)}`);
179
+ E.scopes[E.scopes.length - 1].push({ name: target, type: local.type, boxed: true });
180
+ const v = E.emitExpr(s.init);
181
+ if (isRefCounted(v.type))
182
+ E.moveTemp(v); // the box takes ownership
183
+ E.line(`scr_box_set_${boxAccess(local.type)}(${target}, ${v.name});`);
184
+ break;
185
+ }
186
+ const v = E.emitExpr(s.init);
187
+ E.moveTemp(v);
188
+ E.line(`${target} = ${v.name};${E.srcComment(s.loc)}`);
189
+ if (isRefCounted(v.type)) {
190
+ E.scopes[E.scopes.length - 1].push({ name: target, type: v.type });
191
+ }
192
+ break;
193
+ }
194
+ case "assign": {
195
+ const local = E.currentLocals.get(s.localId);
196
+ if (!local) {
197
+ // Module global: plain static storage, never boxed. Old-value
198
+ // release is NULL-tolerant (statics start NULL).
199
+ const g = E.globalsById.get(s.localId);
200
+ if (!g)
201
+ throw new Error(`emitter bug: assign to unknown binding ${s.localId}`);
202
+ const target = mangleGlobal(g.id);
203
+ const v = E.emitExpr(s.value);
204
+ E.moveTemp(v);
205
+ if (isRefCounted(v.type))
206
+ E.releaseValue(target, v.type);
207
+ E.line(`${target} = ${v.name};${E.srcComment(s.loc)}`);
208
+ break;
209
+ }
210
+ const target = mangleLocal(s.localId);
211
+ const v = E.emitExpr(s.value);
212
+ if (local.boxed) {
213
+ // A scalar TDZ box (forward-captured const): the initializing
214
+ // write mints the one-element array cell — set_ref moves it in
215
+ // (and the empty-slot sentinel ends here).
216
+ if (local.tdz && boxAccess(local.type) !== "ref") {
217
+ const acc = boxAccess(local.type);
218
+ const cell = `sc_t${E.tempCounter++}`;
219
+ E.line(`ScrArr *${cell} = ${E.arrNewC(local.type, 1)};${E.srcComment(s.loc)}`);
220
+ E.line(`scr_arr_push_${acc}(${cell}, ${v.name});`);
221
+ E.line(`scr_box_set_ref(${target}, ${cell});`);
222
+ break;
223
+ }
224
+ if (isRefCounted(v.type))
225
+ E.moveTemp(v); // set_ref releases the old value
226
+ E.line(`scr_box_set_${boxAccess(local.type)}(${target}, ${v.name});${E.srcComment(s.loc)}`);
227
+ break;
228
+ }
229
+ E.moveTemp(v);
230
+ if (isRefCounted(v.type))
231
+ E.releaseValue(target, v.type);
232
+ E.line(`${target} = ${v.name};${E.srcComment(s.loc)}`);
233
+ break;
234
+ }
235
+ case "exprStmt":
236
+ E.emitExpr(s.expr);
237
+ break;
238
+ case "if": {
239
+ const cond = E.emitCondition(s.cond);
240
+ E.line(`if (${cond}) `);
241
+ E.mergeBrace(() => E.emitBlock(s.then));
242
+ if (s.else_) {
243
+ E.line(`else `);
244
+ E.mergeBrace(() => E.emitBlock(s.else_));
245
+ }
246
+ break;
247
+ }
248
+ case "while": {
249
+ E.line(`for (;;) {${E.srcComment(s.loc)}`);
250
+ E.indent++;
251
+ const cond = E.emitCondition(s.cond);
252
+ E.line(`if (!(${cond})) break;`);
253
+ // C continue re-evaluates the condition at the top: exactly right
254
+ // for the unlabeled loop. A LABELED while allocates a continue
255
+ // label placed at the END of the body (falling off it re-enters
256
+ // the condition — the same point) so a labeled continue from a
257
+ // NESTED loop can goto it, and a lazy end label for labeled break.
258
+ const loop = E.loopTarget(null, s.labels);
259
+ E.jumpTargets.push(loop);
260
+ E.emitBlock(s.body);
261
+ E.jumpTargets.pop();
262
+ if (loop.usedContinue && loop.continueLabel)
263
+ E.line(`${loop.continueLabel}:;`);
264
+ E.indent--;
265
+ E.line(`}`);
266
+ if (loop.usedEnd)
267
+ E.line(`${loop.endLabel}:;`);
268
+ break;
269
+ }
270
+ case "doWhile": {
271
+ // `for (;;) { body; cont: cond; if (!cond) break; }` — the body runs
272
+ // before the first condition check (at least once), and `continue`
273
+ // routes through the label so the condition still evaluates.
274
+ E.line(`for (;;) {${E.srcComment(s.loc)}`);
275
+ E.indent++;
276
+ const loop = E.loopTarget(`sc_cont_${E.labelCounter++}`, s.labels);
277
+ E.jumpTargets.push(loop);
278
+ E.emitBlock(s.body);
279
+ E.jumpTargets.pop();
280
+ if (loop.usedContinue)
281
+ E.line(`${loop.continueLabel}:;`);
282
+ const cond = E.emitCondition(s.cond);
283
+ E.line(`if (!(${cond})) break;`);
284
+ E.indent--;
285
+ E.line(`}`);
286
+ if (loop.usedEnd)
287
+ E.line(`${loop.endLabel}:;`);
288
+ break;
289
+ }
290
+ case "switch":
291
+ E.emitSwitch(s);
292
+ break;
293
+ case "for": {
294
+ // Desugared in place; the init's scope wraps the whole loop, so
295
+ // break/continue must NOT release it (scopeDepth captured after).
296
+ E.line(`{${E.srcComment(s.loc)}`);
297
+ E.indent++;
298
+ E.scopes.push([]);
299
+ if (s.init)
300
+ E.emitStmt(s.init);
301
+ E.line(`for (;;) {`);
302
+ E.indent++;
303
+ if (s.cond) {
304
+ const cond = E.emitCondition(s.cond);
305
+ E.line(`if (!(${cond})) break;`);
306
+ }
307
+ // C continue would skip the update; route it through a label instead.
308
+ const loop = E.loopTarget(`sc_cont_${E.labelCounter++}`, s.labels);
309
+ E.jumpTargets.push(loop);
310
+ E.emitBlock(s.body);
311
+ E.jumpTargets.pop();
312
+ if (loop.usedContinue)
313
+ E.line(`${loop.continueLabel}:;`);
314
+ // JS `for (let i ...)`: each iteration gets a FRESH binding holding a
315
+ // copy of the previous one, and the update mutates the fresh binding
316
+ // — that's why closures made in iteration k keep seeing iteration
317
+ // k's value. Only observable (and only emitted) when captured.
318
+ if (s.init?.kind === "varDecl") {
319
+ const initLocal = E.currentLocals.get(s.init.localId);
320
+ if (initLocal?.boxed) {
321
+ const box = mangleLocal(initLocal.id);
322
+ const fresh = `sc_t${E.tempCounter++}`;
323
+ const acc = boxAccess(initLocal.type);
324
+ E.line(`ScrBox *${fresh} = ${E.boxNewC(initLocal.type)}; /* per-iteration ${initLocal.name} */`);
325
+ E.line(`scr_box_set_${acc}(${fresh}, scr_box_get_${acc}(${box}));`);
326
+ E.line(`scr_box_release(${box});`);
327
+ E.line(`${box} = ${fresh};`);
328
+ // The wrapper scope's entry releases whatever `box` points to at
329
+ // loop exit — which is now the freshest binding. Nothing to fix.
330
+ }
331
+ }
332
+ if (s.update)
333
+ E.emitStmt(s.update);
334
+ E.indent--;
335
+ E.line(`}`);
336
+ // A labeled break lands exactly where C break does: BEFORE the
337
+ // init scope's release (the goto path must run the same releases
338
+ // as the fall-through path).
339
+ if (loop.usedEnd)
340
+ E.line(`${loop.endLabel}:;`);
341
+ E.releaseFrame(E.scopes.pop());
342
+ E.indent--;
343
+ E.line(`}`);
344
+ break;
345
+ }
346
+ case "arraySet": {
347
+ // Evaluation order matches JS: array, index, then value. Ownership
348
+ // of a refcounted value moves into the array (the runtime releases
349
+ // the replaced element itself).
350
+ const arr = E.emitExpr(s.arr);
351
+ const idx = E.emitExpr(s.index);
352
+ const v = E.emitExpr(s.value);
353
+ if (s.arr.type.kind !== "array")
354
+ throw new Error("emitter bug: arraySet on non-array");
355
+ const acc = elemAccess(s.arr.type.elem);
356
+ if (acc === "ref")
357
+ E.moveTemp(v);
358
+ E.line(`scr_arr_set_${acc}(${arr.name}, ${idx.name}, ${v.name});${E.srcComment(s.loc)}`);
359
+ break;
360
+ }
361
+ case "bytesSet": {
362
+ // Typed-array element write: same evaluation order as arraySet;
363
+ // the value is a scalar (the runtime coerces JS-exactly), so no
364
+ // ownership moves. Any invalid index traps — no append.
365
+ const arr = E.emitExpr(s.arr);
366
+ const idx = E.emitExpr(s.index);
367
+ const v = E.emitExpr(s.value);
368
+ E.line(`scr_bytes_set(${arr.name}, ${idx.name}, ${v.name});${E.srcComment(s.loc)}`);
369
+ break;
370
+ }
371
+ case "fieldSet":
372
+ case "recordSet": {
373
+ // Evaluation order: obj, then value. New value moved in; the old
374
+ // value is released AFTER the field is overwritten (releases are
375
+ // NULL-tolerant — fields start NULL from the zeroed allocation).
376
+ // Unlink-then-release is load-bearing: a release can trigger a
377
+ // cycle collection, which must never see a heap edge whose count
378
+ // was already given up (scr_cycle.c). Classes and records share
379
+ // the struct layout, so one emission.
380
+ const obj = E.emitExpr(s.obj);
381
+ const v = E.emitExpr(s.value);
382
+ // Runtime error classes use ScrError's own member names.
383
+ const member = s.kind === "fieldSet" && RUNTIME_ERROR_CLASSES.has(s.className)
384
+ ? s.field
385
+ : mangleField(s.field);
386
+ const field = `${obj.name}->${member}`;
387
+ if (isRefCounted(v.type)) {
388
+ E.moveTemp(v);
389
+ const old = `sc_t${E.tempCounter++}`;
390
+ E.line(`${cDecl(v.type, old)} = ${field};`);
391
+ E.line(`${field} = ${v.name};${E.srcComment(s.loc)}`);
392
+ E.releaseValue(old, v.type);
393
+ }
394
+ else {
395
+ E.line(`${field} = ${v.name};${E.srcComment(s.loc)}`);
396
+ }
397
+ break;
398
+ }
399
+ case "recordKeyDelete": {
400
+ // `delete obj[k]` on a pure index-signature shape: a Map delete on
401
+ // the overflow (key and value released; absent keys no-op).
402
+ const obj = E.emitExpr(s.obj);
403
+ const key = E.emitExpr(s.key);
404
+ E.line(`scr_map_delete_str(${obj.name}->${OVERFLOW_MEMBER}, ${key.name});${E.srcComment(s.loc)}`);
405
+ break;
406
+ }
407
+ case "recordKeySet": {
408
+ // Dynamic-keyed record write through the per-shape helper (declared
409
+ // keys write through with validation, undeclared keys land in the
410
+ // overflow map). Evaluation order: obj, key, value. The helper OWNS
411
+ // the value (+1 moves in). MAY THROW when a dyn value must validate
412
+ // against a declared field's type — the pending check runs then.
413
+ const obj = E.emitExpr(s.obj);
414
+ const key = E.emitExpr(s.key);
415
+ const v = E.emitExpr(s.value);
416
+ if (isRefCounted(v.type))
417
+ E.moveTemp(v);
418
+ // A LITERAL key naming no declared field is a plain overflow map
419
+ // insert — no helper, no validation, no throw.
420
+ if (s.overflowOnly) {
421
+ const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref";
422
+ E.line(`scr_map_set_str_${acc}(${obj.name}->${OVERFLOW_MEMBER}, ${key.name}, ${v.name});${E.srcComment(s.loc)}`);
423
+ break;
424
+ }
425
+ const helper = E.recordKeySetHelper(s.shapeId);
426
+ E.line(`${helper}(${obj.name}, ${key.name}, ${v.name});${E.srcComment(s.loc)}`);
427
+ const shape = E.recordsById.get(s.shapeId);
428
+ if (shape && shape.indexValue?.kind === "dyn" && shape.fields.length > 0) {
429
+ E.emitPendingCheck();
430
+ }
431
+ break;
432
+ }
433
+ case "forOf": {
434
+ // Ascending index loop; the length is re-read every iteration
435
+ // (JS-exact — pushes inside the body extend the iteration). The
436
+ // iterable temp lives in this statement's frame, so it is released
437
+ // when the whole loop ends (and by `return`'s frame sweep). A real
438
+ // C for-loop makes plain `continue` correct: the update still runs.
439
+ if (s.iterable.type.kind !== "array")
440
+ throw new Error("emitter bug: forOf over non-array");
441
+ const elem = s.iterable.type.elem;
442
+ const arr = E.emitExpr(s.iterable);
443
+ const idx = `sc_t${E.tempCounter++}`;
444
+ E.line(`for (double ${idx} = 0; ${idx} < scr_arr_len(${arr.name}); ${idx} += 1) {${E.srcComment(s.loc)}`);
445
+ E.indent++;
446
+ // A real C for-loop makes plain `continue` correct (the update
447
+ // still runs); a LABELED forOf allocates a continue label placed at
448
+ // the END of the iteration (after the per-iteration scope release —
449
+ // the goto path released it itself) so nested loops can goto it.
450
+ const loop = E.loopTarget(null, s.labels);
451
+ E.jumpTargets.push(loop);
452
+ // The loop variable is a fresh const per iteration: its scope opens
453
+ // here, holds the (for ref elements: owned +1) current element, and
454
+ // releases it at the end of each iteration.
455
+ E.scopes.push([]);
456
+ const local = mangleLocal(s.localId);
457
+ const localInfo = E.currentLocals.get(s.localId);
458
+ if (localInfo?.boxed) {
459
+ // Captured loop variable: a fresh box per iteration, matching the
460
+ // fresh const binding (closures made in iteration k keep seeing
461
+ // iteration k's element). The box takes ownership of a ref
462
+ // element's +1 and is released with the iteration's scope.
463
+ E.line(`${local} = ${E.boxNewC(elem)}; /* per-iteration ${localInfo.name} */`);
464
+ E.line(`scr_box_set_${boxAccess(elem)}(${local}, scr_arr_get_${elemAccess(elem)}(${arr.name}, ${idx}));`);
465
+ E.scopes[E.scopes.length - 1].push({ name: local, type: elem, boxed: true });
466
+ }
467
+ else {
468
+ E.line(`${local} = scr_arr_get_${elemAccess(elem)}(${arr.name}, ${idx});`);
469
+ if (isRefCounted(elem))
470
+ E.scopes[E.scopes.length - 1].push({ name: local, type: elem });
471
+ }
472
+ E.emitStmts(s.body);
473
+ const endedWithJump = E.endsWithJump(s.body);
474
+ const scope = E.scopes.pop();
475
+ if (!endedWithJump)
476
+ E.releaseFrame(scope);
477
+ E.jumpTargets.pop();
478
+ if (loop.usedContinue && loop.continueLabel)
479
+ E.line(`${loop.continueLabel}:;`);
480
+ E.indent--;
481
+ E.line(`}`);
482
+ if (loop.usedEnd)
483
+ E.line(`${loop.endLabel}:;`);
484
+ break;
485
+ }
486
+ case "block": {
487
+ if (s.labels === undefined) {
488
+ E.emitBlock(s.body);
489
+ break;
490
+ }
491
+ // A labeled block: `break lbl` inside jumps to the end label
492
+ // (releasing the block's scope itself); nothing else can target it.
493
+ const target = {
494
+ kind: "block",
495
+ endLabel: `sc_end_${E.labelCounter++}`,
496
+ usedEnd: false,
497
+ labels: s.labels,
498
+ scopeDepth: E.scopes.length,
499
+ frameDepth: E.frames.length,
500
+ };
501
+ E.jumpTargets.push(target);
502
+ E.emitBlock(s.body);
503
+ E.jumpTargets.pop();
504
+ if (target.usedEnd)
505
+ E.line(`${target.endLabel}:;`);
506
+ break;
507
+ }
508
+ case "break": {
509
+ // Unlabeled: binds to the innermost loop OR switch (labeled block
510
+ // targets are skipped). Labeled: binds to the entry carrying the
511
+ // label. Release every scope entered since that target started
512
+ // (the body scope and anything nested); their natural end-of-block
513
+ // releases are on the fall-through path this jump bypasses. Same
514
+ // for pending frame temps of statements the jump exits (a nested
515
+ // switch's discriminant) — but NOT the target's own frame: a
516
+ // loop's releases after the loop and a switch's after its end
517
+ // label are still on this jump's path.
518
+ let target;
519
+ for (let i = E.jumpTargets.length - 1; i >= 0; i--) {
520
+ const t = E.jumpTargets[i];
521
+ if (s.label !== undefined ? t.labels?.includes(s.label) : t.kind !== "block") {
522
+ target = t;
523
+ break;
524
+ }
525
+ }
526
+ if (!target)
527
+ throw new Error("emitter bug: break target not found");
528
+ E.releaseForJump(target.frameDepth, target.scopeDepth);
529
+ if (target.kind !== "loop" || s.label !== undefined) {
530
+ // Switches are emitted as goto chains and blocks aren't C loops
531
+ // at all, so a C `break` cannot target either; and a LABELED
532
+ // break may target an outer loop a C break would never reach.
533
+ // All three jump to the target's end label (labeled loops always
534
+ // allocate one — loopTarget).
535
+ target.usedEnd = true;
536
+ E.line(`goto ${target.endLabel};${E.srcComment(s.loc)}`);
537
+ }
538
+ else {
539
+ E.line(`break;${E.srcComment(s.loc)}`);
540
+ }
541
+ break;
542
+ }
543
+ case "continue": {
544
+ // Unlabeled: binds to the innermost LOOP, skipping any switches and
545
+ // labeled blocks in between (their scopes are still released — the
546
+ // jump exits them). Labeled: binds to the loop carrying the label.
547
+ let loop;
548
+ for (let i = E.jumpTargets.length - 1; i >= 0; i--) {
549
+ const t = E.jumpTargets[i];
550
+ if (t.kind === "loop" && (s.label === undefined || t.labels?.includes(s.label))) {
551
+ loop = t;
552
+ break;
553
+ }
554
+ }
555
+ if (!loop)
556
+ throw new Error("emitter bug: continue target not found");
557
+ E.releaseForJump(loop.frameDepth, loop.scopeDepth);
558
+ if (loop.continueLabel) {
559
+ // Labeled loops always allocate one (a labeled continue may
560
+ // target an outer loop a C continue could never reach); for/
561
+ // do-while allocate one for their update/condition point.
562
+ loop.usedContinue = true;
563
+ E.line(`goto ${loop.continueLabel};${E.srcComment(s.loc)}`);
564
+ }
565
+ else {
566
+ E.line(`continue;${E.srcComment(s.loc)}`);
567
+ }
568
+ break;
569
+ }
570
+ case "return": {
571
+ const fin = E.finallyStack[E.finallyStack.length - 1];
572
+ if (fin) {
573
+ // Crossing ≥1 finally: the value is computed and snapshotted
574
+ // FIRST (a finally mutating the returned local cannot change
575
+ // it — Node's semantics), then everything down to the innermost
576
+ // region releases and control runs that region's pending-return
577
+ // finally copy; its tail dispatches further out or returns.
578
+ if (s.value) {
579
+ const v = E.emitExpr(s.value);
580
+ E.moveTemp(v); // ownership parks in the slot until the dispatch returns it
581
+ E.line(`sc_pret = ${v.name};${E.srcComment(s.loc)}`);
582
+ }
583
+ fin.used = true;
584
+ E.releaseForJump(fin.frameDepth, fin.scopeDepth);
585
+ E.line(`goto ${fin.label};${s.value ? "" : E.srcComment(s.loc)}`);
586
+ break;
587
+ }
588
+ if (s.value) {
589
+ const v = E.emitExpr(s.value);
590
+ E.moveTemp(v);
591
+ // Everything down to function depth releases; the moved result is
592
+ // exempt (already struck from its frame).
593
+ E.releaseForJump(0, 0);
594
+ E.line(`return ${v.name};${E.srcComment(s.loc)}`);
595
+ }
596
+ else {
597
+ E.releaseForJump(0, 0);
598
+ E.line(`return;${E.srcComment(s.loc)}`);
599
+ }
600
+ break;
601
+ }
602
+ case "throw": {
603
+ // Evaluate, move ownership into the runtime's exception cell, then
604
+ // unwind unconditionally (the innermost try handler, or out of the
605
+ // function) — the same release path as return/break/continue.
606
+ const v = E.emitExpr(s.value);
607
+ const t = s.value.type;
608
+ if (isRefCounted(t))
609
+ E.moveTemp(v); // the cell takes ownership
610
+ if (t.kind === "f64") {
611
+ E.line(`scr_throw_f64(${v.name});${E.srcComment(s.loc)}`);
612
+ }
613
+ else if (t.kind === "bool") {
614
+ E.line(`scr_throw_bool(${v.name});${E.srcComment(s.loc)}`);
615
+ }
616
+ else if (t.kind === "string") {
617
+ E.line(`scr_throw_str(${v.name});${E.srcComment(s.loc)}`);
618
+ }
619
+ else if (t.kind === "object" && E.classMeta.get(t.className)?.hierarchy) {
620
+ // Hierarchy instances carry a vtable word: the OBJ kind keeps the
621
+ // dynamic class inspectable (catch-binding instanceof, and the
622
+ // uncaught printer's "name: message" for Error instances).
623
+ const rc = vAdapters(t);
624
+ E.line(`scr_throw_obj(${v.name}, &${rc.retain}, &${rc.release}, ${E.traceArgC(t)});${E.srcComment(s.loc)}`);
625
+ }
626
+ else {
627
+ const rc = vAdapters(t);
628
+ E.line(`scr_throw_ref(${v.name}, &${rc.retain}, &${rc.release}, ${E.traceArgC(t)});${E.srcComment(s.loc)}`);
629
+ }
630
+ E.emitUnwind();
631
+ break;
632
+ }
633
+ case "runtimeFence": {
634
+ // The deferred JS compile fence: throw a catchable Error naming
635
+ // the construct (message) with the SC code stamped on `code`,
636
+ // then unwind exactly like `throw`.
637
+ const bytes = Buffer.from(s.message, "utf8");
638
+ E.line(`scr_throw_error_msg_code(SCR_ERR_ERROR, ${cStringLiteral(bytes)}, ${bytes.length}, "${s.code}");${E.srcComment(s.loc)}`);
639
+ E.emitUnwind();
640
+ break;
641
+ }
642
+ case "rethrow":
643
+ // Re-raise the saved snapshot (payload retained — the binding local
644
+ // releases with its scope) and unwind like `throw`.
645
+ E.line(`scr_rethrow(${mangleLocal(s.localId)});${E.srcComment(s.loc)}`);
646
+ E.emitUnwind();
647
+ break;
648
+ case "tryCatch":
649
+ E.emitTryCatch(s);
650
+ break;
651
+ default: {
652
+ const _exhaustive = s;
653
+ void _exhaustive;
654
+ }
655
+ }
656
+ const frame = E.frames.pop();
657
+ // return/throw already released their frames on the jump path; emitting
658
+ // the fall-through releases after them would be dead double-release code.
659
+ if (s.kind !== "return" && s.kind !== "throw" && s.kind !== "rethrow" && s.kind !== "runtimeFence")
660
+ E.releaseFrame(frame);
661
+ }
662
+ /** try/catch/finally via pending-flag unwinding. Entering a try emits NO
663
+ * code: the try context is compile-time state (tryStack) that redirects
664
+ * unwinds inside the region to a label here instead of out of the
665
+ * function. Shape:
666
+ *
667
+ * { try body } unwinds inside release frames/scopes down to
668
+ * this statement's depths, then goto the handler
669
+ * goto after; (normal completion skips the handler)
670
+ * sc_catch_N:; (emitted only when some unwind targets it)
671
+ * scr_exc_clear(); catch TAKES the exception (payload discarded —
672
+ * { catch body } the supported catch form is bindingless)
673
+ * after/sc_fin_N:;
674
+ * { finally body } normal path
675
+ * goto sc_tryend_N;
676
+ * sc_finexc_N:; exception path: pending flag still set
677
+ * { finally body } (emitted twice — fresh temps/labels each time;
678
+ * <unwind> duplication is safe, and the pending flag is
679
+ * sc_tryend_N:; the dispatch)
680
+ *
681
+ * Without a catch, unwinds in the try body target sc_finexc_N directly.
682
+ * A catch body's own exceptions target sc_finexc_N (when a finally
683
+ * exists) or the ENCLOSING context — never its own handler. After the
684
+ * exception-path finally, propagation continues (emitUnwind: enclosing
685
+ * handler or dummy return). A `return` inside the try/catch body rides a
686
+ * THIRD finally copy (sc_finret_N — the pending-return path: value
687
+ * snapshotted into sc_pret at the return site, dispatch outward after
688
+ * the copy runs); break/continue never cross a finally and no jump
689
+ * leaves a finally body (frontend fence + validator backstop), so
690
+ * normal, exception, and pending-return are the only paths a finally
691
+ * must model; jumps out of PLAIN try/catch need nothing here —
692
+ * release-on-jump already walks the try scopes. */
693
+ export function emitTryCatch(E, s) {
694
+ const id = E.labelCounter++;
695
+ const hasCatch = s.catchBody !== null;
696
+ const hasFinally = s.finallyBody !== null;
697
+ const catchLabel = `sc_catch_${id}`;
698
+ const finExcLabel = `sc_finexc_${id}`;
699
+ const endLabel = `sc_tryend_${id}`;
700
+ // Where the try body's normal completion continues.
701
+ const afterTryLabel = hasFinally ? `sc_fin_${id}` : endLabel;
702
+ let afterTryLabelUsed = false;
703
+ const handler = {
704
+ label: hasCatch ? catchLabel : finExcLabel,
705
+ used: false,
706
+ frameDepth: E.frames.length,
707
+ scopeDepth: E.scopes.length,
708
+ };
709
+ // The pending-return region: returns inside tryBody/catchBody snapshot
710
+ // their value and jump here-ish (the pending-return finally copy below)
711
+ // instead of returning directly. Same depths as the unwind handler.
712
+ const retEntry = hasFinally
713
+ ? {
714
+ label: `sc_finret_${id}`,
715
+ used: false,
716
+ frameDepth: E.frames.length,
717
+ scopeDepth: E.scopes.length,
718
+ }
719
+ : null;
720
+ E.line(`/* try */${E.srcComment(s.loc)}`);
721
+ if (retEntry)
722
+ E.finallyStack.push(retEntry);
723
+ E.tryStack.push(handler);
724
+ E.emitBlock(s.tryBody);
725
+ E.tryStack.pop();
726
+ // Exceptions raised in the CATCH body unwind to the exception-path
727
+ // finally (pending stays set through it) when one exists.
728
+ const excHandler = {
729
+ label: finExcLabel,
730
+ used: !hasCatch && handler.used,
731
+ frameDepth: E.frames.length,
732
+ scopeDepth: E.scopes.length,
733
+ };
734
+ if (hasCatch && handler.used) {
735
+ if (!E.endsWithJump(s.tryBody)) {
736
+ E.line(`goto ${afterTryLabel};`);
737
+ afterTryLabelUsed = true;
738
+ }
739
+ if (hasFinally)
740
+ E.tryStack.push(excHandler);
741
+ // Generator bodies: a pending GENRET sentinel (.return(v) injected at
742
+ // a yield) is a RETURN completion, not a throw — catch must not take
743
+ // it. Re-unwind past this handler (finally still runs — the unwind
744
+ // targets the exception-path finally or the enclosing context; the
745
+ // runtime depths here equal the handler's, so no double release).
746
+ const genretPrologue = () => {
747
+ if (E.currentGenerator === null)
748
+ return;
749
+ E.line(`if (scr_exc_genret_pending()) { /* .return(): not catchable */`);
750
+ E.indent++;
751
+ E.emitUnwind();
752
+ E.indent--;
753
+ E.line(`}`);
754
+ };
755
+ if (s.catchLocalId !== null) {
756
+ // catch (e): the exception MOVES into the binding's snapshot box,
757
+ // owned by the catch body's scope (released on every exit — normal
758
+ // fall-through, jumps out, and unwinds from the body).
759
+ const binding = mangleLocal(s.catchLocalId);
760
+ E.line(`${catchLabel}:; /* catch (${E.currentLocals.get(s.catchLocalId)?.name ?? "e"}) — takes the exception */`);
761
+ genretPrologue();
762
+ E.emitBlock(s.catchBody, (scope) => {
763
+ E.line(`${binding} = scr_exc_take();`);
764
+ scope.push({ name: binding, type: CAUGHT });
765
+ });
766
+ }
767
+ else {
768
+ E.line(`${catchLabel}:; /* catch — takes the exception */`);
769
+ genretPrologue();
770
+ E.line(`scr_exc_clear();`);
771
+ E.emitBlock(s.catchBody);
772
+ }
773
+ if (hasFinally)
774
+ E.tryStack.pop();
775
+ // Normal completion of the catch falls through to afterTryLabel.
776
+ }
777
+ if (retEntry)
778
+ E.finallyStack.pop();
779
+ if (hasFinally) {
780
+ if (afterTryLabelUsed)
781
+ E.line(`${afterTryLabel}:;`);
782
+ E.line(`/* finally (normal path) */`);
783
+ E.emitBlock(s.finallyBody);
784
+ const needEnd = excHandler.used || retEntry.used;
785
+ if (needEnd)
786
+ E.line(`goto ${endLabel};`);
787
+ if (excHandler.used) {
788
+ // The pending exception is STASHED across the finally body (a
789
+ // ScrCaught snapshot, re-raised after) so the body runs with a
790
+ // CLEAN cell: its own may-throw calls' pending checks answer for
791
+ // themselves — not for the in-flight exception — and a generator
792
+ // suspending here (a yield inside a finally on the .return()/
793
+ // .throw() unwind) resumes into the REST of the finally, exactly
794
+ // Node. A throw inside the body REPLACES the stash (it unwinds
795
+ // past the release through the synthetic scope entry below —
796
+ // JS's replace semantics); normal completion re-raises the stash
797
+ // and keeps propagating.
798
+ const stash = `sc_fexc_${id}`;
799
+ E.line(`${finExcLabel}:; /* finally (exception path — stashed) */`);
800
+ E.line(`ScrCaught *${stash} = scr_exc_take();`);
801
+ E.scopes.push([{ name: stash, type: CAUGHT }]);
802
+ E.emitBlock(s.finallyBody);
803
+ E.scopes.pop(); // normal completion keeps the stash for the re-raise
804
+ E.line(`scr_rethrow(${stash});`);
805
+ E.line(`scr_caught_release(${stash});`);
806
+ E.emitUnwind();
807
+ }
808
+ if (retEntry.used) {
809
+ // Pending-return path: a return in the try/catch body parked its
810
+ // value in sc_pret and jumped here after releasing down to this
811
+ // region. The finally body runs (third copy — fresh temps/labels,
812
+ // like the exception copy), then the dispatch continues outward:
813
+ // the next enclosing finally region of this function, or the
814
+ // actual return. A THROW inside this copy replaces the pending
815
+ // return (JS): the slot's owned value rides a synthetic scope
816
+ // entry so the unwind releases it.
817
+ E.line(`${retEntry.label}:; /* finally (pending-return path) */`);
818
+ const retT = E.currentReturnType;
819
+ const own = isRefCounted(retT);
820
+ if (own)
821
+ E.scopes.push([{ name: "sc_pret", type: retT }]);
822
+ E.emitBlock(s.finallyBody);
823
+ if (own)
824
+ E.scopes.pop();
825
+ const outer = E.finallyStack[E.finallyStack.length - 1];
826
+ if (outer) {
827
+ outer.used = true;
828
+ E.releaseForJump(outer.frameDepth, outer.scopeDepth);
829
+ E.line(`goto ${outer.label};`);
830
+ }
831
+ else {
832
+ E.releaseForJump(0, 0);
833
+ E.line(retT.kind === "void" ? `return;` : `return sc_pret;`);
834
+ }
835
+ }
836
+ if (needEnd)
837
+ E.line(`${endLabel}:;`);
838
+ }
839
+ else if (afterTryLabelUsed) {
840
+ E.line(`${endLabel}:;`);
841
+ }
842
+ }
843
+ /** JS-exact switch as a goto chain — C `switch` cannot express lazily
844
+ * evaluated, arbitrary-expression case tests. Shape:
845
+ *
846
+ * disc temp
847
+ * NULL-reset of refcounted case-body locals (see below)
848
+ * per test, in source order: eval test; if (== disc) goto case_i;
849
+ * goto default (or end when there is none)
850
+ * case_0:; body_0 ─┐ bodies in source order fall through
851
+ * case_1:; body_1 ─┘ naturally, JS-exact
852
+ * scope releases (natural fall-off path)
853
+ * end:; (break jumps here, after releasing scopes itself)
854
+ *
855
+ * All case bodies share ONE scope (JS: one lexical scope per switch body).
856
+ * Because dispatch can jump PAST a varDecl into a later case, a refcounted
857
+ * local of a skipped body is never written — and inside an enclosing loop
858
+ * it still holds the pointer a previous iteration's scope exit already
859
+ * released. The declarations are therefore NULL-reset up front; the
860
+ * scope-exit releases rely on the runtime's NULL-tolerant release calls. */
861
+ export function emitSwitch(E, s) {
862
+ if (s.disc.type.kind !== "f64" && s.disc.type.kind !== "string" && s.disc.type.kind !== "bool") {
863
+ throw new Error(`emitter bug: switch on ${s.disc.type.kind}`);
864
+ }
865
+ const id = E.labelCounter++;
866
+ const endLabel = `sc_swend_${id}`;
867
+ const caseLabel = (i) => `sc_swcase_${id}_${i}`;
868
+ // The disc temp lives in the whole statement's frame: for a string
869
+ // discriminant it stays alive across every test and body, released when
870
+ // the switch statement ends (break lands before that release; return
871
+ // sweeps frames itself).
872
+ const disc = E.emitExpr(s.disc);
873
+ // NULL-reset refcounted/boxed locals declared at the top level of case
874
+ // bodies (nested blocks manage their own scopes on normal control flow).
875
+ for (const c of s.cases) {
876
+ for (const stmt of c.body) {
877
+ if (stmt.kind !== "varDecl")
878
+ continue;
879
+ const local = E.currentLocals.get(stmt.localId);
880
+ if (local.boxed || isRefCounted(local.type)) {
881
+ E.line(`${mangleLocal(local.id)} = NULL; /* case-scoped ${local.name} */`);
882
+ }
883
+ }
884
+ }
885
+ // Dispatch: lazy source-order test evaluation (a test after the match
886
+ // never runs). Each test's temps release right after its comparison.
887
+ let defaultIdx = -1;
888
+ s.cases.forEach((c, i) => {
889
+ if (c.test === null) {
890
+ defaultIdx = i;
891
+ return;
892
+ }
893
+ E.frames.push([]);
894
+ const t = E.emitExpr(c.test);
895
+ const cmp = c.test.type.kind === "string"
896
+ ? `scr_str_eq(${disc.name}, ${t.name})`
897
+ : `${disc.name} == ${t.name}`;
898
+ const hit = E.newTemp(BOOL, cmp);
899
+ E.releaseFrame(E.frames.pop());
900
+ E.line(`if (${hit.name}) goto ${caseLabel(i)};`);
901
+ });
902
+ E.line(`goto ${defaultIdx >= 0 ? caseLabel(defaultIdx) : endLabel};`);
903
+ // Bodies in source order: entering one falls through the rest (JS-exact)
904
+ // until a break jumps to the end label.
905
+ const target = {
906
+ kind: "switch",
907
+ endLabel,
908
+ usedEnd: defaultIdx < 0,
909
+ ...(s.labels !== undefined && { labels: s.labels }),
910
+ scopeDepth: E.scopes.length,
911
+ frameDepth: E.frames.length,
912
+ };
913
+ E.jumpTargets.push(target);
914
+ E.scopes.push([]);
915
+ s.cases.forEach((c, i) => {
916
+ E.line(`${caseLabel(i)}:;`);
917
+ E.indent++;
918
+ E.emitStmts(c.body);
919
+ E.indent--;
920
+ });
921
+ E.jumpTargets.pop();
922
+ const scope = E.scopes.pop();
923
+ // Natural fall-off of the last body releases the shared scope here; a
924
+ // jump (break/return/continue/throw) already released it before jumping.
925
+ const lastBody = s.cases[s.cases.length - 1]?.body;
926
+ if (!lastBody || !E.endsWithJump(lastBody))
927
+ E.releaseFrame(scope);
928
+ if (target.usedEnd)
929
+ E.line(`${endLabel}:;`);
930
+ }
931
+ /** Emits `if (cond) ` followed by a block on the same line for readability. */
932
+ export function mergeBrace(E, emitBlockFn) {
933
+ const head = E.lines.pop();
934
+ const before = E.lines.length;
935
+ emitBlockFn();
936
+ E.lines[before] = head + E.lines[before].trimStart();
937
+ }
938
+ /** Evaluates `expr` in its own statement frame inside an already-open
939
+ * branch and moves the result into the C lvalue `target`: the chosen
940
+ * value's ownership transfers, every other temp the evaluation allocated
941
+ * releases inside the branch. The shared core of the lazily-branched
942
+ * expressions (`logical`, `ternary`); the caller owns the surrounding
943
+ * braces/indentation and registers `target` in ITS frame. */
944
+ export function emitBranchInto(E, target, expr) {
945
+ E.frames.push([]);
946
+ const v = E.emitExpr(expr);
947
+ E.moveTemp(v);
948
+ E.line(`${target} = ${v.name};`);
949
+ E.releaseFrame(E.frames.pop());
950
+ }
951
+ /** Evaluates a condition; its string temps are released before the branch,
952
+ * which is safe because the result is a scalar bool. */
953
+ export function emitCondition(E, cond) {
954
+ const t = E.emitExpr(cond);
955
+ const frame = E.currentFrame();
956
+ E.releaseFrame(frame);
957
+ frame.length = 0;
958
+ return t.name;
959
+ }
960
+ //# sourceMappingURL=emit-stmts.js.map