@justscale/typescript 0.1.1

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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/api.d.ts +144 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +380 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/compiler/analyzer.d.ts +247 -0
  8. package/dist/compiler/analyzer.d.ts.map +1 -0
  9. package/dist/compiler/analyzer.js +3201 -0
  10. package/dist/compiler/analyzer.js.map +1 -0
  11. package/dist/compiler/cli.d.ts +12 -0
  12. package/dist/compiler/cli.d.ts.map +1 -0
  13. package/dist/compiler/cli.js +209 -0
  14. package/dist/compiler/cli.js.map +1 -0
  15. package/dist/compiler/compile.d.ts +26 -0
  16. package/dist/compiler/compile.d.ts.map +1 -0
  17. package/dist/compiler/compile.js +121 -0
  18. package/dist/compiler/compile.js.map +1 -0
  19. package/dist/compiler/errors.d.ts +336 -0
  20. package/dist/compiler/errors.d.ts.map +1 -0
  21. package/dist/compiler/errors.js +466 -0
  22. package/dist/compiler/errors.js.map +1 -0
  23. package/dist/compiler/exports-prepass.d.ts +31 -0
  24. package/dist/compiler/exports-prepass.d.ts.map +1 -0
  25. package/dist/compiler/exports-prepass.js +249 -0
  26. package/dist/compiler/exports-prepass.js.map +1 -0
  27. package/dist/compiler/hmr-change-detector.d.ts +47 -0
  28. package/dist/compiler/hmr-change-detector.d.ts.map +1 -0
  29. package/dist/compiler/hmr-change-detector.js +395 -0
  30. package/dist/compiler/hmr-change-detector.js.map +1 -0
  31. package/dist/compiler/hmr-transformer.d.ts +54 -0
  32. package/dist/compiler/hmr-transformer.d.ts.map +1 -0
  33. package/dist/compiler/hmr-transformer.js +535 -0
  34. package/dist/compiler/hmr-transformer.js.map +1 -0
  35. package/dist/compiler/index.d.ts +19 -0
  36. package/dist/compiler/index.d.ts.map +1 -0
  37. package/dist/compiler/index.js +16 -0
  38. package/dist/compiler/index.js.map +1 -0
  39. package/dist/compiler/primitive-detector.d.ts +70 -0
  40. package/dist/compiler/primitive-detector.d.ts.map +1 -0
  41. package/dist/compiler/primitive-detector.js +338 -0
  42. package/dist/compiler/primitive-detector.js.map +1 -0
  43. package/dist/compiler/ptsc.d.ts +40 -0
  44. package/dist/compiler/ptsc.d.ts.map +1 -0
  45. package/dist/compiler/ptsc.js +462 -0
  46. package/dist/compiler/ptsc.js.map +1 -0
  47. package/dist/compiler/rewriter.d.ts +96 -0
  48. package/dist/compiler/rewriter.d.ts.map +1 -0
  49. package/dist/compiler/rewriter.js +418 -0
  50. package/dist/compiler/rewriter.js.map +1 -0
  51. package/dist/compiler/step-hash.d.ts +43 -0
  52. package/dist/compiler/step-hash.d.ts.map +1 -0
  53. package/dist/compiler/step-hash.js +83 -0
  54. package/dist/compiler/step-hash.js.map +1 -0
  55. package/dist/compiler/switch-codegen.d.ts +84 -0
  56. package/dist/compiler/switch-codegen.d.ts.map +1 -0
  57. package/dist/compiler/switch-codegen.js +1540 -0
  58. package/dist/compiler/switch-codegen.js.map +1 -0
  59. package/dist/compiler/transformer.d.ts +29 -0
  60. package/dist/compiler/transformer.d.ts.map +1 -0
  61. package/dist/compiler/transformer.js +216 -0
  62. package/dist/compiler/transformer.js.map +1 -0
  63. package/dist/config/index.d.ts +122 -0
  64. package/dist/config/index.d.ts.map +1 -0
  65. package/dist/config/index.js +215 -0
  66. package/dist/config/index.js.map +1 -0
  67. package/dist/di-errors/formatter.d.ts +126 -0
  68. package/dist/di-errors/formatter.d.ts.map +1 -0
  69. package/dist/di-errors/formatter.js +384 -0
  70. package/dist/di-errors/formatter.js.map +1 -0
  71. package/dist/di-errors/index.d.ts +5 -0
  72. package/dist/di-errors/index.d.ts.map +1 -0
  73. package/dist/di-errors/index.js +13 -0
  74. package/dist/di-errors/index.js.map +1 -0
  75. package/dist/editor.d.ts +11 -0
  76. package/dist/editor.d.ts.map +1 -0
  77. package/dist/editor.js +2 -0
  78. package/dist/editor.js.map +1 -0
  79. package/dist/index.d.ts +35 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +40 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/language-service/index.d.ts +52 -0
  84. package/dist/language-service/index.d.ts.map +1 -0
  85. package/dist/language-service/index.js +366 -0
  86. package/dist/language-service/index.js.map +1 -0
  87. package/dist/language-service/process-quick-fixes.d.ts +20 -0
  88. package/dist/language-service/process-quick-fixes.d.ts.map +1 -0
  89. package/dist/language-service/process-quick-fixes.js +114 -0
  90. package/dist/language-service/process-quick-fixes.js.map +1 -0
  91. package/dist/language-service/quick-fix-discovery.d.ts +39 -0
  92. package/dist/language-service/quick-fix-discovery.d.ts.map +1 -0
  93. package/dist/language-service/quick-fix-discovery.js +124 -0
  94. package/dist/language-service/quick-fix-discovery.js.map +1 -0
  95. package/dist/loader/incremental.d.ts +50 -0
  96. package/dist/loader/incremental.d.ts.map +1 -0
  97. package/dist/loader/incremental.js +151 -0
  98. package/dist/loader/incremental.js.map +1 -0
  99. package/dist/loader/index.d.ts +25 -0
  100. package/dist/loader/index.d.ts.map +1 -0
  101. package/dist/loader/index.js +24 -0
  102. package/dist/loader/index.js.map +1 -0
  103. package/dist/loader/loader.d.ts +52 -0
  104. package/dist/loader/loader.d.ts.map +1 -0
  105. package/dist/loader/loader.js +248 -0
  106. package/dist/loader/loader.js.map +1 -0
  107. package/dist/loader/register.d.ts +14 -0
  108. package/dist/loader/register.d.ts.map +1 -0
  109. package/dist/loader/register.js +20 -0
  110. package/dist/loader/register.js.map +1 -0
  111. package/dist/plugins/index.d.ts +13 -0
  112. package/dist/plugins/index.d.ts.map +1 -0
  113. package/dist/plugins/index.js +13 -0
  114. package/dist/plugins/index.js.map +1 -0
  115. package/dist/plugins/index.public.d.ts +13 -0
  116. package/dist/plugins/index.public.d.ts.map +1 -0
  117. package/dist/plugins/index.public.js +13 -0
  118. package/dist/plugins/index.public.js.map +1 -0
  119. package/dist/plugins/types.d.ts +83 -0
  120. package/dist/plugins/types.d.ts.map +1 -0
  121. package/dist/plugins/types.js +24 -0
  122. package/dist/plugins/types.js.map +1 -0
  123. package/dist/server/index.d.ts +33 -0
  124. package/dist/server/index.d.ts.map +1 -0
  125. package/dist/server/index.js +42 -0
  126. package/dist/server/index.js.map +1 -0
  127. package/dist/server/tsserver.d.ts +28 -0
  128. package/dist/server/tsserver.d.ts.map +1 -0
  129. package/dist/server/tsserver.js +126 -0
  130. package/dist/server/tsserver.js.map +1 -0
  131. package/lib/lib.d.ts +20 -0
  132. package/lib/lib.decorators.d.ts +382 -0
  133. package/lib/lib.decorators.legacy.d.ts +20 -0
  134. package/lib/lib.dom.asynciterable.d.ts +18 -0
  135. package/lib/lib.dom.d.ts +45125 -0
  136. package/lib/lib.dom.iterable.d.ts +18 -0
  137. package/lib/lib.es2015.collection.d.ts +150 -0
  138. package/lib/lib.es2015.core.d.ts +595 -0
  139. package/lib/lib.es2015.d.ts +26 -0
  140. package/lib/lib.es2015.generator.d.ts +75 -0
  141. package/lib/lib.es2015.iterable.d.ts +603 -0
  142. package/lib/lib.es2015.promise.d.ts +79 -0
  143. package/lib/lib.es2015.proxy.d.ts +126 -0
  144. package/lib/lib.es2015.reflect.d.ts +142 -0
  145. package/lib/lib.es2015.symbol.d.ts +44 -0
  146. package/lib/lib.es2015.symbol.wellknown.d.ts +324 -0
  147. package/lib/lib.es2016.array.include.d.ts +114 -0
  148. package/lib/lib.es2016.d.ts +19 -0
  149. package/lib/lib.es2016.full.d.ts +21 -0
  150. package/lib/lib.es2016.intl.d.ts +29 -0
  151. package/lib/lib.es2017.arraybuffer.d.ts +19 -0
  152. package/lib/lib.es2017.d.ts +24 -0
  153. package/lib/lib.es2017.date.d.ts +29 -0
  154. package/lib/lib.es2017.full.d.ts +21 -0
  155. package/lib/lib.es2017.intl.d.ts +42 -0
  156. package/lib/lib.es2017.object.d.ts +47 -0
  157. package/lib/lib.es2017.sharedmemory.d.ts +133 -0
  158. package/lib/lib.es2017.string.d.ts +43 -0
  159. package/lib/lib.es2017.typedarrays.d.ts +51 -0
  160. package/lib/lib.es2018.asyncgenerator.d.ts +75 -0
  161. package/lib/lib.es2018.asynciterable.d.ts +51 -0
  162. package/lib/lib.es2018.d.ts +22 -0
  163. package/lib/lib.es2018.full.d.ts +22 -0
  164. package/lib/lib.es2018.intl.d.ts +81 -0
  165. package/lib/lib.es2018.promise.d.ts +28 -0
  166. package/lib/lib.es2018.regexp.d.ts +35 -0
  167. package/lib/lib.es2019.array.d.ts +77 -0
  168. package/lib/lib.es2019.d.ts +22 -0
  169. package/lib/lib.es2019.full.d.ts +22 -0
  170. package/lib/lib.es2019.intl.d.ts +21 -0
  171. package/lib/lib.es2019.object.d.ts +31 -0
  172. package/lib/lib.es2019.string.d.ts +35 -0
  173. package/lib/lib.es2019.symbol.d.ts +22 -0
  174. package/lib/lib.es2020.bigint.d.ts +763 -0
  175. package/lib/lib.es2020.d.ts +25 -0
  176. package/lib/lib.es2020.date.d.ts +40 -0
  177. package/lib/lib.es2020.full.d.ts +22 -0
  178. package/lib/lib.es2020.intl.d.ts +472 -0
  179. package/lib/lib.es2020.number.d.ts +26 -0
  180. package/lib/lib.es2020.promise.d.ts +45 -0
  181. package/lib/lib.es2020.sharedmemory.d.ts +97 -0
  182. package/lib/lib.es2020.string.d.ts +42 -0
  183. package/lib/lib.es2020.symbol.wellknown.d.ts +39 -0
  184. package/lib/lib.es2021.d.ts +21 -0
  185. package/lib/lib.es2021.full.d.ts +22 -0
  186. package/lib/lib.es2021.intl.d.ts +164 -0
  187. package/lib/lib.es2021.promise.d.ts +46 -0
  188. package/lib/lib.es2021.string.d.ts +31 -0
  189. package/lib/lib.es2021.weakref.d.ts +76 -0
  190. package/lib/lib.es2022.array.d.ts +119 -0
  191. package/lib/lib.es2022.d.ts +23 -0
  192. package/lib/lib.es2022.error.d.ts +73 -0
  193. package/lib/lib.es2022.full.d.ts +22 -0
  194. package/lib/lib.es2022.intl.d.ts +143 -0
  195. package/lib/lib.es2022.object.d.ts +24 -0
  196. package/lib/lib.es2022.regexp.d.ts +37 -0
  197. package/lib/lib.es2022.string.d.ts +23 -0
  198. package/lib/lib.es2023.array.d.ts +922 -0
  199. package/lib/lib.es2023.collection.d.ts +19 -0
  200. package/lib/lib.es2023.d.ts +20 -0
  201. package/lib/lib.es2023.full.d.ts +22 -0
  202. package/lib/lib.es2023.intl.d.ts +62 -0
  203. package/lib/lib.es2024.arraybuffer.d.ts +63 -0
  204. package/lib/lib.es2024.collection.d.ts +27 -0
  205. package/lib/lib.es2024.d.ts +24 -0
  206. package/lib/lib.es2024.full.d.ts +22 -0
  207. package/lib/lib.es2024.object.d.ts +27 -0
  208. package/lib/lib.es2024.promise.d.ts +33 -0
  209. package/lib/lib.es2024.regexp.d.ts +23 -0
  210. package/lib/lib.es2024.sharedmemory.d.ts +66 -0
  211. package/lib/lib.es2024.string.d.ts +27 -0
  212. package/lib/lib.es2025.collection.d.ts +94 -0
  213. package/lib/lib.es2025.d.ts +23 -0
  214. package/lib/lib.es2025.float16.d.ts +443 -0
  215. package/lib/lib.es2025.full.d.ts +22 -0
  216. package/lib/lib.es2025.intl.d.ts +200 -0
  217. package/lib/lib.es2025.iterator.d.ts +146 -0
  218. package/lib/lib.es2025.promise.d.ts +32 -0
  219. package/lib/lib.es2025.regexp.d.ts +30 -0
  220. package/lib/lib.es5.d.ts +4599 -0
  221. package/lib/lib.es6.d.ts +21 -0
  222. package/lib/lib.esnext.array.d.ts +33 -0
  223. package/lib/lib.esnext.collection.d.ts +47 -0
  224. package/lib/lib.esnext.d.ts +27 -0
  225. package/lib/lib.esnext.date.d.ts +21 -0
  226. package/lib/lib.esnext.decorators.d.ts +26 -0
  227. package/lib/lib.esnext.disposable.d.ts +191 -0
  228. package/lib/lib.esnext.error.d.ts +22 -0
  229. package/lib/lib.esnext.full.d.ts +22 -0
  230. package/lib/lib.esnext.intl.d.ts +107 -0
  231. package/lib/lib.esnext.sharedmemory.d.ts +23 -0
  232. package/lib/lib.esnext.temporal.d.ts +485 -0
  233. package/lib/lib.esnext.typedarrays.d.ts +90 -0
  234. package/lib/lib.scripthost.d.ts +320 -0
  235. package/lib/lib.webworker.asynciterable.d.ts +18 -0
  236. package/lib/lib.webworker.d.ts +15606 -0
  237. package/lib/lib.webworker.importscripts.d.ts +21 -0
  238. package/lib/lib.webworker.iterable.d.ts +18 -0
  239. package/lib/logger.js +144 -0
  240. package/lib/package.json +7 -0
  241. package/lib/tsserver.js +57 -0
  242. package/lib/tsserverlibrary.js +171 -0
  243. package/lib/typesMap.json +497 -0
  244. package/lib/typescript.js +373 -0
  245. package/package.json +115 -0
@@ -0,0 +1,3201 @@
1
+ /**
2
+ * Analyzes process handler functions to produce opcodes for the code generator.
3
+ */
4
+ import ts from 'typescript';
5
+ import { DiagnosticCollector, ProcessErrorCode } from './errors.js';
6
+ import { containsSuspensionPoint, findSuspensionPoints, getPrimitiveCall, isRaceCall as isPrimitiveRaceCall, isSignalCall as isPrimitiveSignalCall, isDelayCall as isPrimitiveDelayCall, isSignalCombinatorCall as isPrimitiveSignalCombinatorCall, isStreamCall as isPrimitiveStreamCall, isScopeCall as isPrimitiveScopeCall, } from './primitive-detector.js';
7
+ /**
8
+ * Check if a function has the async modifier.
9
+ */
10
+ function hasAsyncModifier(node) {
11
+ if (!node.modifiers)
12
+ return false;
13
+ return node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.AsyncKeyword);
14
+ }
15
+ /**
16
+ * Non-deterministic operation patterns that break replay determinism.
17
+ */
18
+ const NON_DETERMINISTIC_PATTERNS = [
19
+ { object: 'Date', method: 'now' },
20
+ { object: 'Math', method: 'random' },
21
+ { object: 'crypto', method: 'randomUUID' },
22
+ { object: 'crypto', method: 'getRandomValues' },
23
+ ];
24
+ /**
25
+ * Check if a call expression is a non-deterministic operation.
26
+ * Detects: Date.now(), Math.random(), crypto.randomUUID(), new Date()
27
+ */
28
+ function isNonDeterministicCall(node) {
29
+ // Check for new Date() without arguments
30
+ if (ts.isNewExpression(node)) {
31
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'Date') {
32
+ // new Date() with no args or new Date() uses current time
33
+ if (!node.arguments || node.arguments.length === 0) {
34
+ return { name: 'new Date()' };
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ // Check for method calls like Date.now(), Math.random()
40
+ if (ts.isCallExpression(node)) {
41
+ const callee = node.expression;
42
+ if (ts.isPropertyAccessExpression(callee)) {
43
+ const obj = callee.expression;
44
+ const method = callee.name.text;
45
+ if (ts.isIdentifier(obj)) {
46
+ const objName = obj.text;
47
+ for (const pattern of NON_DETERMINISTIC_PATTERNS) {
48
+ if (objName === pattern.object && method === pattern.method) {
49
+ return { name: `${objName}.${method}()` };
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Find signal() or delay() calls that are not awaited.
59
+ * Returns the primitive name if found, null otherwise.
60
+ */
61
+ function findUnawaitedPrimitive(expr, ctx) {
62
+ // If the expression is an await, it's fine
63
+ if (ts.isAwaitExpression(expr)) {
64
+ return null;
65
+ }
66
+ // Check if this is a direct call to signal() or delay()
67
+ if (ts.isCallExpression(expr)) {
68
+ if (isWaitForCall(expr, ctx)) {
69
+ return 'signal';
70
+ }
71
+ if (isDelayCall(expr, ctx)) {
72
+ return 'delay';
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /**
78
+ * Recursively check for non-deterministic operations and report them.
79
+ */
80
+ function checkForNonDeterministicOps(node, ctx) {
81
+ const nonDet = isNonDeterministicCall(node);
82
+ if (nonDet) {
83
+ ctx.diagnostics.add(ProcessErrorCode.NonDeterministicOperation, node, nonDet.name);
84
+ }
85
+ // Recursively check children
86
+ ts.forEachChild(node, (child) => checkForNonDeterministicOps(child, ctx));
87
+ }
88
+ /**
89
+ * Recursively check for throw statements and report them.
90
+ * TSP3004: throw statements are not allowed in process handlers.
91
+ */
92
+ function checkForThrowStatements(node, ctx) {
93
+ if (ts.isThrowStatement(node)) {
94
+ ctx.diagnostics.add(ProcessErrorCode.ThrowNotAllowed, node);
95
+ return;
96
+ }
97
+ // Recursively check children
98
+ ts.forEachChild(node, (child) => checkForThrowStatements(child, ctx));
99
+ }
100
+ /**
101
+ * Emit an opcode with optional source node for source map generation.
102
+ */
103
+ function emitOpcode(ctx, opcode, sourceNode) {
104
+ ctx.opcodes.push(opcode);
105
+ ctx.opcodeSourceNodes.push(sourceNode);
106
+ }
107
+ /**
108
+ * Register a variable in ctx.variables, emitting a ShadowedHandlerLocal
109
+ * diagnostic if `name` already maps to a *different* declaration node AND
110
+ * the redeclaration is sequentially nested inside the existing one (i.e.
111
+ * the inner block runs after the outer write, on the same execution path).
112
+ *
113
+ * Skipped (intentionally not flagged):
114
+ * - Function-parameter shadowing — handled by the rewriter's sub-context.
115
+ * - Both old and new are `using` declarations — per-block REHYDRATE opcodes
116
+ * make these independent, no flat-slot conflict.
117
+ * - The name is a race-result var — rewritten to `__raceResult`, not
118
+ * state.vars.{name}.
119
+ * - Mutually-exclusive branches (if/else, switch, ternary) — both
120
+ * declarations write the same slot but only one runs per execution; reads
121
+ * stay within their branch. This is the common pattern in process
122
+ * handlers and isn't a bug.
123
+ */
124
+ function registerVariable(ctx, name, info) {
125
+ const existing = ctx.variables.get(name);
126
+ if (existing &&
127
+ existing.declarationNode !== info.declarationNode &&
128
+ isSequentialNestedShadow(existing.declarationNode, info.declarationNode) &&
129
+ !(existing.isUsing && info.isUsing) &&
130
+ !ctx.raceVars.has(name)) {
131
+ ctx.diagnostics.add(ProcessErrorCode.ShadowedHandlerLocal, info.declarationNode, name);
132
+ }
133
+ ctx.variables.set(name, info);
134
+ }
135
+ /**
136
+ * Return true when `inner` is declared in a Block / for-loop body that is
137
+ * a descendant of `outer`'s enclosing scope WITHOUT a branch boundary
138
+ * (IfStatement, ConditionalExpression, CaseClause, DefaultClause) between
139
+ * them. Such a shadow runs sequentially on the same execution path —
140
+ * `state.vars.{name}` gets overwritten in place.
141
+ *
142
+ * Returns false when inner and outer are in mutually-exclusive sibling
143
+ * branches; both write the same slot but only one runs per execution.
144
+ */
145
+ function isSequentialNestedShadow(outer, inner) {
146
+ // Find each declaration's enclosing scope (Block or function body).
147
+ const outerScope = enclosingScope(outer);
148
+ if (!outerScope)
149
+ return false;
150
+ // Walk inner's ancestors. If we reach outerScope without crossing a
151
+ // branch boundary, it's a sequential nested shadow.
152
+ let cur = inner.parent;
153
+ while (cur) {
154
+ if (cur === outerScope)
155
+ return true;
156
+ if (ts.isIfStatement(cur) ||
157
+ ts.isConditionalExpression(cur) ||
158
+ ts.isCaseClause(cur) ||
159
+ ts.isDefaultClause(cur)) {
160
+ return false;
161
+ }
162
+ cur = cur.parent;
163
+ }
164
+ return false;
165
+ }
166
+ function enclosingScope(node) {
167
+ let cur = node.parent;
168
+ while (cur) {
169
+ if (ts.isBlock(cur) ||
170
+ ts.isSourceFile(cur) ||
171
+ ts.isFunctionDeclaration(cur) ||
172
+ ts.isFunctionExpression(cur) ||
173
+ ts.isArrowFunction(cur) ||
174
+ ts.isMethodDeclaration(cur)) {
175
+ return cur;
176
+ }
177
+ cur = cur.parent;
178
+ }
179
+ return undefined;
180
+ }
181
+ /**
182
+ * Analyze a process handler function.
183
+ */
184
+ export function analyzeHandler(handler, typeChecker) {
185
+ // Check if the handler is a generator function
186
+ // Arrow functions cannot be generators, so we only check FunctionExpression and MethodDeclaration
187
+ const isGenerator = (ts.isFunctionExpression(handler) || ts.isMethodDeclaration(handler)) &&
188
+ handler.asteriskToken !== undefined;
189
+ // TSP1004: Check if handler is async
190
+ const isAsync = hasAsyncModifier(handler);
191
+ const diagnostics = new DiagnosticCollector();
192
+ if (!isAsync) {
193
+ diagnostics.add(ProcessErrorCode.HandlerNotAsync, handler);
194
+ }
195
+ const ctx = {
196
+ typeChecker,
197
+ opcodes: [],
198
+ opcodeSourceNodes: [],
199
+ raceBranchSourceNodes: new Map(),
200
+ blocks: [],
201
+ rehydrationBlocks: {},
202
+ signals: {},
203
+ variables: new Map(),
204
+ raceVars: new Set(),
205
+ diagnostics, // Use the diagnostics collector we created above
206
+ currentBlockStatements: [],
207
+ currentBlockUses: new Set(),
208
+ labelTargets: new Map(),
209
+ pendingLabelPatches: [],
210
+ scopeStack: [],
211
+ nextScopeId: 0,
212
+ loopStack: [],
213
+ inRaceBranch: false,
214
+ nestedSwitchDepth: 0,
215
+ nextLoopId: 0,
216
+ nextParallelId: 0,
217
+ innerFunctions: new Map(),
218
+ inliningStack: [],
219
+ maxInliningDepth: 10,
220
+ isGenerator,
221
+ yields: [],
222
+ nextScopeBlockId: 0,
223
+ subprocesses: [],
224
+ };
225
+ // Get the handler body
226
+ const body = handler.body;
227
+ if (!body) {
228
+ return emptyResult();
229
+ }
230
+ if (ts.isBlock(body)) {
231
+ analyzeStatements(body.statements, ctx);
232
+ }
233
+ else {
234
+ // Arrow function with expression body
235
+ analyzeExpression(body, ctx);
236
+ }
237
+ // Flush any remaining block
238
+ flushBlock(ctx);
239
+ // TSP1011: Check if any registered inner functions escape their scope
240
+ if (ts.isBlock(body)) {
241
+ for (const [name, info] of ctx.innerFunctions) {
242
+ if (info.hasSuspension && checkFunctionEscapes(name, body.statements)) {
243
+ ctx.diagnostics.add(ProcessErrorCode.FunctionEscapesScope, info.node);
244
+ }
245
+ }
246
+ }
247
+ // Patch label references
248
+ patchLabels(ctx);
249
+ return {
250
+ opcodes: ctx.opcodes,
251
+ opcodeSourceNodes: ctx.opcodeSourceNodes,
252
+ raceBranchSourceNodes: ctx.raceBranchSourceNodes,
253
+ blocks: ctx.blocks,
254
+ rehydrationBlocks: ctx.rehydrationBlocks,
255
+ signals: ctx.signals,
256
+ variables: ctx.variables,
257
+ raceVars: ctx.raceVars,
258
+ diagnostics: ctx.diagnostics.getAll(),
259
+ isGenerator: ctx.isGenerator,
260
+ yields: ctx.yields,
261
+ exports: ctx.exports,
262
+ subprocesses: ctx.subprocesses,
263
+ };
264
+ }
265
+ function emptyResult() {
266
+ return {
267
+ opcodes: [],
268
+ opcodeSourceNodes: [],
269
+ raceBranchSourceNodes: new Map(),
270
+ blocks: [],
271
+ rehydrationBlocks: {},
272
+ signals: {},
273
+ variables: new Map(),
274
+ raceVars: new Set(),
275
+ diagnostics: [],
276
+ isGenerator: false,
277
+ yields: [],
278
+ subprocesses: [],
279
+ };
280
+ }
281
+ /**
282
+ * Analyze a list of statements.
283
+ */
284
+ function analyzeStatements(statements, ctx) {
285
+ for (const stmt of statements) {
286
+ analyzeStatement(stmt, ctx);
287
+ }
288
+ }
289
+ /**
290
+ * Analyze a single statement.
291
+ */
292
+ function analyzeStatement(stmt, ctx) {
293
+ // Variable declaration
294
+ if (ts.isVariableStatement(stmt)) {
295
+ analyzeVariableStatement(stmt, ctx);
296
+ return;
297
+ }
298
+ // Expression statement (might contain await)
299
+ if (ts.isExpressionStatement(stmt)) {
300
+ // TSP1002: Check for signal() or delay() not awaited
301
+ const unawaitedPrimitive = findUnawaitedPrimitive(stmt.expression, ctx);
302
+ if (unawaitedPrimitive) {
303
+ ctx.diagnostics.add(ProcessErrorCode.SignalNotAwaited, stmt, unawaitedPrimitive);
304
+ }
305
+ // TSP1005: Check for non-deterministic operations
306
+ checkForNonDeterministicOps(stmt.expression, ctx);
307
+ // TSP1008: Check for nested async functions with suspension passed as arguments
308
+ checkForNestedAsyncWithSuspension(stmt.expression, ctx);
309
+ // Check for direct suspension points OR inner function calls
310
+ if (containsSuspension(stmt.expression, ctx) || containsInnerFunctionCall(stmt.expression, ctx)) {
311
+ flushBlock(ctx);
312
+ analyzeExpression(stmt.expression, ctx);
313
+ }
314
+ else {
315
+ ctx.currentBlockStatements.push(stmt);
316
+ trackUsedVariables(stmt, ctx);
317
+ }
318
+ return;
319
+ }
320
+ // Return statement
321
+ if (ts.isReturnStatement(stmt)) {
322
+ flushBlock(ctx);
323
+ // TSP1005: Check for non-deterministic operations in the return expression
324
+ if (stmt.expression) {
325
+ checkForNonDeterministicOps(stmt.expression, ctx);
326
+ }
327
+ if (stmt.expression && containsSuspension(stmt.expression, ctx)) {
328
+ analyzeExpression(stmt.expression, ctx);
329
+ }
330
+ // Create a block to evaluate the return value (or undefined for empty return)
331
+ // The block executes the return statement which returns the value
332
+ const blockId = createBlock(ctx, [stmt]);
333
+ emitOpcode(ctx, { op: 'BLOCK', blockId }, stmt);
334
+ // RETURN opcode signals process completion (block result is the return value)
335
+ emitOpcode(ctx, { op: 'RETURN', value: undefined }, stmt);
336
+ return;
337
+ }
338
+ // While loop
339
+ if (ts.isWhileStatement(stmt)) {
340
+ analyzeWhileStatement(stmt, ctx);
341
+ return;
342
+ }
343
+ // Do-while loop - reject suspension points
344
+ if (ts.isDoStatement(stmt)) {
345
+ const hasSuspension = containsSuspensionInStatement(stmt.statement, ctx)
346
+ || containsSuspension(stmt.expression, ctx);
347
+ if (hasSuspension) {
348
+ ctx.diagnostics.add(ProcessErrorCode.DoWhileWithSuspension, stmt);
349
+ }
350
+ ctx.currentBlockStatements.push(stmt);
351
+ trackUsedVariables(stmt, ctx);
352
+ return;
353
+ }
354
+ // If statement
355
+ if (ts.isIfStatement(stmt)) {
356
+ analyzeIfStatement(stmt, ctx);
357
+ return;
358
+ }
359
+ // Switch statement (might be a race)
360
+ if (ts.isSwitchStatement(stmt)) {
361
+ analyzeSwitchStatement(stmt, ctx);
362
+ return;
363
+ }
364
+ // Try statement - check for suspension points (not supported)
365
+ if (ts.isTryStatement(stmt)) {
366
+ if (containsSuspensionInBlock(stmt.tryBlock, ctx)) {
367
+ ctx.diagnostics.add(ProcessErrorCode.TryCatchWithSuspension, stmt);
368
+ }
369
+ // Still add to block (will fail at runtime but compile for better error messages)
370
+ ctx.currentBlockStatements.push(stmt);
371
+ trackUsedVariables(stmt, ctx);
372
+ return;
373
+ }
374
+ // For-of statement - support durable iteration with suspension points
375
+ if (ts.isForOfStatement(stmt)) {
376
+ const hasSuspension = containsSuspensionInStatement(stmt.statement, ctx);
377
+ if (hasSuspension) {
378
+ // TSP3001/TSP3002: Validate durable iterator if the iterable has the brand
379
+ checkDurableIteratorType(stmt.expression, ctx);
380
+ // Durable iteration: generate ITER_* opcodes
381
+ const loopId = ctx.nextLoopId++;
382
+ const cursorVar = `__cursor_${loopId}`;
383
+ // Extract the loop variable name and check if it needs `using`.
384
+ // The binding spans the entire body; if the body suspends (which it does
385
+ // here, by definition - hasSuspension is true), a non-JSON item type
386
+ // cannot be rehydrated from `const`.
387
+ let itemVar = '__item';
388
+ let itemIsUsing = false;
389
+ let itemIsSerializable = true;
390
+ if (ts.isVariableDeclarationList(stmt.initializer)) {
391
+ const decl = stmt.initializer.declarations[0];
392
+ if (ts.isIdentifier(decl.name)) {
393
+ itemVar = decl.name.text;
394
+ }
395
+ itemIsUsing = (stmt.initializer.flags & ts.NodeFlags.Using) !== 0;
396
+ // Only flag when we have enough type information to prove non-JSON.
397
+ // With `any`/`unknown`, the type checker has nothing to say - stay silent.
398
+ if (!itemIsUsing &&
399
+ !hasNoTypeInformation(decl, ctx.typeChecker) &&
400
+ !isJsonSerializable(decl, ctx.typeChecker)) {
401
+ ctx.diagnostics.add(ProcessErrorCode.NonSerializableConst, decl, itemVar);
402
+ }
403
+ itemIsSerializable = isSerializableType(decl, ctx.typeChecker);
404
+ }
405
+ // Flush any pending block before the loop
406
+ flushBlock(ctx);
407
+ // Generate loop start label
408
+ const startLabelName = `__loop_${loopId}_start`;
409
+ const endLabelName = `__loop_${loopId}_end`;
410
+ // LABEL for loop start (for continue)
411
+ ctx.labelTargets.set(startLabelName, ctx.opcodes.length);
412
+ emitOpcode(ctx, { op: 'LABEL', label: startLabelName }, stmt);
413
+ // ITER_START - initializes iterator, sets up cursor var
414
+ emitOpcode(ctx, {
415
+ op: 'ITER_START',
416
+ iterableExpr: stmt.expression,
417
+ cursorVar,
418
+ itemVar,
419
+ loopId,
420
+ }, stmt);
421
+ // ITER_NEXT - fetches next item, jumps to done if exhausted
422
+ const iterNextOpcode = {
423
+ op: 'ITER_NEXT',
424
+ cursorVar,
425
+ doneTarget: -1, // Will be patched
426
+ loopId,
427
+ };
428
+ emitOpcode(ctx, iterNextOpcode, stmt);
429
+ // Push loop onto stack for break/continue handling
430
+ ctx.loopStack.push({ startLabel: startLabelName, endLabel: endLabelName });
431
+ // Track the item variable
432
+ ctx.variables.set(itemVar, {
433
+ name: itemVar,
434
+ isUsing: itemIsUsing,
435
+ isSerializable: itemIsSerializable,
436
+ declarationNode: stmt.initializer,
437
+ usedInBlocks: [],
438
+ });
439
+ // ITER_SAVE - save cursor before any suspension points in loop body
440
+ emitOpcode(ctx, { op: 'ITER_SAVE', cursorVar, loopId }, stmt.statement);
441
+ // Analyze the loop body
442
+ analyzeStatement(stmt.statement, ctx);
443
+ // Flush loop body block
444
+ flushBlock(ctx);
445
+ // Jump back to start
446
+ emitOpcode(ctx, { op: 'JUMP', target: ctx.labelTargets.get(startLabelName) }, stmt);
447
+ // Pop loop from stack
448
+ ctx.loopStack.pop();
449
+ // LABEL for loop end (for break and done)
450
+ ctx.labelTargets.set(endLabelName, ctx.opcodes.length);
451
+ emitOpcode(ctx, { op: 'LABEL', label: endLabelName }, stmt);
452
+ iterNextOpcode.doneTarget = ctx.labelTargets.get(endLabelName);
453
+ return;
454
+ }
455
+ // No suspension - add to block as regular statement
456
+ ctx.currentBlockStatements.push(stmt);
457
+ trackUsedVariables(stmt, ctx);
458
+ return;
459
+ }
460
+ // For-in statement - check for suspension points
461
+ if (ts.isForInStatement(stmt)) {
462
+ if (containsSuspensionInStatement(stmt.statement, ctx)) {
463
+ ctx.diagnostics.add(ProcessErrorCode.ForInWithSuspension, stmt);
464
+ }
465
+ ctx.currentBlockStatements.push(stmt);
466
+ trackUsedVariables(stmt, ctx);
467
+ return;
468
+ }
469
+ // Regular for statement - reject suspension points
470
+ if (ts.isForStatement(stmt)) {
471
+ const hasSuspension = containsSuspensionInStatement(stmt.statement, ctx);
472
+ if (hasSuspension) {
473
+ ctx.diagnostics.add(ProcessErrorCode.ForWithSuspension, stmt);
474
+ }
475
+ // Add to block as regular statement (no durable support for classic for)
476
+ ctx.currentBlockStatements.push(stmt);
477
+ trackUsedVariables(stmt, ctx);
478
+ return;
479
+ }
480
+ // Labeled statement (for break/continue targets and observability)
481
+ if (ts.isLabeledStatement(stmt)) {
482
+ const labelName = stmt.label.text;
483
+ flushBlock(ctx);
484
+ // LABEL opcode for jump targets (break/continue)
485
+ ctx.labelTargets.set(labelName, ctx.opcodes.length);
486
+ emitOpcode(ctx, { op: 'LABEL', label: labelName }, stmt);
487
+ // LABEL_ENTER for observability tracking
488
+ emitOpcode(ctx, { op: 'LABEL_ENTER', label: labelName }, stmt);
489
+ // Analyze the inner statement
490
+ analyzeStatement(stmt.statement, ctx);
491
+ // Flush before exit
492
+ flushBlock(ctx);
493
+ // LABEL_EXIT for observability tracking
494
+ emitOpcode(ctx, { op: 'LABEL_EXIT', label: labelName }, stmt);
495
+ return;
496
+ }
497
+ // Break statement
498
+ if (ts.isBreakStatement(stmt)) {
499
+ flushBlock(ctx);
500
+ const label = stmt.label?.text;
501
+ if (label) {
502
+ // Labeled break - jump to the named label
503
+ const jumpOp = { op: 'JUMP', target: -1 };
504
+ ctx.pendingLabelPatches.push({ opcode: jumpOp, label, field: 'target' });
505
+ emitOpcode(ctx, jumpOp, stmt);
506
+ }
507
+ else if (ctx.inRaceBranch && ctx.nestedSwitchDepth === 0) {
508
+ // Unlabeled break inside race switch (not nested switch) - don't emit JUMP.
509
+ // The case's nextStep will handle continuation to the loop-back step.
510
+ // This is equivalent to the switch case ending normally.
511
+ // Note: breaks in nested switches are added to the block as normal statements.
512
+ }
513
+ else if (ctx.loopStack.length > 0) {
514
+ // Unlabeled break in a loop - jump to current loop end
515
+ const currentLoop = ctx.loopStack[ctx.loopStack.length - 1];
516
+ const jumpOp = { op: 'JUMP', target: -1 };
517
+ ctx.pendingLabelPatches.push({ opcode: jumpOp, label: currentLoop.endLabel, field: 'target' });
518
+ emitOpcode(ctx, jumpOp, stmt);
519
+ }
520
+ return;
521
+ }
522
+ // Continue statement
523
+ if (ts.isContinueStatement(stmt)) {
524
+ flushBlock(ctx);
525
+ const label = stmt.label?.text;
526
+ if (label) {
527
+ const jumpOp = { op: 'JUMP', target: -1 };
528
+ ctx.pendingLabelPatches.push({ opcode: jumpOp, label, field: 'target' });
529
+ emitOpcode(ctx, jumpOp, stmt);
530
+ }
531
+ else if (ctx.loopStack.length > 0) {
532
+ // Unlabeled continue - jump to current loop start
533
+ const currentLoop = ctx.loopStack[ctx.loopStack.length - 1];
534
+ const jumpOp = { op: 'JUMP', target: -1 };
535
+ ctx.pendingLabelPatches.push({ opcode: jumpOp, label: currentLoop.startLabel, field: 'target' });
536
+ emitOpcode(ctx, jumpOp, stmt);
537
+ }
538
+ return;
539
+ }
540
+ // TSP3004: Throw statement - not allowed in process handlers
541
+ if (ts.isThrowStatement(stmt)) {
542
+ ctx.diagnostics.add(ProcessErrorCode.ThrowNotAllowed, stmt);
543
+ // Still add to block for better error messages
544
+ ctx.currentBlockStatements.push(stmt);
545
+ return;
546
+ }
547
+ // Block statement
548
+ if (ts.isBlock(stmt)) {
549
+ ctx.scopeStack.push(ctx.nextScopeId++);
550
+ emitOpcode(ctx, { op: 'SCOPE_ENTER', scopeId: ctx.scopeStack[ctx.scopeStack.length - 1] }, stmt);
551
+ analyzeStatements(stmt.statements, ctx);
552
+ flushBlock(ctx);
553
+ emitOpcode(ctx, { op: 'SCOPE_EXIT', scopeId: ctx.scopeStack.pop() }, stmt);
554
+ return;
555
+ }
556
+ // Function declaration - register inner functions with suspension points
557
+ if (ts.isFunctionDeclaration(stmt)) {
558
+ const isAsync = stmt.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
559
+ if (isAsync && stmt.name && stmt.body) {
560
+ const hasSuspension = containsSuspensionInBlock(stmt.body, ctx);
561
+ if (hasSuspension) {
562
+ const funcName = stmt.name.text;
563
+ // Register the inner function for potential inlining
564
+ registerInnerFunction(funcName, stmt, ctx);
565
+ // Track as a variable (for reference tracking)
566
+ ctx.variables.set(funcName, {
567
+ name: funcName,
568
+ isUsing: false,
569
+ isSerializable: false,
570
+ declarationNode: stmt,
571
+ usedInBlocks: [],
572
+ });
573
+ // Don't add to block statements - function will be inlined at call site
574
+ return;
575
+ }
576
+ }
577
+ // Non-async or no suspension points - add to block as-is
578
+ ctx.currentBlockStatements.push(stmt);
579
+ trackUsedVariables(stmt, ctx);
580
+ return;
581
+ }
582
+ // Default: add to current block
583
+ ctx.currentBlockStatements.push(stmt);
584
+ trackUsedVariables(stmt, ctx);
585
+ }
586
+ /**
587
+ * Analyze a variable statement (const/let/using).
588
+ */
589
+ function analyzeVariableStatement(stmt, ctx) {
590
+ const declarations = stmt.declarationList.declarations;
591
+ for (const decl of declarations) {
592
+ const initializer = decl.initializer;
593
+ // Check for Promise.all/Promise.race with signals (handles both simple and destructuring patterns)
594
+ if (initializer && ts.isAwaitExpression(initializer)) {
595
+ const promiseCombinator = checkPromiseCombinatorWithSignals(initializer.expression, ctx);
596
+ if (promiseCombinator) {
597
+ ctx.diagnostics.add(ProcessErrorCode.PromiseCombinatorWithSignal, initializer, promiseCombinator);
598
+ }
599
+ }
600
+ // TSP1005: Check for non-deterministic operations in initializer
601
+ if (initializer) {
602
+ checkForNonDeterministicOps(initializer, ctx);
603
+ }
604
+ // Check for inner async function with suspension points - register for inlining
605
+ if (initializer &&
606
+ ts.isIdentifier(decl.name) &&
607
+ (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
608
+ const isAsync = initializer.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
609
+ if (isAsync) {
610
+ const hasSuspension = ts.isBlock(initializer.body)
611
+ ? containsSuspensionInBlock(initializer.body, ctx)
612
+ : containsSuspension(initializer.body, ctx);
613
+ if (hasSuspension) {
614
+ const funcName = decl.name.text;
615
+ // Register the inner function for potential inlining
616
+ registerInnerFunction(funcName, initializer, ctx);
617
+ // Track as a variable (for reference tracking)
618
+ ctx.variables.set(funcName, {
619
+ name: funcName,
620
+ isUsing: false,
621
+ isSerializable: false,
622
+ declarationNode: decl,
623
+ usedInBlocks: [],
624
+ });
625
+ // Don't add to block statements - function will be inlined at call site
626
+ continue;
627
+ }
628
+ }
629
+ }
630
+ // Check for createSubProcess({ name, path, handler }) declarations
631
+ if (initializer &&
632
+ ts.isIdentifier(decl.name) &&
633
+ ts.isCallExpression(initializer) &&
634
+ ts.isIdentifier(initializer.expression) &&
635
+ initializer.expression.text === 'createSubProcess') {
636
+ const subInfo = extractSubProcessInfo(initializer, decl.name.text, ctx);
637
+ if (subInfo) {
638
+ ctx.subprocesses.push(subInfo);
639
+ // Track the subprocess variable name so call sites can reference it
640
+ ctx.variables.set(decl.name.text, {
641
+ name: decl.name.text,
642
+ isUsing: false,
643
+ isSerializable: false,
644
+ declarationNode: decl,
645
+ usedInBlocks: [],
646
+ });
647
+ // Don't add to block statements - subprocess is compiled separately
648
+ continue;
649
+ }
650
+ }
651
+ // Handle destructuring patterns - both primitive-suspension and service-call await paths.
652
+ if (!ts.isIdentifier(decl.name)) {
653
+ // Detect `const { a, b } = await svc.x()` / `const [a, b] = await svc.x()`.
654
+ // Must be an await on a plain service call - NOT a signal combinator (signal.all)
655
+ // or a scope call, which have their own dedicated emit paths in analyzeAwaitExpression.
656
+ const isDestructureServiceAwait = (() => {
657
+ if (!initializer || !ts.isAwaitExpression(initializer))
658
+ return false;
659
+ if (!(ts.isObjectBindingPattern(decl.name) || ts.isArrayBindingPattern(decl.name)))
660
+ return false;
661
+ const inner = initializer.expression;
662
+ if (!ts.isCallExpression(inner))
663
+ return false;
664
+ if (isWaitForCall(inner, ctx) || isDelayCall(inner, ctx) || isRaceCall(inner, ctx))
665
+ return false;
666
+ if (isSignalCombinatorCall(inner, ctx))
667
+ return false;
668
+ if (isScopeCall(inner, ctx))
669
+ return false;
670
+ return true;
671
+ })();
672
+ if (isDestructureServiceAwait) {
673
+ // `const { a, b } = await svc.x()` or `const [a, b] = await svc.x()` inside
674
+ // a race branch. Without special handling the entire await call is dropped and
675
+ // every destructured name stays undefined.
676
+ //
677
+ // Strategy: emit a BLOCK whose body is:
678
+ // __blockResult = await svc.x()
679
+ // a = __blockResult.a // object: property access
680
+ // a = __blockResult[0] // array: index access
681
+ // Because a/b are registered in ctx.variables (and therefore in localVars),
682
+ // the rewriter turns `a = __blockResult.a` into
683
+ // `state.vars.a = __blockResult.a` - exactly what we need.
684
+ flushBlock(ctx);
685
+ const bindingPattern = decl.name;
686
+ const isObject = ts.isObjectBindingPattern(bindingPattern);
687
+ // Build the synthetic `__blockResult = await svc.x()` statement.
688
+ const syntheticAssignment = ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createIdentifier('__blockResult'), ts.factory.createToken(ts.SyntaxKind.EqualsToken), initializer));
689
+ ts.setTextRange(syntheticAssignment, initializer);
690
+ // Build per-name extract statements and register each name.
691
+ const extractStatements = [];
692
+ const elements = bindingPattern.elements;
693
+ for (let idx = 0; idx < elements.length; idx++) {
694
+ const elem = elements[idx];
695
+ if (ts.isOmittedExpression(elem))
696
+ continue;
697
+ if (!ts.isIdentifier(elem.name))
698
+ continue; // skip nested patterns (deferred)
699
+ const localName = elem.name.text;
700
+ // Determine the access expression for this element:
701
+ // object: __blockResult['propName'] (string-literal bracket access avoids
702
+ // the rewriter mis-treating the prop identifier as a localVar)
703
+ // array: __blockResult[idx]
704
+ let accessExpr;
705
+ if (isObject) {
706
+ const propName = elem.propertyName
707
+ ? (ts.isIdentifier(elem.propertyName) ? elem.propertyName.text : localName)
708
+ : localName;
709
+ // Use bracket notation with a string literal so the rewriter never
710
+ // mistakes the property name for a localVar and double-rewrites it.
711
+ accessExpr = ts.factory.createElementAccessExpression(ts.factory.createIdentifier('__blockResult'), ts.factory.createStringLiteral(propName));
712
+ }
713
+ else {
714
+ accessExpr = ts.factory.createElementAccessExpression(ts.factory.createIdentifier('__blockResult'), ts.factory.createNumericLiteral(idx));
715
+ }
716
+ // `localName = accessExpr` - the rewriter will turn localName -> state.vars.localName
717
+ extractStatements.push(ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createIdentifier(localName), ts.factory.createToken(ts.SyntaxKind.EqualsToken), accessExpr)));
718
+ // Register in ctx.variables so the rewriter includes the name in localVars.
719
+ ctx.variables.set(localName, {
720
+ name: localName,
721
+ isUsing: false,
722
+ isSerializable: true,
723
+ declarationNode: decl,
724
+ usedInBlocks: [],
725
+ });
726
+ }
727
+ // Collect using-var dependencies from the await expression.
728
+ trackUsedVariables(syntheticAssignment, ctx);
729
+ const blockUses = Array.from(ctx.currentBlockUses);
730
+ ctx.currentBlockUses = new Set();
731
+ const blockStatements = [syntheticAssignment, ...extractStatements];
732
+ const blockId = createBlock(ctx, blockStatements, blockUses);
733
+ emitOpcode(ctx, { op: 'BLOCK', blockId }, initializer);
734
+ // No STORE: each name's assignment is inlined in the block body itself.
735
+ continue;
736
+ }
737
+ // Destructuring pattern: const { a, b } = await signal(...)
738
+ if (initializer && containsSuspension(initializer, ctx)) {
739
+ flushBlock(ctx);
740
+ // Generate temp variable name for the awaited result
741
+ const tempVar = `__destructure_${ctx.nextScopeId++}`;
742
+ analyzeAwaitExpression(initializer, tempVar, ctx);
743
+ }
744
+ continue;
745
+ }
746
+ const varName = decl.name.text;
747
+ // Check if this is a `using` declaration
748
+ const isUsing = (stmt.declarationList.flags & ts.NodeFlags.Using) !== 0;
749
+ // Early exit: `const alice = await child('alice')` where `child` is a
750
+ // subprocess variable. The result SubRef is JSON-serializable so neither
751
+ // hasSuspension nor the !isJsonSerializable guard would catch it - we
752
+ // must intercept here before the statement lands in a regular BLOCK.
753
+ if (initializer && ts.isAwaitExpression(initializer)) {
754
+ const inner = initializer.expression;
755
+ if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression)) {
756
+ const calleeName = inner.expression.text;
757
+ if (ctx.subprocesses.find(s => s.varName === calleeName)) {
758
+ flushBlock(ctx);
759
+ trySubprocessSpawn(inner, varName, ctx, true);
760
+ registerVariable(ctx, varName, {
761
+ name: varName,
762
+ isUsing: false,
763
+ isSerializable: true,
764
+ declarationNode: decl,
765
+ usedInBlocks: [],
766
+ });
767
+ continue;
768
+ }
769
+ }
770
+ }
771
+ // Check if initializer contains a suspension point
772
+ const hasSuspension = initializer && containsSuspension(initializer, ctx);
773
+ // Check if initializer is an await on a service call (not signal/delay)
774
+ const isServiceAwait = initializer && isAwaitOnServiceCall(initializer, ctx);
775
+ if (varName === 'exports' && initializer && ts.isObjectLiteralExpression(initializer)) {
776
+ // `[const|using] exports = { ... }` - process exports declaration
777
+ // This is a regular persisted var, NOT a using/rehydration var.
778
+ // The rewriter transforms exports.foo -> state.vars.exports.foo like any other local var.
779
+ const fields = [];
780
+ const methods = [];
781
+ for (const prop of initializer.properties) {
782
+ if (ts.isMethodDeclaration(prop) && ts.isIdentifier(prop.name)) {
783
+ methods.push({ name: prop.name.text, node: prop });
784
+ }
785
+ else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
786
+ if (prop.initializer && (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
787
+ methods.push({ name: prop.name.text, node: prop });
788
+ }
789
+ else {
790
+ fields.push({ name: prop.name.text, node: prop });
791
+ }
792
+ }
793
+ else if (ts.isShorthandPropertyAssignment(prop)) {
794
+ fields.push({ name: prop.name.text, node: prop });
795
+ }
796
+ }
797
+ ctx.exports = { fields, methods, declarationNode: decl };
798
+ // Track as a regular serializable variable - NOT isUsing, NOT rehydration
799
+ registerVariable(ctx, varName, {
800
+ name: varName,
801
+ isUsing: false,
802
+ isSerializable: true,
803
+ declarationNode: decl,
804
+ usedInBlocks: [],
805
+ });
806
+ // Add the statement to the current block - it's a normal variable assignment
807
+ ctx.currentBlockStatements.push(stmt);
808
+ trackUsedVariables(stmt, ctx);
809
+ }
810
+ else if (isUsing && initializer) {
811
+ // `using` declarations become rehydration blocks
812
+ flushBlock(ctx);
813
+ // Track the variable
814
+ registerVariable(ctx, varName, {
815
+ name: varName,
816
+ isUsing: true,
817
+ isSerializable: false,
818
+ declarationNode: decl,
819
+ usedInBlocks: [],
820
+ });
821
+ // Create rehydration block
822
+ ctx.rehydrationBlocks[varName] = {
823
+ deps: extractDependencies(initializer, ctx),
824
+ expression: initializer,
825
+ };
826
+ // Emit REHYDRATE opcode (carries the expression so each opcode site has its own
827
+ // initializer independent of the global rehydrationBlocks map)
828
+ emitOpcode(ctx, { op: 'REHYDRATE', var: varName, blockId: varName, expression: initializer }, stmt);
829
+ }
830
+ else if (isServiceAwait && !isUsing && !isJsonSerializable(decl, ctx.typeChecker)) {
831
+ // Awaited service call returning a non-JSON value. It only *needs* `using`
832
+ // if the value must survive a suspension (signal/delay/race/yield). When
833
+ // the variable is never read after any suspension in its enclosing scope,
834
+ // `const` is safe - the value lives entirely within one continuation.
835
+ if (isVarReadAfterSuspension(decl, varName, ctx)) {
836
+ ctx.diagnostics.add(ProcessErrorCode.NonSerializableConst, decl, varName);
837
+ }
838
+ // Still process it so we can continue analyzing
839
+ flushBlock(ctx);
840
+ analyzeAwaitExpression(initializer, varName, ctx);
841
+ // Track the variable for rewriting in subsequent blocks
842
+ registerVariable(ctx, varName, {
843
+ name: varName,
844
+ isUsing: false,
845
+ isSerializable: isSerializableType(decl, ctx.typeChecker),
846
+ declarationNode: decl,
847
+ usedInBlocks: [],
848
+ });
849
+ }
850
+ else if (hasSuspension && initializer) {
851
+ // Suspending initializer (e.g., const x = await signal(...))
852
+ flushBlock(ctx);
853
+ analyzeAwaitExpression(initializer, varName, ctx);
854
+ // Track the variable for rewriting in subsequent blocks
855
+ registerVariable(ctx, varName, {
856
+ name: varName,
857
+ isUsing: false,
858
+ isSerializable: isSerializableType(decl, ctx.typeChecker),
859
+ declarationNode: decl,
860
+ usedInBlocks: [],
861
+ });
862
+ }
863
+ else {
864
+ // Check for invalid pattern: storing signal/delay in a variable
865
+ // e.g., const s = signal(orders.paid) - should be: await signal(...)
866
+ if (initializer && isStoredSignalOrDelay(initializer)) {
867
+ const funcName = getSignalOrDelayName(initializer);
868
+ ctx.diagnostics.add(ProcessErrorCode.SignalStoredInVariable, decl, funcName);
869
+ }
870
+ // Regular variable - add to current block
871
+ ctx.currentBlockStatements.push(stmt);
872
+ trackUsedVariables(stmt, ctx);
873
+ registerVariable(ctx, varName, {
874
+ name: varName,
875
+ isUsing: false,
876
+ isSerializable: isSerializableType(decl, ctx.typeChecker),
877
+ declarationNode: decl,
878
+ usedInBlocks: [],
879
+ });
880
+ }
881
+ }
882
+ }
883
+ /**
884
+ * Check if expression is a direct signal() or delay.xxx() call (not awaited, not race).
885
+ * These must be used directly with await or in switch cases, not stored.
886
+ */
887
+ function isStoredSignalOrDelay(expr) {
888
+ if (!ts.isCallExpression(expr))
889
+ return false;
890
+ const callExpr = expr.expression;
891
+ // Check for direct identifier: signal(...)
892
+ if (ts.isIdentifier(callExpr)) {
893
+ const name = callExpr.text;
894
+ // signal() should not be stored - race() is allowed
895
+ return name === 'signal';
896
+ }
897
+ // Check for property access: delay.minutes(...), delay.hours(...), etc.
898
+ if (ts.isPropertyAccessExpression(callExpr)) {
899
+ const obj = callExpr.expression;
900
+ if (ts.isIdentifier(obj) && obj.text === 'delay') {
901
+ const methodName = callExpr.name.text;
902
+ return ['seconds', 'minutes', 'hours', 'days'].includes(methodName);
903
+ }
904
+ }
905
+ return false;
906
+ }
907
+ /**
908
+ * Get the function name from a signal/delay call for error messages.
909
+ */
910
+ function getSignalOrDelayName(expr) {
911
+ if (ts.isCallExpression(expr)) {
912
+ const callExpr = expr.expression;
913
+ if (ts.isIdentifier(callExpr)) {
914
+ return callExpr.text;
915
+ }
916
+ // Handle delay.minutes(), delay.hours(), etc.
917
+ if (ts.isPropertyAccessExpression(callExpr)) {
918
+ const obj = callExpr.expression;
919
+ if (ts.isIdentifier(obj) && obj.text === 'delay') {
920
+ return `delay.${callExpr.name.text}`;
921
+ }
922
+ }
923
+ }
924
+ return 'signal';
925
+ }
926
+ /**
927
+ * Check if an expression is an await on a service call (not signal/delay/race).
928
+ * Service calls return non-serializable objects that need rehydration.
929
+ */
930
+ function isAwaitOnServiceCall(expr, ctx) {
931
+ if (!ts.isAwaitExpression(expr))
932
+ return false;
933
+ const inner = expr.expression;
934
+ if (!ts.isCallExpression(inner))
935
+ return false;
936
+ // If it's signal/waitFor/delay/race, it's not a service call
937
+ if (isWaitForCall(inner, ctx) || isDelayCall(inner, ctx) || isRaceCall(inner, ctx)) {
938
+ return false;
939
+ }
940
+ // It's an await on something that isn't a process primitive - likely a service call
941
+ return true;
942
+ }
943
+ /**
944
+ * Return the list of statements a container node holds, or undefined if the
945
+ * container isn't a supported statement container.
946
+ */
947
+ function getContainerStatements(container) {
948
+ if (!container)
949
+ return undefined;
950
+ if (ts.isBlock(container))
951
+ return container.statements;
952
+ if (ts.isSourceFile(container))
953
+ return container.statements;
954
+ if (ts.isCaseClause(container) || ts.isDefaultClause(container))
955
+ return container.statements;
956
+ if (ts.isModuleBlock(container))
957
+ return container.statements;
958
+ return undefined;
959
+ }
960
+ /**
961
+ * Detect whether a switch statement is a race-switch (cases are signal/delay primitives).
962
+ */
963
+ function isRaceSwitchNode(node, ctx) {
964
+ for (const clause of node.caseBlock.clauses) {
965
+ if (!ts.isCaseClause(clause))
966
+ continue;
967
+ const expr = clause.expression;
968
+ if (!ts.isCallExpression(expr))
969
+ continue;
970
+ const prim = getPrimitiveCall(expr, ctx.typeChecker);
971
+ if (prim && (prim.kind === 'signal' || prim.kind === 'delay'))
972
+ return true;
973
+ }
974
+ return false;
975
+ }
976
+ /**
977
+ * Walk forward lexically from a declaration, tracking whether any suspension
978
+ * point (signal/delay/race/yield) occurs before a subsequent read of the variable.
979
+ *
980
+ * Returns true if at least one read of `varName` happens AFTER a suspension -
981
+ * i.e., the value must survive state serialization and the declaration needs `using`.
982
+ * Returns false if all reads happen before any suspension, meaning the value
983
+ * never crosses a persistence boundary and `const` is safe.
984
+ *
985
+ * Conservative default: returns true when the container shape is unfamiliar
986
+ * (e.g., declaration sits in a ForOfStatement binding) so we keep the strict
987
+ * check for patterns we don't analyze.
988
+ */
989
+ function isVarReadAfterSuspension(decl, varName, ctx) {
990
+ // Walk up to the enclosing VariableStatement.
991
+ let stmtNode = decl;
992
+ while (stmtNode.parent && !ts.isVariableStatement(stmtNode)) {
993
+ stmtNode = stmtNode.parent;
994
+ }
995
+ if (!ts.isVariableStatement(stmtNode))
996
+ return true;
997
+ const container = stmtNode.parent;
998
+ const siblings = getContainerStatements(container);
999
+ if (!siblings)
1000
+ return true;
1001
+ const idx = siblings.indexOf(stmtNode);
1002
+ if (idx < 0)
1003
+ return true;
1004
+ let seenSuspension = false;
1005
+ let foundRead = false;
1006
+ const visit = (node) => {
1007
+ if (foundRead)
1008
+ return;
1009
+ // Nested function bodies have their own execution model - skip.
1010
+ if (ts.isFunctionExpression(node) ||
1011
+ ts.isArrowFunction(node) ||
1012
+ ts.isFunctionDeclaration(node)) {
1013
+ return;
1014
+ }
1015
+ // Read of our variable?
1016
+ if (ts.isIdentifier(node) && node.text === varName) {
1017
+ const parent = node.parent;
1018
+ const isDeclName = (ts.isVariableDeclaration(parent) && parent.name === node) ||
1019
+ (ts.isParameter(parent) && parent.name === node) ||
1020
+ (ts.isBindingElement(parent) && parent.name === node);
1021
+ if (!isDeclName && seenSuspension) {
1022
+ foundRead = true;
1023
+ return;
1024
+ }
1025
+ }
1026
+ // Race switch: discriminant and case labels run before suspension;
1027
+ // case bodies run after.
1028
+ if (ts.isSwitchStatement(node) && isRaceSwitchNode(node, ctx)) {
1029
+ visit(node.expression);
1030
+ for (const clause of node.caseBlock.clauses) {
1031
+ if (ts.isCaseClause(clause))
1032
+ visit(clause.expression);
1033
+ }
1034
+ seenSuspension = true;
1035
+ for (const clause of node.caseBlock.clauses) {
1036
+ for (const s of clause.statements)
1037
+ visit(s);
1038
+ }
1039
+ return;
1040
+ }
1041
+ // Detect whether this node is itself a suspension point.
1042
+ // Children are evaluated before the suspension effect, so visit them first.
1043
+ let suspends = false;
1044
+ if (ts.isAwaitExpression(node) && ts.isCallExpression(node.expression)) {
1045
+ const prim = getPrimitiveCall(node.expression, ctx.typeChecker);
1046
+ if (prim && (prim.kind === 'signal' || prim.kind === 'delay')) {
1047
+ suspends = true;
1048
+ }
1049
+ }
1050
+ if (ts.isYieldExpression(node))
1051
+ suspends = true;
1052
+ ts.forEachChild(node, visit);
1053
+ if (suspends)
1054
+ seenSuspension = true;
1055
+ };
1056
+ for (let i = idx + 1; i < siblings.length; i++) {
1057
+ visit(siblings[i]);
1058
+ if (foundRead)
1059
+ return true;
1060
+ }
1061
+ return false;
1062
+ }
1063
+ /**
1064
+ * Analyze an expression that might contain suspension points.
1065
+ */
1066
+ function analyzeExpression(expr, ctx) {
1067
+ if (ts.isAwaitExpression(expr)) {
1068
+ analyzeAwaitExpression(expr, undefined, ctx);
1069
+ return;
1070
+ }
1071
+ if (ts.isYieldExpression(expr)) {
1072
+ analyzeYieldExpression(expr, ctx);
1073
+ return;
1074
+ }
1075
+ if (ts.isCallExpression(expr)) {
1076
+ // Check for race() call
1077
+ if (isRaceCall(expr, ctx)) {
1078
+ // Race is handled at the switch statement level
1079
+ return;
1080
+ }
1081
+ }
1082
+ // Non-suspending expression - should have been caught earlier
1083
+ }
1084
+ /**
1085
+ * Analyze a yield expression.
1086
+ * Yields emit events without suspending the process.
1087
+ */
1088
+ function analyzeYieldExpression(expr, ctx) {
1089
+ // Check if we're in a generator context
1090
+ if (!ctx.isGenerator) {
1091
+ ctx.diagnostics.add(ProcessErrorCode.YieldInNonGenerator, expr);
1092
+ return;
1093
+ }
1094
+ // Get the yielded value expression (or undefined for bare yield)
1095
+ const valueExpr = expr.expression;
1096
+ if (valueExpr) {
1097
+ // Track the yield for type extraction
1098
+ ctx.yields.push(valueExpr);
1099
+ // Emit YIELD_EMIT opcode - this doesn't suspend, just emits an event
1100
+ emitOpcode(ctx, { op: 'YIELD_EMIT', valueExpr }, expr);
1101
+ }
1102
+ // Note: bare `yield;` without a value is unusual but valid - we just skip it
1103
+ }
1104
+ /**
1105
+ * Analyze an await expression.
1106
+ */
1107
+ function analyzeAwaitExpression(expr, storeVar, ctx) {
1108
+ // Unwrap AwaitExpression
1109
+ let inner = expr;
1110
+ if (ts.isAwaitExpression(expr)) {
1111
+ inner = expr.expression;
1112
+ }
1113
+ // Check for waitFor(signal) - direct call
1114
+ if (ts.isCallExpression(inner) && isWaitForCall(inner, ctx)) {
1115
+ const signalArg = inner.arguments[0];
1116
+ const signalInfo = extractSignalInfo(signalArg, ctx);
1117
+ if (signalInfo) {
1118
+ // Get rehydration deps for this wait point
1119
+ const rehydrateDeps = getRehydrationDepsAtPoint(ctx);
1120
+ emitOpcode(ctx, {
1121
+ op: 'WAIT',
1122
+ signal: signalInfo.signalName,
1123
+ signalExpr: signalArg,
1124
+ rehydrate: rehydrateDeps.length > 0 ? rehydrateDeps : undefined,
1125
+ }, expr);
1126
+ ctx.signals[signalInfo.signalName] = {
1127
+ identity: signalInfo.identity,
1128
+ payloadType: signalInfo.payloadType,
1129
+ };
1130
+ if (storeVar) {
1131
+ emitOpcode(ctx, { op: 'STORE', var: storeVar, fromSignal: true }, expr);
1132
+ }
1133
+ }
1134
+ return;
1135
+ }
1136
+ // Check for delay(duration) - direct call
1137
+ if (ts.isCallExpression(inner) && isDelayCall(inner, ctx)) {
1138
+ const rehydrateDeps = getRehydrationDepsAtPoint(ctx);
1139
+ const delayInfo = extractDelayInfo(inner);
1140
+ emitOpcode(ctx, {
1141
+ op: 'WAIT',
1142
+ signal: '__timer__',
1143
+ timer: delayInfo ? { unit: delayInfo.unit, valueExpr: delayInfo.valueExpr } : undefined,
1144
+ rehydrate: rehydrateDeps.length > 0 ? rehydrateDeps : undefined,
1145
+ }, expr);
1146
+ if (storeVar) {
1147
+ emitOpcode(ctx, { op: 'STORE', var: storeVar, fromSignal: true }, expr);
1148
+ }
1149
+ return;
1150
+ }
1151
+ // Check for signal.all([...]) or signal.all({...}) or signal.settled([...])
1152
+ if (ts.isCallExpression(inner) && isSignalCombinatorCall(inner, ctx)) {
1153
+ analyzeSignalCombinator(inner, storeVar, ctx);
1154
+ return;
1155
+ }
1156
+ // Check for scope(entities, handler) or scope(signal, entities)
1157
+ if (ts.isCallExpression(inner) && isScopeCall(inner, ctx)) {
1158
+ analyzeScopeCall(inner, storeVar, ctx);
1159
+ return;
1160
+ }
1161
+ // Check for nested suspension points in complex expressions
1162
+ // e.g., (await signal(svc.check)).status === 'paid'
1163
+ const suspensions = findSuspensionPoints(expr, ctx.typeChecker);
1164
+ if (suspensions.length > 0) {
1165
+ // Emit WAIT for each suspension point
1166
+ for (let i = 0; i < suspensions.length; i++) {
1167
+ const suspension = suspensions[i];
1168
+ const primitive = suspension.primitive;
1169
+ if (primitive.kind === 'signal') {
1170
+ const signalArg = primitive.node.arguments[0];
1171
+ const signalInfo = extractSignalInfo(signalArg, ctx);
1172
+ if (signalInfo) {
1173
+ const rehydrateDeps = getRehydrationDepsAtPoint(ctx);
1174
+ emitOpcode(ctx, {
1175
+ op: 'WAIT',
1176
+ signal: signalInfo.signalName,
1177
+ signalExpr: signalArg,
1178
+ rehydrate: rehydrateDeps.length > 0 ? rehydrateDeps : undefined,
1179
+ }, primitive.node);
1180
+ ctx.signals[signalInfo.signalName] = {
1181
+ identity: signalInfo.identity,
1182
+ payloadType: signalInfo.payloadType,
1183
+ };
1184
+ // Store intermediate result
1185
+ const tempVar = `__await_${ctx.nextScopeId++}`;
1186
+ emitOpcode(ctx, { op: 'STORE', var: tempVar, fromSignal: true }, primitive.node);
1187
+ }
1188
+ }
1189
+ else if (primitive.kind === 'delay') {
1190
+ const rehydrateDeps = getRehydrationDepsAtPoint(ctx);
1191
+ const delayInfo = extractDelayInfo(primitive.node);
1192
+ emitOpcode(ctx, {
1193
+ op: 'WAIT',
1194
+ signal: '__timer__',
1195
+ timer: delayInfo ? { unit: delayInfo.unit, valueExpr: delayInfo.valueExpr } : undefined,
1196
+ rehydrate: rehydrateDeps.length > 0 ? rehydrateDeps : undefined,
1197
+ }, primitive.node);
1198
+ const tempVar = `__await_${ctx.nextScopeId++}`;
1199
+ emitOpcode(ctx, { op: 'STORE', var: tempVar, fromSignal: true }, primitive.node);
1200
+ }
1201
+ }
1202
+ // Create a block for the rest of the expression evaluation
1203
+ // (Note: in a full implementation, we'd rewrite the expression to use temp vars)
1204
+ const blockId = createBlock(ctx, []);
1205
+ // Use expr as source position for the block opcode
1206
+ emitOpcode(ctx, { op: 'BLOCK', blockId }, expr);
1207
+ if (storeVar) {
1208
+ emitOpcode(ctx, { op: 'STORE', var: storeVar, fromBlock: true });
1209
+ }
1210
+ return;
1211
+ }
1212
+ // Check for inner function call - try to inline it
1213
+ if (ts.isCallExpression(inner)) {
1214
+ if (tryInlineInnerFunctionCall(inner, storeVar, ctx)) {
1215
+ return;
1216
+ }
1217
+ // Check for subprocess spawn (await playerSeat('alice'))
1218
+ if (trySubprocessSpawn(inner, storeVar, ctx, true)) {
1219
+ return;
1220
+ }
1221
+ }
1222
+ // Regular async call - becomes a block whose body actually runs the
1223
+ // awaited call and stashes the result in __blockResult. Without the
1224
+ // synthetic assignment the compiled output would emit
1225
+ // state.vars.x = __blockResult
1226
+ // with nothing ever writing __blockResult, so `x` would be undefined.
1227
+ //
1228
+ // The block body is a single expression statement:
1229
+ // __blockResult = await <inner-call>
1230
+ // Identifiers inside <inner-call> (services, using/param vars, ...) get
1231
+ // rewritten by the BLOCK codegen visitor just like hand-written block
1232
+ // statements, so this stays consistent with every other service-await
1233
+ // path.
1234
+ const syntheticAssignment = ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(ts.factory.createIdentifier('__blockResult'), ts.factory.createToken(ts.SyntaxKind.EqualsToken), expr));
1235
+ // Keep source position on the synthetic statement so source maps and
1236
+ // later `usedInBlocks` tracking resolve to the right place.
1237
+ ts.setTextRange(syntheticAssignment, expr);
1238
+ // Record uses of `using` vars in this awaited expression so the block's
1239
+ // enclosing step rehydrates them before replay. We capture uses into
1240
+ // `currentBlockUses`, snapshot them into the new block, then reset -
1241
+ // `currentBlockUses` is meant to be per-pending-block and we just
1242
+ // finished building ours.
1243
+ trackUsedVariables(syntheticAssignment, ctx);
1244
+ const blockId = createBlock(ctx, [syntheticAssignment], Array.from(ctx.currentBlockUses));
1245
+ ctx.currentBlockUses = new Set();
1246
+ // Use expr as source position for the block opcode
1247
+ emitOpcode(ctx, { op: 'BLOCK', blockId }, expr);
1248
+ if (storeVar) {
1249
+ emitOpcode(ctx, { op: 'STORE', var: storeVar, fromBlock: true });
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Analyze a while statement.
1254
+ */
1255
+ function analyzeWhileStatement(stmt, ctx) {
1256
+ flushBlock(ctx);
1257
+ // Mark loop start
1258
+ const loopStartPc = ctx.opcodes.length;
1259
+ const startLabel = `__while_${loopStartPc}`;
1260
+ const endLabel = `__while_end_${loopStartPc}`;
1261
+ // Register labels in labelTargets for pendingLabelPatches
1262
+ ctx.labelTargets.set(startLabel, loopStartPc);
1263
+ emitOpcode(ctx, { op: 'LABEL', label: startLabel }, stmt);
1264
+ // Push loop onto stack for continue/break handling
1265
+ ctx.loopStack.push({ startLabel, endLabel });
1266
+ // TSP1013: Detect suspension points in while-loop condition
1267
+ if (containsSuspension(stmt.expression, ctx)) {
1268
+ ctx.diagnostics.add(ProcessErrorCode.WhileConditionSuspension, stmt.expression);
1269
+ }
1270
+ // Emit a condition check for non-literal-true conditions. Without this,
1271
+ // `while (running)` compiles identically to `while (true)`: the loop body
1272
+ // runs forever because the condition is never re-evaluated after each
1273
+ // iteration. Setting `running = false` inside a race branch cannot exit
1274
+ // the loop.
1275
+ //
1276
+ // Strategy: evaluate the condition in a BLOCK, STORE to `__condition`,
1277
+ // then JUMP_IF(__condition_false) to the end label. This is the same
1278
+ // pattern used by the complex-if path in analyzeIfStatement.
1279
+ //
1280
+ // Skip the check for literal `while (true)` - it's the common infinite-loop
1281
+ // pattern used by race-switch handlers and emitting a redundant check would
1282
+ // add a pointless BLOCK per iteration.
1283
+ let conditionJumpOp;
1284
+ const isTrueLiteral = stmt.expression.kind === ts.SyntaxKind.TrueKeyword;
1285
+ if (!isTrueLiteral) {
1286
+ // Build a synthetic block `async () => { return <condition>; }`.
1287
+ const conditionReturn = ts.factory.createReturnStatement(stmt.expression);
1288
+ ts.setTextRange(conditionReturn, stmt.expression);
1289
+ const conditionBlockId = createBlock(ctx, [conditionReturn]);
1290
+ emitOpcode(ctx, { op: 'BLOCK', blockId: conditionBlockId }, stmt.expression);
1291
+ emitOpcode(ctx, { op: 'STORE', var: '__condition', fromBlock: true });
1292
+ conditionJumpOp = { op: 'JUMP_IF', condition: '__condition_false', target: -1 };
1293
+ emitOpcode(ctx, conditionJumpOp, stmt);
1294
+ }
1295
+ // Analyze body
1296
+ if (ts.isBlock(stmt.statement)) {
1297
+ analyzeStatements(stmt.statement.statements, ctx);
1298
+ }
1299
+ else {
1300
+ analyzeStatement(stmt.statement, ctx);
1301
+ }
1302
+ flushBlock(ctx);
1303
+ // Pop loop from stack
1304
+ ctx.loopStack.pop();
1305
+ // Add a "continue" label before the jump for proper step boundaries.
1306
+ // This ensures non-returning branches (like signal handlers) have a step
1307
+ // that contains the JUMP back to the loop start, instead of falling through.
1308
+ const continueLabel = `__while_continue_${loopStartPc}`;
1309
+ ctx.labelTargets.set(continueLabel, ctx.opcodes.length);
1310
+ emitOpcode(ctx, { op: 'LABEL', label: continueLabel }, stmt);
1311
+ // Jump back to loop start (re-evaluates the condition on next iteration)
1312
+ emitOpcode(ctx, { op: 'JUMP', target: loopStartPc }, stmt);
1313
+ // Register end label and mark loop end for break statements.
1314
+ // Also patch the condition check to jump here when the condition is false.
1315
+ ctx.labelTargets.set(endLabel, ctx.opcodes.length);
1316
+ if (conditionJumpOp) {
1317
+ conditionJumpOp.target = ctx.opcodes.length;
1318
+ }
1319
+ emitOpcode(ctx, { op: 'LABEL', label: endLabel }, stmt);
1320
+ }
1321
+ /**
1322
+ * Analyze an if statement.
1323
+ *
1324
+ * For simple if statements (no suspension points in any branch),
1325
+ * we inline them into the current block for cleaner generated code.
1326
+ *
1327
+ * For if statements with suspension points, we still need separate
1328
+ * steps for each branch that contains a suspension.
1329
+ */
1330
+ function analyzeIfStatement(stmt, ctx) {
1331
+ // Check if branches contain suspension points
1332
+ const thenHasSuspension = containsSuspensionInStatement(stmt.thenStatement, ctx);
1333
+ const elseHasSuspension = stmt.elseStatement
1334
+ ? containsSuspensionInStatement(stmt.elseStatement, ctx)
1335
+ : false;
1336
+ // Simple if - no suspension points, add to current block
1337
+ // Codegen will handle it with normal if/else and transform returns
1338
+ if (!thenHasSuspension && !elseHasSuspension) {
1339
+ // TSP3004: Still check for throw statements inside branches
1340
+ checkForThrowStatements(stmt.thenStatement, ctx);
1341
+ if (stmt.elseStatement) {
1342
+ checkForThrowStatements(stmt.elseStatement, ctx);
1343
+ }
1344
+ // TSP1005: Check for non-deterministic operations
1345
+ checkForNonDeterministicOps(stmt, ctx);
1346
+ ctx.currentBlockStatements.push(stmt);
1347
+ trackUsedVariables(stmt, ctx);
1348
+ return;
1349
+ }
1350
+ // Complex if with suspension points - use step-based approach
1351
+ flushBlock(ctx);
1352
+ // Create a block that returns the condition expression
1353
+ // This becomes: async () => { return !user }
1354
+ const conditionReturn = ts.factory.createReturnStatement(stmt.expression);
1355
+ // Copy source position from original condition for source maps
1356
+ ts.setTextRange(conditionReturn, stmt.expression);
1357
+ const conditionBlockId = createBlock(ctx, [conditionReturn]);
1358
+ emitOpcode(ctx, { op: 'BLOCK', blockId: conditionBlockId }, stmt.expression);
1359
+ emitOpcode(ctx, { op: 'STORE', var: '__condition', fromBlock: true });
1360
+ // Map JUMP_IF to the original if statement for source maps
1361
+ const jumpIfFalse = { op: 'JUMP_IF', condition: '__condition_false', target: -1 };
1362
+ emitOpcode(ctx, jumpIfFalse, stmt);
1363
+ // Then branch
1364
+ if (ts.isBlock(stmt.thenStatement)) {
1365
+ analyzeStatements(stmt.thenStatement.statements, ctx);
1366
+ }
1367
+ else {
1368
+ analyzeStatement(stmt.thenStatement, ctx);
1369
+ }
1370
+ flushBlock(ctx);
1371
+ if (stmt.elseStatement) {
1372
+ const jumpToEnd = { op: 'JUMP', target: -1 };
1373
+ emitOpcode(ctx, jumpToEnd);
1374
+ jumpIfFalse.target = ctx.opcodes.length;
1375
+ // Else branch
1376
+ if (ts.isBlock(stmt.elseStatement)) {
1377
+ analyzeStatements(stmt.elseStatement.statements, ctx);
1378
+ }
1379
+ else {
1380
+ analyzeStatement(stmt.elseStatement, ctx);
1381
+ }
1382
+ flushBlock(ctx);
1383
+ jumpToEnd.target = ctx.opcodes.length;
1384
+ }
1385
+ else {
1386
+ // Patch the jump-if-false
1387
+ ;
1388
+ jumpIfFalse.target = ctx.opcodes.length;
1389
+ }
1390
+ }
1391
+ /**
1392
+ * Analyze a switch statement - might be a race pattern.
1393
+ */
1394
+ function analyzeSwitchStatement(stmt, ctx) {
1395
+ const expr = stmt.expression;
1396
+ // Race pattern: switch (true) { case signal(r, ...): }
1397
+ // Using switch(true) with type guards enables type narrowing in each case
1398
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
1399
+ const raceVarName = findRaceVariableInCases(stmt.caseBlock.clauses, ctx);
1400
+ if (raceVarName) {
1401
+ analyzeRaceSwitch(stmt, raceVarName, ctx);
1402
+ return;
1403
+ }
1404
+ // TSP1003: Cases contain signal/delay calls but no race() variable
1405
+ if (casesContainPrimitiveCalls(stmt.caseBlock.clauses, ctx)) {
1406
+ ctx.diagnostics.add(ProcessErrorCode.InvalidRacePattern, stmt);
1407
+ return;
1408
+ }
1409
+ }
1410
+ // Regular switch - check if it contains any suspension points
1411
+ const hasSuspension = checkSwitchHasSuspension(stmt, ctx);
1412
+ if (hasSuspension) {
1413
+ // Switch contains suspension - need to decompose (complex, for future)
1414
+ flushBlock(ctx);
1415
+ // Track nested switch depth so break statements aren't suppressed
1416
+ if (ctx.inRaceBranch) {
1417
+ ctx.nestedSwitchDepth++;
1418
+ }
1419
+ for (const clause of stmt.caseBlock.clauses) {
1420
+ if (ts.isCaseClause(clause)) {
1421
+ analyzeStatements(clause.statements, ctx);
1422
+ }
1423
+ else {
1424
+ analyzeStatements(clause.statements, ctx);
1425
+ }
1426
+ }
1427
+ if (ctx.inRaceBranch) {
1428
+ ctx.nestedSwitchDepth--;
1429
+ }
1430
+ }
1431
+ else {
1432
+ // No suspension - add the entire switch as a statement in the current block
1433
+ // The switch (including its case bodies and break statements) will be preserved as-is
1434
+ ctx.currentBlockStatements.push(stmt);
1435
+ trackUsedVariables(stmt, ctx);
1436
+ }
1437
+ }
1438
+ /**
1439
+ * Check if a switch statement contains any suspension points.
1440
+ */
1441
+ function checkSwitchHasSuspension(stmt, ctx) {
1442
+ for (const clause of stmt.caseBlock.clauses) {
1443
+ for (const s of clause.statements) {
1444
+ if (checkHasSuspension(s, ctx)) {
1445
+ return true;
1446
+ }
1447
+ }
1448
+ }
1449
+ return false;
1450
+ }
1451
+ /**
1452
+ * Check if a statement contains any suspension points (signal, delay, stream, race, await).
1453
+ */
1454
+ function checkHasSuspension(node, ctx) {
1455
+ if (ts.isAwaitExpression(node)) {
1456
+ return true;
1457
+ }
1458
+ if (ts.isCallExpression(node)) {
1459
+ if (isWaitForCall(node, ctx) || isDelayCall(node, ctx) || isStreamCall(node, ctx)) {
1460
+ return true;
1461
+ }
1462
+ // Check if it's race()
1463
+ const callee = node.expression;
1464
+ if (ts.isIdentifier(callee) && callee.text === 'race') {
1465
+ return true;
1466
+ }
1467
+ }
1468
+ // Recursively check children
1469
+ let found = false;
1470
+ ts.forEachChild(node, child => {
1471
+ if (checkHasSuspension(child, ctx)) {
1472
+ found = true;
1473
+ }
1474
+ });
1475
+ return found;
1476
+ }
1477
+ /**
1478
+ * Find race variable name from switch cases.
1479
+ * Looks for pattern: case signal(r, target) or case delay(r, duration)
1480
+ * where r is a variable initialized from race().
1481
+ */
1482
+ function findRaceVariableInCases(clauses, ctx) {
1483
+ for (const clause of clauses) {
1484
+ if (!ts.isCaseClause(clause))
1485
+ continue;
1486
+ const caseExpr = clause.expression;
1487
+ if (!ts.isCallExpression(caseExpr))
1488
+ continue;
1489
+ // Check for signal(r, ...), delay(r, ...), signal.all(r, [...]),
1490
+ // signal.settled(r, [...]), or stream(r, ...) - any two-arg race primitive
1491
+ const isRacePrimitive = (isWaitForCall(caseExpr, ctx) || isDelayCall(caseExpr, ctx) ||
1492
+ isSignalCombinatorCall(caseExpr, ctx) || isStreamCall(caseExpr, ctx)) &&
1493
+ caseExpr.arguments.length >= 2;
1494
+ if (isRacePrimitive) {
1495
+ const firstArg = caseExpr.arguments[0];
1496
+ if (ts.isIdentifier(firstArg)) {
1497
+ const varName = firstArg.text;
1498
+ if (isRaceVariable(varName, ctx)) {
1499
+ return varName;
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+ return null;
1505
+ }
1506
+ /**
1507
+ * Check if any case clause contains signal/delay/stream calls (primitive calls
1508
+ * that suggest the user intended a race pattern but forgot the race() variable).
1509
+ */
1510
+ function casesContainPrimitiveCalls(clauses, ctx) {
1511
+ for (const clause of clauses) {
1512
+ if (!ts.isCaseClause(clause))
1513
+ continue;
1514
+ const caseExpr = clause.expression;
1515
+ if (!ts.isCallExpression(caseExpr))
1516
+ continue;
1517
+ if (isWaitForCall(caseExpr, ctx) || isDelayCall(caseExpr, ctx) || isStreamCall(caseExpr, ctx)) {
1518
+ return true;
1519
+ }
1520
+ }
1521
+ return false;
1522
+ }
1523
+ /**
1524
+ * Check if a variable was initialized from race().
1525
+ * Tracks `const r = race()` declarations.
1526
+ */
1527
+ function isRaceVariable(varName, ctx) {
1528
+ const varInfo = ctx.variables.get(varName);
1529
+ if (!varInfo)
1530
+ return false;
1531
+ const declNode = varInfo.declarationNode;
1532
+ if (!ts.isVariableDeclaration(declNode))
1533
+ return false;
1534
+ const init = declNode.initializer;
1535
+ if (!init)
1536
+ return false;
1537
+ // Direct race() call
1538
+ if (ts.isCallExpression(init) && isRaceCall(init, ctx)) {
1539
+ return true;
1540
+ }
1541
+ return false;
1542
+ }
1543
+ /**
1544
+ * Analyze a race switch pattern.
1545
+ *
1546
+ * Pattern: switch (true) { case signal(r, target): ... case delay(r, duration): ... }
1547
+ *
1548
+ * Uses switch(true) with type guard functions to enable type narrowing in each case.
1549
+ *
1550
+ * @param raceVarName - The variable name from race() call
1551
+ */
1552
+ function analyzeRaceSwitch(stmt, raceVarName, ctx) {
1553
+ flushBlock(ctx);
1554
+ // Track the race variable for rewriting r.xxx to __raceResult.xxx
1555
+ ctx.raceVars.add(raceVarName);
1556
+ const branches = [];
1557
+ const branchBodies = [];
1558
+ const branchSourceNodes = new Map();
1559
+ // Collect branches from case clauses
1560
+ for (const clause of stmt.caseBlock.clauses) {
1561
+ if (!ts.isCaseClause(clause))
1562
+ continue;
1563
+ const caseExpr = clause.expression;
1564
+ let branch = null;
1565
+ // Pattern: case signal(r, target) or case delay(r, duration)
1566
+ if (ts.isCallExpression(caseExpr) && caseExpr.arguments.length >= 2) {
1567
+ const firstArg = caseExpr.arguments[0];
1568
+ if (ts.isIdentifier(firstArg) && firstArg.text === raceVarName) {
1569
+ if (isWaitForCall(caseExpr, ctx)) {
1570
+ // signal(r, target) - second arg is the signal
1571
+ const signalArg = caseExpr.arguments[1];
1572
+ const signalInfo = extractSignalInfo(signalArg, ctx);
1573
+ if (signalInfo) {
1574
+ branch = {
1575
+ id: signalInfo.signalName,
1576
+ signal: signalInfo.signalName,
1577
+ signalExpr: signalArg, // Store for runtime .signalName access
1578
+ jumpTarget: -1,
1579
+ };
1580
+ ctx.signals[signalInfo.signalName] = {
1581
+ identity: signalInfo.identity,
1582
+ payloadType: signalInfo.payloadType,
1583
+ };
1584
+ branchSourceNodes.set(signalInfo.signalName, caseExpr);
1585
+ }
1586
+ }
1587
+ else if (isDelayCall(caseExpr, ctx)) {
1588
+ // delay.unit(r, value) - get unit from method name, value is second arg
1589
+ const delayInfo = extractDelayInfo(caseExpr);
1590
+ if (delayInfo) {
1591
+ branch = {
1592
+ id: '__timer__',
1593
+ timer: {
1594
+ unit: delayInfo.unit,
1595
+ valueExpr: delayInfo.valueExpr,
1596
+ },
1597
+ jumpTarget: -1,
1598
+ };
1599
+ branchSourceNodes.set('__timer__', caseExpr);
1600
+ }
1601
+ }
1602
+ else if (isStreamCall(caseExpr, ctx)) {
1603
+ // stream(r, entity.field) - second arg is the stream field
1604
+ const streamArg = caseExpr.arguments[1];
1605
+ const streamInfo = extractStreamInfo(streamArg, ctx);
1606
+ if (streamInfo) {
1607
+ branch = {
1608
+ id: streamInfo.signalName,
1609
+ signal: streamInfo.signalName,
1610
+ signalExpr: streamArg, // Store for runtime access
1611
+ jumpTarget: -1,
1612
+ };
1613
+ ctx.signals[streamInfo.signalName] = {
1614
+ identity: streamInfo.identity,
1615
+ payloadType: streamInfo.payloadType,
1616
+ };
1617
+ branchSourceNodes.set(streamInfo.signalName, caseExpr);
1618
+ }
1619
+ }
1620
+ else if (isSignalCombinatorCall(caseExpr, ctx)) {
1621
+ // signal.all(r, [...]) / signal.settled(r, [...]) inside a race switch
1622
+ // is not yet supported - emit a clear compiler error (TSP3012).
1623
+ ctx.diagnostics.add(ProcessErrorCode.RaceCombinatorNotSupported, caseExpr);
1624
+ }
1625
+ }
1626
+ }
1627
+ if (branch) {
1628
+ branches.push(branch);
1629
+ branchBodies.push({
1630
+ statements: Array.from(clause.statements),
1631
+ jumpTarget: -1,
1632
+ });
1633
+ }
1634
+ }
1635
+ // Validate non-empty branches
1636
+ if (branches.length === 0) {
1637
+ ctx.diagnostics.add(ProcessErrorCode.EmptyRace, stmt);
1638
+ return;
1639
+ }
1640
+ // Get rehydration deps
1641
+ const rehydrateDeps = getRehydrationDepsAtPoint(ctx);
1642
+ // Store branch source nodes before emitting opcode (opcode index will be current length)
1643
+ const raceOpcodeIndex = ctx.opcodes.length;
1644
+ ctx.raceBranchSourceNodes.set(raceOpcodeIndex, branchSourceNodes);
1645
+ // Map RACE_START to original switch statement for source maps
1646
+ emitOpcode(ctx, {
1647
+ op: 'RACE_START',
1648
+ branches,
1649
+ rehydrate: rehydrateDeps.length > 0 ? rehydrateDeps : undefined,
1650
+ }, stmt);
1651
+ // Emit RACE_SUSPEND
1652
+ emitOpcode(ctx, { op: 'RACE_SUSPEND' }, stmt);
1653
+ // Second pass: emit branch bodies and patch jump targets
1654
+ // Set inRaceBranch so break statements don't emit JUMP (they fall through to nextStep)
1655
+ const wasInRaceBranch = ctx.inRaceBranch;
1656
+ ctx.inRaceBranch = true;
1657
+ for (let i = 0; i < branches.length; i++) {
1658
+ branches[i].jumpTarget = ctx.opcodes.length;
1659
+ // Store the race result
1660
+ emitOpcode(ctx, { op: 'STORE', var: '__raceResult', fromRace: true });
1661
+ // Analyze branch body
1662
+ for (const s of branchBodies[i].statements) {
1663
+ analyzeStatement(s, ctx);
1664
+ }
1665
+ flushBlock(ctx);
1666
+ }
1667
+ ctx.inRaceBranch = wasInRaceBranch;
1668
+ }
1669
+ /**
1670
+ * Check if an expression contains a suspension point.
1671
+ * Uses type-based detection for reliability.
1672
+ */
1673
+ function containsSuspension(expr, ctx) {
1674
+ // Check for yield expressions (in generator handlers)
1675
+ if (ts.isYieldExpression(expr)) {
1676
+ return true;
1677
+ }
1678
+ // Check for yield expressions nested in the expression
1679
+ let hasYield = false;
1680
+ const checkYield = (node) => {
1681
+ if (hasYield)
1682
+ return;
1683
+ if (ts.isYieldExpression(node)) {
1684
+ hasYield = true;
1685
+ return;
1686
+ }
1687
+ // Don't recurse into nested functions
1688
+ if (ts.isFunctionExpression(node) || ts.isArrowFunction(node))
1689
+ return;
1690
+ ts.forEachChild(node, checkYield);
1691
+ };
1692
+ checkYield(expr);
1693
+ if (hasYield)
1694
+ return true;
1695
+ return containsSuspensionPoint(expr, ctx.typeChecker);
1696
+ }
1697
+ /**
1698
+ * Check if a block contains any suspension points.
1699
+ */
1700
+ function containsSuspensionInBlock(block, ctx) {
1701
+ for (const stmt of block.statements) {
1702
+ if (containsSuspensionInStatement(stmt, ctx)) {
1703
+ return true;
1704
+ }
1705
+ }
1706
+ return false;
1707
+ }
1708
+ /**
1709
+ * Check if an expression contains a call to an inner function with suspension points.
1710
+ * These need to be inlined at the call site.
1711
+ */
1712
+ function containsInnerFunctionCall(expr, ctx) {
1713
+ let found = false;
1714
+ const visit = (node) => {
1715
+ if (found)
1716
+ return;
1717
+ // Check for call expressions to registered inner functions
1718
+ if (ts.isCallExpression(node)) {
1719
+ const callee = node.expression;
1720
+ if (ts.isIdentifier(callee)) {
1721
+ const funcInfo = ctx.innerFunctions.get(callee.text);
1722
+ if (funcInfo && funcInfo.hasSuspension) {
1723
+ found = true;
1724
+ return;
1725
+ }
1726
+ }
1727
+ }
1728
+ // Skip nested function bodies
1729
+ if (ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isFunctionDeclaration(node)) {
1730
+ return;
1731
+ }
1732
+ ts.forEachChild(node, visit);
1733
+ };
1734
+ visit(expr);
1735
+ return found;
1736
+ }
1737
+ /**
1738
+ * Check if a statement contains any suspension points (deep search).
1739
+ */
1740
+ function containsSuspensionInStatement(stmt, ctx) {
1741
+ let found = false;
1742
+ const visit = (node) => {
1743
+ if (found)
1744
+ return;
1745
+ // Check yield expressions (in generator handlers)
1746
+ if (ts.isYieldExpression(node)) {
1747
+ found = true;
1748
+ return;
1749
+ }
1750
+ // Check await expressions
1751
+ if (ts.isAwaitExpression(node)) {
1752
+ const inner = node.expression;
1753
+ if (ts.isCallExpression(inner)) {
1754
+ const primitive = getPrimitiveCall(inner, ctx.typeChecker);
1755
+ if (primitive && (primitive.kind === 'signal' || primitive.kind === 'delay')) {
1756
+ found = true;
1757
+ return;
1758
+ }
1759
+ }
1760
+ }
1761
+ // Skip nested function bodies (they have their own scope)
1762
+ if (ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isFunctionDeclaration(node)) {
1763
+ return;
1764
+ }
1765
+ ts.forEachChild(node, visit);
1766
+ };
1767
+ visit(stmt);
1768
+ return found;
1769
+ }
1770
+ /**
1771
+ * Walk an expression tree looking for async arrow/function expressions with
1772
+ * suspension points that are passed as arguments to calls (not assigned to
1773
+ * named variables, which get registered for inlining).
1774
+ */
1775
+ function checkForNestedAsyncWithSuspension(expr, ctx) {
1776
+ const visit = (node) => {
1777
+ // Skip into call expressions to check their arguments
1778
+ if (ts.isCallExpression(node)) {
1779
+ for (const arg of node.arguments) {
1780
+ if ((ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) &&
1781
+ arg.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword)) {
1782
+ const hasSuspension = ts.isBlock(arg.body)
1783
+ ? containsSuspensionInBlock(arg.body, ctx)
1784
+ : containsSuspension(arg.body, ctx);
1785
+ if (hasSuspension) {
1786
+ ctx.diagnostics.add(ProcessErrorCode.NestedAsyncWithSuspension, arg);
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ ts.forEachChild(node, visit);
1792
+ };
1793
+ visit(expr);
1794
+ }
1795
+ /**
1796
+ * Register an inner function for potential inlining.
1797
+ */
1798
+ function registerInnerFunction(name, node, ctx) {
1799
+ const isAsync = node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
1800
+ let hasSuspension = false;
1801
+ if (node.body) {
1802
+ if (ts.isBlock(node.body)) {
1803
+ hasSuspension = containsSuspensionInBlock(node.body, ctx);
1804
+ }
1805
+ else if (!ts.isFunctionDeclaration(node)) {
1806
+ // Arrow function expression body
1807
+ hasSuspension = containsSuspension(node.body, ctx);
1808
+ }
1809
+ }
1810
+ const capturedVars = findCapturedVariables(node, ctx);
1811
+ const callsTo = findFunctionCalls(node);
1812
+ const info = {
1813
+ name,
1814
+ node,
1815
+ hasSuspension,
1816
+ isAsync,
1817
+ capturedVars,
1818
+ inlineCount: 0,
1819
+ callsTo,
1820
+ };
1821
+ ctx.innerFunctions.set(name, info);
1822
+ return info;
1823
+ }
1824
+ /**
1825
+ * Find variables captured from outer scope by a function.
1826
+ */
1827
+ function findCapturedVariables(node, ctx) {
1828
+ const captured = new Set();
1829
+ const localVars = new Set();
1830
+ // Collect parameters as local variables
1831
+ for (const param of node.parameters) {
1832
+ if (ts.isIdentifier(param.name)) {
1833
+ localVars.add(param.name.text);
1834
+ }
1835
+ }
1836
+ const visit = (n) => {
1837
+ // Track local variable declarations
1838
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
1839
+ localVars.add(n.name.text);
1840
+ }
1841
+ // Check identifier references
1842
+ if (ts.isIdentifier(n)) {
1843
+ const name = n.text;
1844
+ // Skip if it's a property access (not a standalone reference)
1845
+ const parent = n.parent;
1846
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === n) {
1847
+ return;
1848
+ }
1849
+ // Skip if it's a function name in a call
1850
+ if (parent && ts.isCallExpression(parent) && parent.expression === n) {
1851
+ // This is a function call - we track it separately
1852
+ return;
1853
+ }
1854
+ // If it's known in outer scope but not local, it's captured
1855
+ if (!localVars.has(name) && ctx.variables.has(name)) {
1856
+ captured.add(name);
1857
+ }
1858
+ }
1859
+ ts.forEachChild(n, visit);
1860
+ };
1861
+ if (node.body) {
1862
+ visit(node.body);
1863
+ }
1864
+ return captured;
1865
+ }
1866
+ /**
1867
+ * Find which inner functions are called by a function.
1868
+ */
1869
+ function findFunctionCalls(node) {
1870
+ const calls = new Set();
1871
+ const visit = (n) => {
1872
+ if (ts.isCallExpression(n)) {
1873
+ const callee = n.expression;
1874
+ if (ts.isIdentifier(callee)) {
1875
+ const name = callee.text;
1876
+ // Will be checked against innerFunctions later
1877
+ calls.add(name);
1878
+ }
1879
+ }
1880
+ ts.forEachChild(n, visit);
1881
+ };
1882
+ if (node.body) {
1883
+ visit(node.body);
1884
+ }
1885
+ return calls;
1886
+ }
1887
+ /**
1888
+ * Check if a function escapes its scope (returned, passed as argument, stored in array/object).
1889
+ */
1890
+ function checkFunctionEscapes(name, containingBlock) {
1891
+ let escapes = false;
1892
+ const visit = (n) => {
1893
+ if (escapes)
1894
+ return;
1895
+ // Return statement - function escapes if returned
1896
+ if (ts.isReturnStatement(n) && n.expression) {
1897
+ if (ts.isIdentifier(n.expression) && n.expression.text === name) {
1898
+ escapes = true;
1899
+ return;
1900
+ }
1901
+ // Check if returned in object/array literal
1902
+ if (containsIdentifierReference(n.expression, name)) {
1903
+ const parent = n.expression;
1904
+ if (ts.isObjectLiteralExpression(parent) || ts.isArrayLiteralExpression(parent)) {
1905
+ escapes = true;
1906
+ return;
1907
+ }
1908
+ }
1909
+ }
1910
+ // Passed as argument to another function (except await)
1911
+ if (ts.isCallExpression(n)) {
1912
+ for (const arg of n.arguments) {
1913
+ if (ts.isIdentifier(arg) && arg.text === name) {
1914
+ // Check if it's an await call to the function itself - that's OK
1915
+ const callee = n.expression;
1916
+ if (ts.isIdentifier(callee) && callee.text === name) {
1917
+ continue; // Calling the function is OK
1918
+ }
1919
+ escapes = true;
1920
+ return;
1921
+ }
1922
+ }
1923
+ }
1924
+ // Assigned to object property or array element
1925
+ if (ts.isPropertyAssignment(n)) {
1926
+ if (ts.isIdentifier(n.initializer) && n.initializer.text === name) {
1927
+ escapes = true;
1928
+ return;
1929
+ }
1930
+ }
1931
+ if (ts.isArrayLiteralExpression(n)) {
1932
+ for (const elem of n.elements) {
1933
+ if (ts.isIdentifier(elem) && elem.text === name) {
1934
+ escapes = true;
1935
+ return;
1936
+ }
1937
+ }
1938
+ }
1939
+ ts.forEachChild(n, visit);
1940
+ };
1941
+ for (const stmt of containingBlock) {
1942
+ visit(stmt);
1943
+ if (escapes)
1944
+ break;
1945
+ }
1946
+ return escapes;
1947
+ }
1948
+ /**
1949
+ * Check if an expression contains a reference to a specific identifier.
1950
+ */
1951
+ function containsIdentifierReference(expr, name) {
1952
+ let found = false;
1953
+ const visit = (n) => {
1954
+ if (found)
1955
+ return;
1956
+ if (ts.isIdentifier(n) && n.text === name) {
1957
+ found = true;
1958
+ return;
1959
+ }
1960
+ ts.forEachChild(n, visit);
1961
+ };
1962
+ visit(expr);
1963
+ return found;
1964
+ }
1965
+ /**
1966
+ * Check for mutual recursion between functions.
1967
+ * Only checks for A -> B -> A patterns, not self-recursion (A -> A).
1968
+ * Self-recursion is detected by the inlining stack check.
1969
+ */
1970
+ function checkMutualRecursion(funcName, ctx) {
1971
+ const funcInfo = ctx.innerFunctions.get(funcName);
1972
+ if (!funcInfo)
1973
+ return null;
1974
+ // Check if any function we call (excluding self), calls us back
1975
+ for (const calledName of funcInfo.callsTo) {
1976
+ // Skip self-recursion - that's handled by the inlining stack
1977
+ if (calledName === funcName)
1978
+ continue;
1979
+ const calledInfo = ctx.innerFunctions.get(calledName);
1980
+ if (calledInfo && calledInfo.callsTo.has(funcName)) {
1981
+ return calledName;
1982
+ }
1983
+ }
1984
+ return null;
1985
+ }
1986
+ /**
1987
+ * Try to inline an inner function call. Returns true if inlined, false if not possible.
1988
+ */
1989
+ function tryInlineInnerFunctionCall(callExpr, storeVar, ctx) {
1990
+ const callee = callExpr.expression;
1991
+ if (!ts.isIdentifier(callee))
1992
+ return false;
1993
+ const funcName = callee.text;
1994
+ const funcInfo = ctx.innerFunctions.get(funcName);
1995
+ if (!funcInfo)
1996
+ return false;
1997
+ // Only inline functions with suspension points
1998
+ if (!funcInfo.hasSuspension)
1999
+ return false;
2000
+ // Check for recursion - is this function already being inlined?
2001
+ if (ctx.inliningStack.includes(funcName)) {
2002
+ ctx.diagnostics.add(ProcessErrorCode.RecursionDepthUnknown, callExpr, funcName);
2003
+ return false;
2004
+ }
2005
+ // Check for mutual recursion
2006
+ const mutualWith = checkMutualRecursion(funcName, ctx);
2007
+ if (mutualWith) {
2008
+ ctx.diagnostics.add(ProcessErrorCode.MutualRecursion, callExpr, funcName, mutualWith);
2009
+ return false;
2010
+ }
2011
+ // Check inlining depth
2012
+ if (ctx.inliningStack.length >= ctx.maxInliningDepth) {
2013
+ ctx.diagnostics.add(ProcessErrorCode.MaxInliningDepthExceeded, callExpr, ctx.maxInliningDepth.toString(), funcName);
2014
+ return false;
2015
+ }
2016
+ // Handle parameterized inner functions:
2017
+ // Store each argument into a state variable named after the parameter,
2018
+ // then inline the body. The rewriter maps parameter references to state.vars.<param>.
2019
+ const params = funcInfo.node.parameters;
2020
+ const args = callExpr.arguments;
2021
+ if (params.length > 0) {
2022
+ for (let i = 0; i < params.length; i++) {
2023
+ const param = params[i];
2024
+ if (!ts.isIdentifier(param.name))
2025
+ continue;
2026
+ const paramName = param.name.text;
2027
+ // Register parameter as a local serializable variable
2028
+ ctx.variables.set(paramName, {
2029
+ name: paramName,
2030
+ isUsing: false,
2031
+ isSerializable: true,
2032
+ declarationNode: param,
2033
+ usedInBlocks: [],
2034
+ });
2035
+ // Create synthetic assignment: paramName = argExpr
2036
+ const argExpr = i < args.length ? args[i] : ts.factory.createIdentifier('undefined');
2037
+ const syntheticAssignment = ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createIdentifier(paramName), argExpr));
2038
+ ctx.currentBlockStatements.push(syntheticAssignment);
2039
+ trackUsedVariables(syntheticAssignment, ctx);
2040
+ }
2041
+ }
2042
+ // Increment inline count for this function
2043
+ funcInfo.inlineCount++;
2044
+ // Push onto inlining stack
2045
+ ctx.inliningStack.push(funcName);
2046
+ // Inline the function body
2047
+ const body = funcInfo.node.body;
2048
+ if (body) {
2049
+ if (ts.isBlock(body)) {
2050
+ analyzeStatements(body.statements, ctx);
2051
+ }
2052
+ else {
2053
+ // Expression body - analyze as expression
2054
+ analyzeExpression(body, ctx);
2055
+ }
2056
+ }
2057
+ // Pop from inlining stack
2058
+ ctx.inliningStack.pop();
2059
+ return true;
2060
+ }
2061
+ /**
2062
+ * Check if an expression is a Promise.all/Promise.race/Promise.any with signals.
2063
+ * Returns the combinator name if found, null otherwise.
2064
+ */
2065
+ /**
2066
+ * Extract subprocess info from a createSubProcess({ name, path, handler }) call.
2067
+ * Returns null if the call doesn't match the expected shape.
2068
+ */
2069
+ function extractSubProcessInfo(callExpr, varName, ctx) {
2070
+ const arg = callExpr.arguments[0];
2071
+ if (!arg || !ts.isObjectLiteralExpression(arg))
2072
+ return null;
2073
+ let name;
2074
+ let path;
2075
+ let handlerNode;
2076
+ for (const prop of arg.properties) {
2077
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
2078
+ const key = prop.name.text;
2079
+ if (key === 'name' && ts.isStringLiteral(prop.initializer)) {
2080
+ name = prop.initializer.text;
2081
+ }
2082
+ else if (key === 'path' && ts.isStringLiteral(prop.initializer)) {
2083
+ path = prop.initializer.text;
2084
+ }
2085
+ else if (key === 'handler') {
2086
+ if (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer)) {
2087
+ handlerNode = prop.initializer;
2088
+ }
2089
+ }
2090
+ }
2091
+ else if (ts.isMethodDeclaration(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'handler') {
2092
+ // async handler(...) { ... } - method shorthand syntax
2093
+ const funcExpr = ts.factory.createFunctionExpression(prop.modifiers?.filter(ts.isModifier), prop.asteriskToken, undefined, prop.typeParameters, prop.parameters, prop.type, prop.body ?? ts.factory.createBlock([]));
2094
+ handlerNode = funcExpr;
2095
+ }
2096
+ }
2097
+ if (!name || !path || !handlerNode)
2098
+ return null;
2099
+ // Extract handler parameter names
2100
+ const handlerParams = handlerNode.parameters
2101
+ .filter(p => ts.isIdentifier(p.name))
2102
+ .map(p => p.name.text);
2103
+ // Analyze the subprocess handler body recursively
2104
+ const analysis = analyzeHandler(handlerNode, ctx.typeChecker);
2105
+ return { name, path, varName, handlerNode, handlerParams, analysis };
2106
+ }
2107
+ /**
2108
+ * Check if a call expression is a subprocess spawn (e.g., `await playerSeat('alice')`)
2109
+ * and if so, emit the SUBPROCESS_SPAWN opcode.
2110
+ *
2111
+ * `awaited` indicates whether the call expression's syntactic parent is an
2112
+ * AwaitExpression. The executor uses this to decide whether to suspend the
2113
+ * parent until the child reaches DONE (awaited) or continue immediately
2114
+ * (detached).
2115
+ */
2116
+ function trySubprocessSpawn(callExpr, storeVar, ctx, awaited) {
2117
+ if (!ts.isIdentifier(callExpr.expression))
2118
+ return false;
2119
+ const calleeName = callExpr.expression.text;
2120
+ // Check if this callee name matches a registered subprocess
2121
+ const subInfo = ctx.subprocesses.find(s => s.varName === calleeName);
2122
+ if (!subInfo)
2123
+ return false;
2124
+ const argExprs = Array.from(callExpr.arguments);
2125
+ flushBlock(ctx);
2126
+ emitOpcode(ctx, {
2127
+ op: 'SUBPROCESS_SPAWN',
2128
+ name: subInfo.name,
2129
+ argExprs,
2130
+ storeVar,
2131
+ awaited,
2132
+ }, callExpr);
2133
+ return true;
2134
+ }
2135
+ function checkPromiseCombinatorWithSignals(expr, ctx) {
2136
+ if (!ts.isCallExpression(expr))
2137
+ return null;
2138
+ const callee = expr.expression;
2139
+ if (!ts.isPropertyAccessExpression(callee))
2140
+ return null;
2141
+ // Check for Promise.all, Promise.race, Promise.any, Promise.allSettled
2142
+ const object = callee.expression;
2143
+ const method = callee.name.text;
2144
+ if (!ts.isIdentifier(object) || object.text !== 'Promise')
2145
+ return null;
2146
+ const combinators = ['all', 'race', 'any', 'allSettled'];
2147
+ if (!combinators.includes(method))
2148
+ return null;
2149
+ // Check if any argument contains a signal/delay call
2150
+ for (const arg of expr.arguments) {
2151
+ if (containsSignalOrDelayCall(arg, ctx)) {
2152
+ return `Promise.${method}`;
2153
+ }
2154
+ }
2155
+ return null;
2156
+ }
2157
+ /**
2158
+ * Check if an expression contains signal() or delay() calls (not awaited).
2159
+ */
2160
+ function containsSignalOrDelayCall(expr, ctx) {
2161
+ let found = false;
2162
+ const visit = (node) => {
2163
+ if (found)
2164
+ return;
2165
+ if (ts.isCallExpression(node)) {
2166
+ const primitive = getPrimitiveCall(node, ctx.typeChecker);
2167
+ if (primitive && (primitive.kind === 'signal' || primitive.kind === 'delay')) {
2168
+ found = true;
2169
+ return;
2170
+ }
2171
+ }
2172
+ ts.forEachChild(node, visit);
2173
+ };
2174
+ visit(expr);
2175
+ return found;
2176
+ }
2177
+ /**
2178
+ * Check if a call is a signal/waitFor call.
2179
+ */
2180
+ function isWaitForCall(node, ctx) {
2181
+ if (ctx) {
2182
+ return isPrimitiveSignalCall(node, ctx.typeChecker);
2183
+ }
2184
+ // Fallback for cases without context
2185
+ const expr = node.expression;
2186
+ if (ts.isIdentifier(expr)) {
2187
+ return expr.text === 'waitFor' || expr.text === 'signal';
2188
+ }
2189
+ return false;
2190
+ }
2191
+ /**
2192
+ * Check if a call is a delay call.
2193
+ */
2194
+ function isDelayCall(node, ctx) {
2195
+ if (ctx) {
2196
+ return isPrimitiveDelayCall(node, ctx.typeChecker);
2197
+ }
2198
+ const expr = node.expression;
2199
+ if (ts.isIdentifier(expr)) {
2200
+ return expr.text === 'delay';
2201
+ }
2202
+ return false;
2203
+ }
2204
+ /**
2205
+ * Check if a call is a race call.
2206
+ */
2207
+ function isRaceCall(node, ctx) {
2208
+ if (ctx) {
2209
+ return isPrimitiveRaceCall(node, ctx.typeChecker);
2210
+ }
2211
+ const expr = node.expression;
2212
+ if (ts.isIdentifier(expr)) {
2213
+ return expr.text === 'race';
2214
+ }
2215
+ return false;
2216
+ }
2217
+ /**
2218
+ * Check if a call is a signal.all() or signal.settled() call.
2219
+ */
2220
+ function isSignalCombinatorCall(node, ctx) {
2221
+ return isPrimitiveSignalCombinatorCall(node, ctx.typeChecker);
2222
+ }
2223
+ /**
2224
+ * Check if a call is a stream() call.
2225
+ */
2226
+ function isStreamCall(node, ctx) {
2227
+ return isPrimitiveStreamCall(node, ctx.typeChecker);
2228
+ }
2229
+ /**
2230
+ * Check if a call is a scope() call.
2231
+ */
2232
+ function isScopeCall(node, ctx) {
2233
+ return isPrimitiveScopeCall(node, ctx.typeChecker);
2234
+ }
2235
+ /**
2236
+ * TSP3001/TSP3002: Check if a for-of iterable is a durable iterator
2237
+ * (has __durableIterator brand) and validate it has orderBy + serializable cursor.
2238
+ *
2239
+ * Regular iterables (arrays, Sets) are allowed without checks.
2240
+ * Durable iterators (repository queries) must have orderBy for keyset pagination
2241
+ * and a serializable cursor type.
2242
+ */
2243
+ function checkDurableIteratorType(iterableExpr, ctx) {
2244
+ const type = ctx.typeChecker.getTypeAtLocation(iterableExpr);
2245
+ if (!type)
2246
+ return;
2247
+ // Check for __durableIterator brand property
2248
+ const durableProp = type.getProperty('__durableIterator');
2249
+ if (!durableProp)
2250
+ return; // Regular iterable - no validation needed
2251
+ // TSP3001: Durable iterator must have orderBy
2252
+ const orderByProp = type.getProperty('orderBy');
2253
+ if (!orderByProp) {
2254
+ ctx.diagnostics.add(ProcessErrorCode.InvalidDurableIterator, iterableExpr);
2255
+ return;
2256
+ }
2257
+ // Check if orderBy was actually called (type narrows from function to result)
2258
+ const orderByType = ctx.typeChecker.getTypeOfSymbolAtLocation(orderByProp, iterableExpr);
2259
+ // If orderBy is still a function type (not called), the query doesn't have ordering
2260
+ if (orderByType.getCallSignatures().length > 0) {
2261
+ ctx.diagnostics.add(ProcessErrorCode.InvalidDurableIterator, iterableExpr);
2262
+ return;
2263
+ }
2264
+ // TSP3002: Check cursor type is serializable
2265
+ const cursorProp = type.getProperty('__cursorType');
2266
+ if (cursorProp) {
2267
+ const cursorType = ctx.typeChecker.getTypeOfSymbolAtLocation(cursorProp, iterableExpr);
2268
+ if (!isCursorTypeSerializable(cursorType, ctx.typeChecker)) {
2269
+ const typeName = ctx.typeChecker.typeToString(cursorType);
2270
+ ctx.diagnostics.add(ProcessErrorCode.NonSerializableCursor, iterableExpr, typeName);
2271
+ }
2272
+ }
2273
+ }
2274
+ /**
2275
+ * Check if a cursor type is JSON-serializable (primitives or plain objects of primitives).
2276
+ */
2277
+ function isCursorTypeSerializable(type, checker) {
2278
+ // Primitives are serializable
2279
+ if (type.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.Null | ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BooleanLiteral)) {
2280
+ return true;
2281
+ }
2282
+ // Union types: all members must be serializable
2283
+ if (type.isUnion()) {
2284
+ return type.types.every(t => isCursorTypeSerializable(t, checker));
2285
+ }
2286
+ // Object types: all properties must be serializable, and no call signatures (not a function)
2287
+ if (type.flags & ts.TypeFlags.Object) {
2288
+ if (type.getCallSignatures().length > 0)
2289
+ return false;
2290
+ const properties = type.getProperties();
2291
+ return properties.every(prop => {
2292
+ const propType = checker.getTypeOfSymbol(prop);
2293
+ return isCursorTypeSerializable(propType, checker);
2294
+ });
2295
+ }
2296
+ return false;
2297
+ }
2298
+ /**
2299
+ * Analyze a scope() call.
2300
+ * Detects both signal-first and handler forms.
2301
+ * Emits SCOPE_START -> SCOPE_HANDLER (if handler form) -> SCOPE_END opcodes.
2302
+ */
2303
+ function analyzeScopeCall(call, storeVar, ctx) {
2304
+ const args = call.arguments;
2305
+ if (args.length < 2) {
2306
+ ctx.diagnostics.add(ProcessErrorCode.InvalidScopeArguments, call);
2307
+ return;
2308
+ }
2309
+ const scopeId = ctx.nextScopeBlockId++;
2310
+ // Detect which form: signal-first or handler
2311
+ // Signal-first: scope(svc.signal, entities, ?idFn)
2312
+ // Handler: scope(entities, handler) or scope(entities, idFn/alias, handler)
2313
+ const firstArg = args[0];
2314
+ const secondArg = args[1];
2315
+ const thirdArg = args[2];
2316
+ // Check if first arg is a signal reference (property access on a service)
2317
+ const signalInfo = ts.isPropertyAccessExpression(firstArg) ? extractSignalInfo(firstArg, ctx) : null;
2318
+ if (signalInfo) {
2319
+ // Signal-first form: scope(svc.signal, entities, ?idFn)
2320
+ ctx.signals[signalInfo.signalName] = {
2321
+ identity: signalInfo.identity,
2322
+ payloadType: signalInfo.payloadType,
2323
+ };
2324
+ emitOpcode(ctx, {
2325
+ op: 'SCOPE_START',
2326
+ scopeId,
2327
+ iterableExpr: secondArg,
2328
+ idExtractor: thirdArg,
2329
+ }, call);
2330
+ emitOpcode(ctx, {
2331
+ op: 'SCOPE_WAIT',
2332
+ scopeId,
2333
+ signalExpr: firstArg,
2334
+ }, call);
2335
+ }
2336
+ else {
2337
+ // Handler form: scope(entities, handler) or scope(entities, idFn/alias, handler)
2338
+ let handlerArg;
2339
+ let idExtractor;
2340
+ let paramAlias;
2341
+ if (thirdArg && (ts.isArrowFunction(thirdArg) || ts.isFunctionExpression(thirdArg))) {
2342
+ // scope(entities, idFn/alias, handler)
2343
+ if (ts.isStringLiteral(secondArg)) {
2344
+ paramAlias = secondArg.text;
2345
+ }
2346
+ else {
2347
+ idExtractor = secondArg;
2348
+ }
2349
+ handlerArg = thirdArg;
2350
+ }
2351
+ else if (ts.isArrowFunction(secondArg) || ts.isFunctionExpression(secondArg)) {
2352
+ // scope(entities, handler)
2353
+ handlerArg = secondArg;
2354
+ }
2355
+ else {
2356
+ ctx.diagnostics.add(ProcessErrorCode.InvalidScopeArguments, call);
2357
+ return;
2358
+ }
2359
+ // TSP3007: Check handler for yield expressions
2360
+ if (ctx.isGenerator && containsYieldExpression(handlerArg)) {
2361
+ ctx.diagnostics.add(ProcessErrorCode.ScopeHandlerCannotYield, handlerArg);
2362
+ }
2363
+ // TSP3009: Check for nested scope collision
2364
+ // (scopeStack tracks active scope IDs - we'd need model type tracking for true collision detection,
2365
+ // but for now we just prevent any nested scope calls)
2366
+ if (ctx.scopeStack.length > 0) {
2367
+ ctx.diagnostics.add(ProcessErrorCode.NestedScopeCollision, call);
2368
+ }
2369
+ // Walk the handler body for nested scope() calls. The handler body
2370
+ // is emitted as a single SCOPE_HANDLER opcode without recursive
2371
+ // statement analysis, so the TSP3009 check above would never fire
2372
+ // for nested scopes. Detect them with a targeted AST walk while the
2373
+ // outer scopeId is on the stack.
2374
+ const checkNestedScopeIn = (node) => {
2375
+ if (ts.isCallExpression(node) &&
2376
+ ts.isIdentifier(node.expression) &&
2377
+ node.expression.text === 'scope') {
2378
+ ctx.diagnostics.add(ProcessErrorCode.NestedScopeCollision, node);
2379
+ }
2380
+ ts.forEachChild(node, checkNestedScopeIn);
2381
+ };
2382
+ ts.forEachChild(handlerArg, checkNestedScopeIn);
2383
+ // Extract handler parameter names
2384
+ const handlerFn = handlerArg;
2385
+ const handlerParams = handlerFn.parameters.map(p => ts.isIdentifier(p.name) ? p.name.text : `__scope_${scopeId}_param`);
2386
+ // Extract the handler body
2387
+ const handlerBody = ts.isArrowFunction(handlerArg) || ts.isFunctionExpression(handlerArg)
2388
+ ? (ts.isBlock(handlerArg.body) ? handlerArg.body : undefined)
2389
+ : undefined;
2390
+ if (!handlerBody) {
2391
+ // Expression body - wrap as block
2392
+ const body = handlerArg.body;
2393
+ const returnStmt = ts.factory.createReturnStatement(body);
2394
+ const block = ts.factory.createBlock([returnStmt], true);
2395
+ emitOpcode(ctx, {
2396
+ op: 'SCOPE_START',
2397
+ scopeId,
2398
+ iterableExpr: firstArg,
2399
+ idExtractor,
2400
+ paramAlias,
2401
+ }, call);
2402
+ ctx.scopeStack.push(scopeId);
2403
+ emitOpcode(ctx, {
2404
+ op: 'SCOPE_HANDLER',
2405
+ scopeId,
2406
+ handlerBody: block,
2407
+ handlerParams,
2408
+ }, call);
2409
+ ctx.scopeStack.pop();
2410
+ }
2411
+ else {
2412
+ emitOpcode(ctx, {
2413
+ op: 'SCOPE_START',
2414
+ scopeId,
2415
+ iterableExpr: firstArg,
2416
+ idExtractor,
2417
+ paramAlias,
2418
+ }, call);
2419
+ ctx.scopeStack.push(scopeId);
2420
+ emitOpcode(ctx, {
2421
+ op: 'SCOPE_HANDLER',
2422
+ scopeId,
2423
+ handlerBody,
2424
+ handlerParams,
2425
+ }, call);
2426
+ ctx.scopeStack.pop();
2427
+ }
2428
+ }
2429
+ const resultVar = storeVar ?? `__scope_${scopeId}_result`;
2430
+ emitOpcode(ctx, {
2431
+ op: 'SCOPE_END',
2432
+ scopeId,
2433
+ resultVar,
2434
+ }, call);
2435
+ if (storeVar) {
2436
+ emitOpcode(ctx, { op: 'STORE', var: storeVar, fromBlock: true }, call);
2437
+ }
2438
+ }
2439
+ /**
2440
+ * Check if an expression (function body) contains yield expressions.
2441
+ */
2442
+ function containsYieldExpression(node) {
2443
+ let found = false;
2444
+ const visit = (n) => {
2445
+ if (found)
2446
+ return;
2447
+ if (ts.isYieldExpression(n)) {
2448
+ found = true;
2449
+ return;
2450
+ }
2451
+ // Don't descend into nested functions
2452
+ if (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) {
2453
+ return;
2454
+ }
2455
+ ts.forEachChild(n, visit);
2456
+ };
2457
+ ts.forEachChild(node, visit);
2458
+ return found;
2459
+ }
2460
+ /**
2461
+ * Analyze a signal.all() or signal.settled() call.
2462
+ * Emits PARALLEL_START, PARALLEL_WAIT, PARALLEL_COLLECT opcodes.
2463
+ */
2464
+ function analyzeSignalCombinator(call, storeVar, ctx) {
2465
+ const callee = call.expression;
2466
+ if (!ts.isPropertyAccessExpression(callee))
2467
+ return;
2468
+ const methodName = callee.name.text;
2469
+ const isSettled = methodName === 'settled';
2470
+ const arg = call.arguments[0];
2471
+ if (!arg)
2472
+ return;
2473
+ const parallelId = ctx.nextParallelId++;
2474
+ const branches = [];
2475
+ let isObjectForm = false;
2476
+ // Determine if it's array form or object form
2477
+ if (ts.isArrayLiteralExpression(arg)) {
2478
+ // Array form: signal.all([svc.a, svc.b, () => doWork()])
2479
+ isObjectForm = false;
2480
+ arg.elements.forEach((element, index) => {
2481
+ const branch = extractParallelBranch(element, index, ctx);
2482
+ if (branch) {
2483
+ branches.push(branch);
2484
+ }
2485
+ });
2486
+ }
2487
+ else if (ts.isObjectLiteralExpression(arg)) {
2488
+ // Object form: signal.all({ payment: svc.paid, shipping: svc.shipped })
2489
+ isObjectForm = true;
2490
+ for (const prop of arg.properties) {
2491
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
2492
+ const key = prop.name.text;
2493
+ const branch = extractParallelBranch(prop.initializer, key, ctx);
2494
+ if (branch) {
2495
+ branches.push(branch);
2496
+ }
2497
+ }
2498
+ else if (ts.isShorthandPropertyAssignment(prop)) {
2499
+ // { svc.paid } shorthand
2500
+ const key = prop.name.text;
2501
+ const branch = extractParallelBranch(prop.name, key, ctx);
2502
+ if (branch) {
2503
+ branches.push(branch);
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+ // Validate non-empty branches
2509
+ if (branches.length === 0) {
2510
+ ctx.diagnostics.add(ProcessErrorCode.EmptyParallelBlock, call);
2511
+ return;
2512
+ }
2513
+ // Emit PARALLEL_START
2514
+ emitOpcode(ctx, {
2515
+ op: 'PARALLEL_START',
2516
+ parallelId,
2517
+ branches,
2518
+ isSettled,
2519
+ }, call);
2520
+ // Emit PARALLEL_WAIT - suspends until all branches complete
2521
+ emitOpcode(ctx, {
2522
+ op: 'PARALLEL_WAIT',
2523
+ parallelId,
2524
+ }, call);
2525
+ // Emit PARALLEL_COLLECT - collects results into the store variable
2526
+ const resultVar = storeVar ?? `__parallel_${parallelId}`;
2527
+ emitOpcode(ctx, {
2528
+ op: 'PARALLEL_COLLECT',
2529
+ parallelId,
2530
+ resultVar,
2531
+ isObject: isObjectForm,
2532
+ }, call);
2533
+ if (storeVar) {
2534
+ emitOpcode(ctx, { op: 'STORE', var: storeVar, fromBlock: true }, call);
2535
+ }
2536
+ }
2537
+ /**
2538
+ * Extract a parallel branch from an expression.
2539
+ * Handles signal references, delay calls, and async functions.
2540
+ */
2541
+ function extractParallelBranch(expr, id, ctx) {
2542
+ // Check for signal reference (property access like svc.paid)
2543
+ if (ts.isPropertyAccessExpression(expr)) {
2544
+ const signalInfo = extractSignalInfo(expr, ctx);
2545
+ if (signalInfo) {
2546
+ ctx.signals[signalInfo.signalName] = {
2547
+ identity: signalInfo.identity,
2548
+ payloadType: signalInfo.payloadType,
2549
+ };
2550
+ return {
2551
+ id,
2552
+ expr,
2553
+ type: 'signal',
2554
+ };
2555
+ }
2556
+ }
2557
+ // Check for delay call
2558
+ if (ts.isCallExpression(expr) && isDelayCall(expr, ctx)) {
2559
+ return {
2560
+ id,
2561
+ expr,
2562
+ type: 'delay',
2563
+ };
2564
+ }
2565
+ // Check for async arrow function: async () => { ... }
2566
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
2567
+ return {
2568
+ id,
2569
+ expr,
2570
+ type: 'function',
2571
+ };
2572
+ }
2573
+ // Check for function call that might be a signal reference
2574
+ if (ts.isCallExpression(expr)) {
2575
+ const signalInfo = extractSignalInfo(expr, ctx);
2576
+ if (signalInfo) {
2577
+ ctx.signals[signalInfo.signalName] = {
2578
+ identity: signalInfo.identity,
2579
+ payloadType: signalInfo.payloadType,
2580
+ };
2581
+ return {
2582
+ id,
2583
+ expr,
2584
+ type: 'signal',
2585
+ };
2586
+ }
2587
+ // Otherwise treat as a function call
2588
+ return {
2589
+ id,
2590
+ expr,
2591
+ type: 'function',
2592
+ };
2593
+ }
2594
+ // Identifier reference (could be a signal variable)
2595
+ if (ts.isIdentifier(expr)) {
2596
+ const signalInfo = extractSignalInfo(expr, ctx);
2597
+ if (signalInfo) {
2598
+ ctx.signals[signalInfo.signalName] = {
2599
+ identity: signalInfo.identity,
2600
+ payloadType: signalInfo.payloadType,
2601
+ };
2602
+ return {
2603
+ id,
2604
+ expr,
2605
+ type: 'signal',
2606
+ };
2607
+ }
2608
+ }
2609
+ return null;
2610
+ }
2611
+ /**
2612
+ * Extract a stable, comment-free name from an expression node used as a
2613
+ * signal service or identity argument. Prefers AST-walked names over raw
2614
+ * `.getText()` (which includes whitespace + comments from the source).
2615
+ *
2616
+ * Falls back to `.getText().trim()` for shapes we don't recognize so we
2617
+ * don't lose functionality on legacy patterns; the trim at least keeps
2618
+ * stray whitespace out of registered metadata.
2619
+ */
2620
+ export function expressionToName(node) {
2621
+ if (ts.isIdentifier(node))
2622
+ return node.text;
2623
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
2624
+ return node.text;
2625
+ }
2626
+ if (ts.isPropertyAccessExpression(node)) {
2627
+ return `${expressionToName(node.expression)}.${node.name.text}`;
2628
+ }
2629
+ if (node.kind === ts.SyntaxKind.ThisKeyword)
2630
+ return 'this';
2631
+ // Unknown shape — fall back but trim source artefacts.
2632
+ return node.getText().trim();
2633
+ }
2634
+ function extractSignalInfo(arg, ctx) {
2635
+ // Pattern 1: signal(service.signalName) - property access directly
2636
+ // e.g., signal(orders.paid), signal(twofa.codeSubmitted)
2637
+ if (ts.isPropertyAccessExpression(arg)) {
2638
+ const serviceName = expressionToName(arg.expression);
2639
+ const methodName = arg.name.text;
2640
+ // Get payload type from type checker
2641
+ const type = ctx.typeChecker.getTypeAtLocation(arg);
2642
+ const payloadType = ctx.typeChecker.typeToString(type);
2643
+ // defineSignals builder chain: BuiltSignal carries `readonly path: TPath`
2644
+ // and `readonly typesConfig: TTypes` as literal-typed properties. When
2645
+ // present, derive the routing name and identity params from the path so
2646
+ // metadata matches what `buildSignal()` registers at runtime.
2647
+ const builderInfo = extractDefineSignalsInfo(type, ctx);
2648
+ if (builderInfo) {
2649
+ return {
2650
+ signalName: builderInfo.signalName,
2651
+ identity: builderInfo.identity,
2652
+ payloadType,
2653
+ };
2654
+ }
2655
+ return { signalName: `${serviceName}.${methodName}`, identity: [], payloadType };
2656
+ }
2657
+ // Pattern 2: signal(service.signalName(identityArgs)) - call expression
2658
+ // e.g., signal(payments.received(orderId)) - legacy pattern
2659
+ if (ts.isCallExpression(arg)) {
2660
+ const callExpr = arg.expression;
2661
+ if (ts.isPropertyAccessExpression(callExpr)) {
2662
+ const serviceName = expressionToName(callExpr.expression);
2663
+ const methodName = callExpr.name.text;
2664
+ const signalName = `${serviceName}.${methodName}`;
2665
+ // Extract identity from arguments — AST-walk each rather than
2666
+ // `.getText()` so the registered identity metadata is just the
2667
+ // name, with no source-text whitespace / inline comments leaking
2668
+ // in. Path-param identifiers and string literals dominate this
2669
+ // position; the fallback handles oddballs.
2670
+ const identity = arg.arguments.map(expressionToName);
2671
+ // Get payload type from type checker
2672
+ const type = ctx.typeChecker.getTypeAtLocation(arg);
2673
+ const payloadType = ctx.typeChecker.typeToString(type);
2674
+ return { signalName, identity, payloadType };
2675
+ }
2676
+ }
2677
+ // Pattern 3: signal(signalVariable) - identifier reference to createSignal export
2678
+ // e.g., signal(r, emailVerified) where emailVerified = createSignal('auth.email.verified', ...)
2679
+ if (ts.isIdentifier(arg)) {
2680
+ // Try to find the signal name from the symbol's declaration
2681
+ const symbol = ctx.typeChecker.getSymbolAtLocation(arg);
2682
+ if (symbol) {
2683
+ const declarations = symbol.getDeclarations();
2684
+ if (declarations && declarations.length > 0) {
2685
+ const decl = declarations[0];
2686
+ // Look for: const signalName = createSignal('signal.name', ...)
2687
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
2688
+ if (ts.isCallExpression(decl.initializer)) {
2689
+ const callExpr = decl.initializer.expression;
2690
+ if (ts.isIdentifier(callExpr) && callExpr.text === 'createSignal') {
2691
+ // First argument is the signal name
2692
+ const signalNameArg = decl.initializer.arguments[0];
2693
+ if (signalNameArg && ts.isStringLiteral(signalNameArg)) {
2694
+ const signalName = signalNameArg.text;
2695
+ // Second argument is the identity keys array
2696
+ const identityArg = decl.initializer.arguments[1];
2697
+ let identity = [];
2698
+ if (identityArg && ts.isArrayLiteralExpression(identityArg)) {
2699
+ identity = identityArg.elements
2700
+ .filter(ts.isStringLiteral)
2701
+ .map(e => e.text);
2702
+ }
2703
+ // Get payload type from type checker
2704
+ const type = ctx.typeChecker.getTypeAtLocation(arg);
2705
+ const payloadType = ctx.typeChecker.typeToString(type);
2706
+ return { signalName, identity, payloadType };
2707
+ }
2708
+ }
2709
+ }
2710
+ }
2711
+ }
2712
+ }
2713
+ // Fallback: use identifier name as signal name
2714
+ const signalName = arg.text;
2715
+ const type = ctx.typeChecker.getTypeAtLocation(arg);
2716
+ const payloadType = ctx.typeChecker.typeToString(type);
2717
+ return { signalName, identity: [], payloadType };
2718
+ }
2719
+ return null;
2720
+ }
2721
+ /**
2722
+ * If `type` is a `BuiltSignal<TPath, ...>` from defineSignals, extract the
2723
+ * literal path and convert it into a routing name + identity params, mirroring
2724
+ * `pathToSignalName` / `extractPathParams` in
2725
+ * `packages/core/core/src/process/define-signals.ts`.
2726
+ *
2727
+ * Returns null when the type isn't a builder chain (preserves the legacy
2728
+ * `service.method` naming for callers).
2729
+ */
2730
+ function extractDefineSignalsInfo(type, ctx) {
2731
+ const pathSym = type.getProperty('path');
2732
+ if (!pathSym)
2733
+ return null;
2734
+ const pathDecls = pathSym.getDeclarations();
2735
+ if (!pathDecls || pathDecls.length === 0)
2736
+ return null;
2737
+ const pathType = ctx.typeChecker.getTypeOfSymbolAtLocation(pathSym, pathDecls[0]);
2738
+ if (!pathType.isStringLiteral())
2739
+ return null;
2740
+ const path = pathType.value;
2741
+ return {
2742
+ signalName: pathToSignalName(path),
2743
+ identity: extractPathParams(path),
2744
+ };
2745
+ }
2746
+ /** Mirror of `pathToSignalName` in process/define-signals.ts (compile-time copy). */
2747
+ function pathToSignalName(path) {
2748
+ if (!path)
2749
+ return 'anonymous';
2750
+ return path
2751
+ .replace(/^\//, '')
2752
+ .replace(/\//g, '.')
2753
+ .replace(/:/g, '');
2754
+ }
2755
+ /** Mirror of `extractPathParams` in process/define-signals.ts (compile-time copy). */
2756
+ function extractPathParams(path) {
2757
+ if (!path)
2758
+ return [];
2759
+ const params = [];
2760
+ const regex = /:([a-zA-Z_][a-zA-Z0-9_]*)/g;
2761
+ let match;
2762
+ while ((match = regex.exec(path)) !== null) {
2763
+ params.push(match[1]);
2764
+ }
2765
+ return params;
2766
+ }
2767
+ /**
2768
+ * Extract delay info from a delay.unit(r, value) call.
2769
+ * Returns the unit and the value expression.
2770
+ */
2771
+ function extractDelayInfo(call) {
2772
+ const callee = call.expression;
2773
+ // Pattern: delay.minutes(r, 5) or delay.minutes(5)
2774
+ if (!ts.isPropertyAccessExpression(callee)) {
2775
+ return null;
2776
+ }
2777
+ const unit = callee.name.text;
2778
+ if (unit !== 'seconds' && unit !== 'minutes' && unit !== 'hours' && unit !== 'days') {
2779
+ return null;
2780
+ }
2781
+ // Get the value expression - it's either the first arg (simple) or second arg (race pattern)
2782
+ const args = call.arguments;
2783
+ if (args.length === 0) {
2784
+ return null;
2785
+ }
2786
+ // If 2 args, first is racer, second is value. If 1 arg, it's the value.
2787
+ const valueExpr = args.length >= 2 ? args[1] : args[0];
2788
+ return { unit, valueExpr };
2789
+ }
2790
+ /**
2791
+ * Extract stream info from a stream(r, entity.field) call.
2792
+ * Returns a signal name with wildcard entity ID: stream:ModelName:*:fieldName
2793
+ *
2794
+ * The wildcard (*) is resolved at runtime using the process's identity.
2795
+ */
2796
+ function extractStreamInfo(arg, ctx) {
2797
+ // Pattern: stream(r, entity.streamField) - property access on model entity
2798
+ // e.g., stream(r, order.statusUpdates) where order: Order
2799
+ if (ts.isPropertyAccessExpression(arg)) {
2800
+ const fieldName = arg.name.text;
2801
+ // Get the entity's type to determine the model name
2802
+ const entityType = ctx.typeChecker.getTypeAtLocation(arg.expression);
2803
+ let modelName = extractModelNameFromType(entityType, ctx.typeChecker);
2804
+ // If extractModelNameFromType returned a wrapper type (Persistent, __type, etc.),
2805
+ // trace back through the variable declaration to find the Ref<Model> source type.
2806
+ // Pattern: `using room = await roomRef` -> roomRef: Ref<Room> -> extract "Room"
2807
+ if (modelName === 'Persistent' || modelName.startsWith('__') || modelName.startsWith('{')) {
2808
+ const refModelName = extractModelNameFromRefSource(arg.expression, ctx);
2809
+ if (refModelName) {
2810
+ modelName = refModelName;
2811
+ }
2812
+ }
2813
+ // Generate wildcard signal name: stream:ModelName:*:fieldName
2814
+ // The * is resolved at runtime from process identity
2815
+ const signalName = `stream:${modelName}:*:${fieldName}`;
2816
+ // Get the stream's element type (Stream<T> -> T)
2817
+ const streamType = ctx.typeChecker.getTypeAtLocation(arg);
2818
+ let payloadType = ctx.typeChecker.typeToString(streamType);
2819
+ // Try to extract the inner type from AsyncIterable<T> or Stream<T>
2820
+ // TypeChecker returns the full type, but we want the element type
2821
+ if (streamType.aliasTypeArguments && streamType.aliasTypeArguments.length > 0) {
2822
+ payloadType = ctx.typeChecker.typeToString(streamType.aliasTypeArguments[0]);
2823
+ }
2824
+ else if (streamType.typeArguments) {
2825
+ const typeArgs = streamType.typeArguments;
2826
+ if (typeArgs && typeArgs.length > 0) {
2827
+ payloadType = ctx.typeChecker.typeToString(typeArgs[0]);
2828
+ }
2829
+ }
2830
+ return {
2831
+ signalName,
2832
+ identity: [], // Identity is resolved at runtime from process path params
2833
+ payloadType,
2834
+ };
2835
+ }
2836
+ return null;
2837
+ }
2838
+ /**
2839
+ * Extract model name by tracing a variable back to its Ref<Model> source.
2840
+ *
2841
+ * For `using room = await roomRef`, finds roomRef's type Ref<Room> and extracts "Room".
2842
+ * Also handles direct Ref types and process handler destructured params.
2843
+ */
2844
+ function extractModelNameFromRefSource(expr, ctx) {
2845
+ if (!ts.isIdentifier(expr))
2846
+ return null;
2847
+ const symbol = ctx.typeChecker.getSymbolAtLocation(expr);
2848
+ if (!symbol)
2849
+ return null;
2850
+ const declarations = symbol.getDeclarations();
2851
+ if (!declarations || declarations.length === 0)
2852
+ return null;
2853
+ for (const decl of declarations) {
2854
+ // Pattern: `using room = await roomRef` - check the initializer
2855
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
2856
+ let initExpr = decl.initializer;
2857
+ // Unwrap `await` expression
2858
+ if (ts.isAwaitExpression(initExpr)) {
2859
+ initExpr = initExpr.expression;
2860
+ }
2861
+ // Strategy 1: Extract from Ref type arguments
2862
+ const refType = ctx.typeChecker.getTypeAtLocation(initExpr);
2863
+ const refModelName = extractModelNameFromRefType(refType, ctx.typeChecker);
2864
+ if (refModelName)
2865
+ return refModelName;
2866
+ // Strategy 2: Extract from ref variable name convention
2867
+ // `roomRef` -> remove "Ref" suffix -> "room" -> capitalize -> "Room"
2868
+ if (ts.isIdentifier(initExpr)) {
2869
+ const refVarName = initExpr.text;
2870
+ if (refVarName.endsWith('Ref')) {
2871
+ const base = refVarName.slice(0, -3); // Remove "Ref"
2872
+ if (base.length > 0) {
2873
+ return base.charAt(0).toUpperCase() + base.slice(1);
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+ }
2879
+ return null;
2880
+ }
2881
+ /**
2882
+ * Extract model name from a Ref<Model> type.
2883
+ * Checks symbol name, alias type arguments, and string parsing.
2884
+ */
2885
+ function extractModelNameFromRefType(type, typeChecker) {
2886
+ const typeStr = typeChecker.typeToString(type);
2887
+ // Try Ref<Model> pattern in type string
2888
+ const refMatch = typeStr.match(/Ref<(?:typeof\s+)?(\w+)>/);
2889
+ if (refMatch) {
2890
+ return refMatch[1];
2891
+ }
2892
+ // Check alias type arguments (Ref<T> preserves T)
2893
+ if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
2894
+ const innerType = type.aliasTypeArguments[0];
2895
+ const innerSymbol = innerType.getSymbol() || innerType.aliasSymbol;
2896
+ const name = innerSymbol?.getName();
2897
+ if (name && !name.startsWith('__') && name !== 'Persistent') {
2898
+ return name;
2899
+ }
2900
+ }
2901
+ // Check generic type arguments
2902
+ const typeRef = type;
2903
+ const typeArgs = typeChecker.getTypeArguments?.(typeRef) ?? [];
2904
+ if (typeArgs.length > 0) {
2905
+ const innerSymbol = typeArgs[0].getSymbol() || typeArgs[0].aliasSymbol;
2906
+ const name = innerSymbol?.getName();
2907
+ if (name && !name.startsWith('__') && name !== 'Persistent') {
2908
+ return name;
2909
+ }
2910
+ }
2911
+ return null;
2912
+ }
2913
+ /**
2914
+ * Extract the model name from a type.
2915
+ * Handles: Persistent<M> wrappers, Model type aliases, interfaces with __modelName property,
2916
+ * or falls back to type symbol name.
2917
+ */
2918
+ function extractModelNameFromType(type, typeChecker) {
2919
+ // Check if this is a Persistent<M> type reference - extract M
2920
+ // Persistent<typeof Room> should extract "Room", not "Persistent"
2921
+ const symbol = type.getSymbol() || type.aliasSymbol;
2922
+ const symbolName = symbol?.getName();
2923
+ if (symbolName === 'Persistent') {
2924
+ // Try to get the type argument (the model type inside Persistent<M>)
2925
+ const typeRef = type;
2926
+ const typeArgs = typeChecker.getTypeArguments?.(typeRef) ?? [];
2927
+ if (typeArgs.length > 0) {
2928
+ return extractModelNameFromType(typeArgs[0], typeChecker);
2929
+ }
2930
+ // Persistent<T> is a type alias that resolves to an intersection.
2931
+ // When resolved, getTypeArguments returns empty. Try alias type arguments.
2932
+ if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
2933
+ return extractModelNameFromType(type.aliasTypeArguments[0], typeChecker);
2934
+ }
2935
+ // For intersection types (resolved Persistent<T>), look for __modelName
2936
+ // in the constituent types
2937
+ if (type.isIntersection()) {
2938
+ for (const constituent of type.types) {
2939
+ const modelNameProp = constituent.getProperty('__modelName');
2940
+ if (modelNameProp) {
2941
+ const propType = typeChecker.getTypeOfSymbol(modelNameProp);
2942
+ if (propType.isStringLiteral()) {
2943
+ return propType.value;
2944
+ }
2945
+ }
2946
+ }
2947
+ }
2948
+ // Try __modelName directly on the type
2949
+ const modelNameProp = type.getProperty('__modelName');
2950
+ if (modelNameProp) {
2951
+ const propType = typeChecker.getTypeOfSymbol(modelNameProp);
2952
+ if (propType.isStringLiteral()) {
2953
+ return propType.value;
2954
+ }
2955
+ }
2956
+ // Try string parsing as last resort
2957
+ const typeString = typeChecker.typeToString(type);
2958
+ const persistentMatch = typeString.match(/Persistent<(?:typeof\s+)?(\w+)>/);
2959
+ if (persistentMatch) {
2960
+ return persistentMatch[1];
2961
+ }
2962
+ }
2963
+ // For non-Persistent types, try the symbol name
2964
+ if (symbol) {
2965
+ const name = symbol.getName();
2966
+ // Skip __type and other synthetic names
2967
+ if (name && !name.startsWith('__')) {
2968
+ return name;
2969
+ }
2970
+ }
2971
+ // Try to find a __modelName property on the type (from defineModel)
2972
+ const modelNameProp = type.getProperty('__modelName');
2973
+ if (modelNameProp) {
2974
+ const propType = typeChecker.getTypeOfSymbol(modelNameProp);
2975
+ if (propType.isStringLiteral()) {
2976
+ return propType.value;
2977
+ }
2978
+ }
2979
+ // Fallback: use the stringified type and extract likely model name
2980
+ const typeString = typeChecker.typeToString(type);
2981
+ // Remove Persistent<...> wrapper if present
2982
+ const persistentMatch = typeString.match(/Persistent<(?:typeof\s+)?(\w+)>/);
2983
+ if (persistentMatch) {
2984
+ return persistentMatch[1];
2985
+ }
2986
+ // Use the type string as-is (might be an interface name or type alias)
2987
+ return typeString;
2988
+ }
2989
+ function extractDependencies(expr, ctx) {
2990
+ const deps = [];
2991
+ const visit = (node) => {
2992
+ if (ts.isIdentifier(node)) {
2993
+ const varInfo = ctx.variables.get(node.text);
2994
+ if (varInfo) {
2995
+ deps.push(node.text);
2996
+ }
2997
+ }
2998
+ ts.forEachChild(node, visit);
2999
+ };
3000
+ visit(expr);
3001
+ return deps;
3002
+ }
3003
+ function isSerializableType(decl, typeChecker) {
3004
+ const type = typeChecker.getTypeAtLocation(decl);
3005
+ const typeString = typeChecker.typeToString(type);
3006
+ // Simple heuristic: primitives and plain objects are serializable
3007
+ // Complex types like functions, symbols, classes are not
3008
+ const nonSerializable = ['Function', 'Symbol', 'Promise', 'AsyncGenerator'];
3009
+ for (const ns of nonSerializable) {
3010
+ if (typeString.includes(ns)) {
3011
+ return false;
3012
+ }
3013
+ }
3014
+ return true;
3015
+ }
3016
+ /**
3017
+ * Check if a type is JSON-serializable (can be safely stored in process state).
3018
+ *
3019
+ * JSON-serializable types include:
3020
+ * - Primitives: string, number, boolean, null, undefined
3021
+ * - Arrays of serializable types
3022
+ * - Plain objects with serializable properties
3023
+ *
3024
+ * Non-serializable types include:
3025
+ * - Functions
3026
+ * - Symbols
3027
+ * - Classes with methods (model instances, services, etc.)
3028
+ * - Promises, AsyncGenerators
3029
+ * - Types with non-serializable properties
3030
+ */
3031
+ function isJsonSerializable(decl, typeChecker) {
3032
+ const type = typeChecker.getTypeAtLocation(decl);
3033
+ return isTypeJsonSerializable(type, typeChecker, new Set());
3034
+ }
3035
+ /**
3036
+ * True when the type resolves to any/unknown/error - i.e., the type checker
3037
+ * has no usable information.
3038
+ */
3039
+ function hasNoTypeInformation(decl, typeChecker) {
3040
+ const type = typeChecker.getTypeAtLocation(decl);
3041
+ const flags = type.getFlags();
3042
+ return (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
3043
+ }
3044
+ function isTypeJsonSerializable(type, typeChecker, visited) {
3045
+ // Prevent infinite recursion with recursive types
3046
+ if (visited.has(type))
3047
+ return true;
3048
+ visited.add(type);
3049
+ const flags = type.getFlags();
3050
+ // Primitives are always serializable
3051
+ if (flags & ts.TypeFlags.String ||
3052
+ flags & ts.TypeFlags.Number ||
3053
+ flags & ts.TypeFlags.Boolean ||
3054
+ flags & ts.TypeFlags.Null ||
3055
+ flags & ts.TypeFlags.Undefined ||
3056
+ flags & ts.TypeFlags.Void ||
3057
+ flags & ts.TypeFlags.BigInt ||
3058
+ flags & ts.TypeFlags.StringLiteral ||
3059
+ flags & ts.TypeFlags.NumberLiteral ||
3060
+ flags & ts.TypeFlags.BooleanLiteral) {
3061
+ return true;
3062
+ }
3063
+ // Union types: all members must be serializable
3064
+ if (type.isUnion()) {
3065
+ return type.types.every(t => isTypeJsonSerializable(t, typeChecker, visited));
3066
+ }
3067
+ // Intersection types: check if result is serializable
3068
+ if (type.isIntersection()) {
3069
+ return type.types.every(t => isTypeJsonSerializable(t, typeChecker, visited));
3070
+ }
3071
+ // Check for array types
3072
+ if (typeChecker.isArrayType(type)) {
3073
+ const elementType = typeChecker.getTypeArguments(type)[0];
3074
+ if (elementType) {
3075
+ return isTypeJsonSerializable(elementType, typeChecker, visited);
3076
+ }
3077
+ return true;
3078
+ }
3079
+ // Check for tuple types
3080
+ if (typeChecker.isTupleType(type)) {
3081
+ const typeArgs = typeChecker.getTypeArguments(type);
3082
+ return typeArgs.every(t => isTypeJsonSerializable(t, typeChecker, visited));
3083
+ }
3084
+ // Object types: check properties
3085
+ if (flags & ts.TypeFlags.Object) {
3086
+ const objectType = type;
3087
+ const objectFlags = objectType.objectFlags;
3088
+ // Reject function types
3089
+ if (objectFlags & ts.ObjectFlags.Anonymous) {
3090
+ const callSignatures = type.getCallSignatures();
3091
+ if (callSignatures.length > 0) {
3092
+ return false; // It's a function type
3093
+ }
3094
+ }
3095
+ // Check the type string for known non-serializable patterns
3096
+ const typeString = typeChecker.typeToString(type);
3097
+ // Reject known non-serializable types
3098
+ const nonSerializablePatterns = [
3099
+ 'Promise<',
3100
+ 'AsyncGenerator',
3101
+ 'Generator',
3102
+ 'Symbol',
3103
+ 'Map<',
3104
+ 'Set<',
3105
+ 'WeakMap',
3106
+ 'WeakSet',
3107
+ 'ArrayBuffer',
3108
+ 'DataView',
3109
+ 'Int8Array',
3110
+ 'Uint8Array',
3111
+ 'Float32Array',
3112
+ 'Float64Array',
3113
+ 'RegExp',
3114
+ 'Error',
3115
+ 'Date', // Date needs special handling for JSON
3116
+ ];
3117
+ for (const pattern of nonSerializablePatterns) {
3118
+ if (typeString.includes(pattern)) {
3119
+ return false;
3120
+ }
3121
+ }
3122
+ // Check all properties are serializable
3123
+ const properties = type.getProperties();
3124
+ for (const prop of properties) {
3125
+ // Skip methods (properties with call signatures)
3126
+ const propType = typeChecker.getTypeOfSymbol(prop);
3127
+ const propCallSigs = propType.getCallSignatures();
3128
+ if (propCallSigs.length > 0) {
3129
+ return false; // Has methods - not plain data
3130
+ }
3131
+ // Check property type is serializable
3132
+ if (!isTypeJsonSerializable(propType, typeChecker, visited)) {
3133
+ return false;
3134
+ }
3135
+ }
3136
+ return true;
3137
+ }
3138
+ // Default: assume not serializable for safety
3139
+ return false;
3140
+ }
3141
+ function trackUsedVariables(stmt, ctx) {
3142
+ const visit = (node) => {
3143
+ if (ts.isIdentifier(node)) {
3144
+ const varInfo = ctx.variables.get(node.text);
3145
+ if (varInfo?.isUsing) {
3146
+ ctx.currentBlockUses.add(node.text);
3147
+ }
3148
+ }
3149
+ ts.forEachChild(node, visit);
3150
+ };
3151
+ visit(stmt);
3152
+ }
3153
+ function getRehydrationDepsAtPoint(ctx) {
3154
+ const deps = [];
3155
+ for (const [name, info] of ctx.variables) {
3156
+ if (info.isUsing) {
3157
+ deps.push(name);
3158
+ }
3159
+ }
3160
+ return deps;
3161
+ }
3162
+ /**
3163
+ * Create a block and track which using variables are used in it.
3164
+ */
3165
+ function createBlock(ctx, statements, uses) {
3166
+ const blockId = ctx.blocks.length;
3167
+ const blockUses = uses ?? Array.from(ctx.currentBlockUses);
3168
+ ctx.blocks.push({
3169
+ id: blockId,
3170
+ uses: blockUses,
3171
+ statements,
3172
+ });
3173
+ // Update usedInBlocks for each using variable
3174
+ for (const varName of blockUses) {
3175
+ const varInfo = ctx.variables.get(varName);
3176
+ if (varInfo && varInfo.isUsing) {
3177
+ varInfo.usedInBlocks.push(blockId);
3178
+ }
3179
+ }
3180
+ return blockId;
3181
+ }
3182
+ function flushBlock(ctx) {
3183
+ if (ctx.currentBlockStatements.length > 0) {
3184
+ const statements = ctx.currentBlockStatements;
3185
+ const blockId = createBlock(ctx, statements);
3186
+ // Use first statement as source position for the block opcode
3187
+ emitOpcode(ctx, { op: 'BLOCK', blockId }, statements[0]);
3188
+ ctx.currentBlockStatements = [];
3189
+ ctx.currentBlockUses = new Set();
3190
+ }
3191
+ }
3192
+ function patchLabels(ctx) {
3193
+ for (const patch of ctx.pendingLabelPatches) {
3194
+ const target = ctx.labelTargets.get(patch.label);
3195
+ if (target !== undefined) {
3196
+ ;
3197
+ patch.opcode[patch.field] = target;
3198
+ }
3199
+ }
3200
+ }
3201
+ //# sourceMappingURL=analyzer.js.map