@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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/api.d.ts +144 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +380 -0
- package/dist/api.js.map +1 -0
- package/dist/compiler/analyzer.d.ts +247 -0
- package/dist/compiler/analyzer.d.ts.map +1 -0
- package/dist/compiler/analyzer.js +3201 -0
- package/dist/compiler/analyzer.js.map +1 -0
- package/dist/compiler/cli.d.ts +12 -0
- package/dist/compiler/cli.d.ts.map +1 -0
- package/dist/compiler/cli.js +209 -0
- package/dist/compiler/cli.js.map +1 -0
- package/dist/compiler/compile.d.ts +26 -0
- package/dist/compiler/compile.d.ts.map +1 -0
- package/dist/compiler/compile.js +121 -0
- package/dist/compiler/compile.js.map +1 -0
- package/dist/compiler/errors.d.ts +336 -0
- package/dist/compiler/errors.d.ts.map +1 -0
- package/dist/compiler/errors.js +466 -0
- package/dist/compiler/errors.js.map +1 -0
- package/dist/compiler/exports-prepass.d.ts +31 -0
- package/dist/compiler/exports-prepass.d.ts.map +1 -0
- package/dist/compiler/exports-prepass.js +249 -0
- package/dist/compiler/exports-prepass.js.map +1 -0
- package/dist/compiler/hmr-change-detector.d.ts +47 -0
- package/dist/compiler/hmr-change-detector.d.ts.map +1 -0
- package/dist/compiler/hmr-change-detector.js +395 -0
- package/dist/compiler/hmr-change-detector.js.map +1 -0
- package/dist/compiler/hmr-transformer.d.ts +54 -0
- package/dist/compiler/hmr-transformer.d.ts.map +1 -0
- package/dist/compiler/hmr-transformer.js +535 -0
- package/dist/compiler/hmr-transformer.js.map +1 -0
- package/dist/compiler/index.d.ts +19 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +16 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/primitive-detector.d.ts +70 -0
- package/dist/compiler/primitive-detector.d.ts.map +1 -0
- package/dist/compiler/primitive-detector.js +338 -0
- package/dist/compiler/primitive-detector.js.map +1 -0
- package/dist/compiler/ptsc.d.ts +40 -0
- package/dist/compiler/ptsc.d.ts.map +1 -0
- package/dist/compiler/ptsc.js +462 -0
- package/dist/compiler/ptsc.js.map +1 -0
- package/dist/compiler/rewriter.d.ts +96 -0
- package/dist/compiler/rewriter.d.ts.map +1 -0
- package/dist/compiler/rewriter.js +418 -0
- package/dist/compiler/rewriter.js.map +1 -0
- package/dist/compiler/step-hash.d.ts +43 -0
- package/dist/compiler/step-hash.d.ts.map +1 -0
- package/dist/compiler/step-hash.js +83 -0
- package/dist/compiler/step-hash.js.map +1 -0
- package/dist/compiler/switch-codegen.d.ts +84 -0
- package/dist/compiler/switch-codegen.d.ts.map +1 -0
- package/dist/compiler/switch-codegen.js +1540 -0
- package/dist/compiler/switch-codegen.js.map +1 -0
- package/dist/compiler/transformer.d.ts +29 -0
- package/dist/compiler/transformer.d.ts.map +1 -0
- package/dist/compiler/transformer.js +216 -0
- package/dist/compiler/transformer.js.map +1 -0
- package/dist/config/index.d.ts +122 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +215 -0
- package/dist/config/index.js.map +1 -0
- package/dist/di-errors/formatter.d.ts +126 -0
- package/dist/di-errors/formatter.d.ts.map +1 -0
- package/dist/di-errors/formatter.js +384 -0
- package/dist/di-errors/formatter.js.map +1 -0
- package/dist/di-errors/index.d.ts +5 -0
- package/dist/di-errors/index.d.ts.map +1 -0
- package/dist/di-errors/index.js +13 -0
- package/dist/di-errors/index.js.map +1 -0
- package/dist/editor.d.ts +11 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +2 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/language-service/index.d.ts +52 -0
- package/dist/language-service/index.d.ts.map +1 -0
- package/dist/language-service/index.js +366 -0
- package/dist/language-service/index.js.map +1 -0
- package/dist/language-service/process-quick-fixes.d.ts +20 -0
- package/dist/language-service/process-quick-fixes.d.ts.map +1 -0
- package/dist/language-service/process-quick-fixes.js +114 -0
- package/dist/language-service/process-quick-fixes.js.map +1 -0
- package/dist/language-service/quick-fix-discovery.d.ts +39 -0
- package/dist/language-service/quick-fix-discovery.d.ts.map +1 -0
- package/dist/language-service/quick-fix-discovery.js +124 -0
- package/dist/language-service/quick-fix-discovery.js.map +1 -0
- package/dist/loader/incremental.d.ts +50 -0
- package/dist/loader/incremental.d.ts.map +1 -0
- package/dist/loader/incremental.js +151 -0
- package/dist/loader/incremental.js.map +1 -0
- package/dist/loader/index.d.ts +25 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/index.js +24 -0
- package/dist/loader/index.js.map +1 -0
- package/dist/loader/loader.d.ts +52 -0
- package/dist/loader/loader.d.ts.map +1 -0
- package/dist/loader/loader.js +248 -0
- package/dist/loader/loader.js.map +1 -0
- package/dist/loader/register.d.ts +14 -0
- package/dist/loader/register.d.ts.map +1 -0
- package/dist/loader/register.js +20 -0
- package/dist/loader/register.js.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +13 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/index.public.d.ts +13 -0
- package/dist/plugins/index.public.d.ts.map +1 -0
- package/dist/plugins/index.public.js +13 -0
- package/dist/plugins/index.public.js.map +1 -0
- package/dist/plugins/types.d.ts +83 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +24 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/server/index.d.ts +33 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +42 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/tsserver.d.ts +28 -0
- package/dist/server/tsserver.d.ts.map +1 -0
- package/dist/server/tsserver.js +126 -0
- package/dist/server/tsserver.js.map +1 -0
- package/lib/lib.d.ts +20 -0
- package/lib/lib.decorators.d.ts +382 -0
- package/lib/lib.decorators.legacy.d.ts +20 -0
- package/lib/lib.dom.asynciterable.d.ts +18 -0
- package/lib/lib.dom.d.ts +45125 -0
- package/lib/lib.dom.iterable.d.ts +18 -0
- package/lib/lib.es2015.collection.d.ts +150 -0
- package/lib/lib.es2015.core.d.ts +595 -0
- package/lib/lib.es2015.d.ts +26 -0
- package/lib/lib.es2015.generator.d.ts +75 -0
- package/lib/lib.es2015.iterable.d.ts +603 -0
- package/lib/lib.es2015.promise.d.ts +79 -0
- package/lib/lib.es2015.proxy.d.ts +126 -0
- package/lib/lib.es2015.reflect.d.ts +142 -0
- package/lib/lib.es2015.symbol.d.ts +44 -0
- package/lib/lib.es2015.symbol.wellknown.d.ts +324 -0
- package/lib/lib.es2016.array.include.d.ts +114 -0
- package/lib/lib.es2016.d.ts +19 -0
- package/lib/lib.es2016.full.d.ts +21 -0
- package/lib/lib.es2016.intl.d.ts +29 -0
- package/lib/lib.es2017.arraybuffer.d.ts +19 -0
- package/lib/lib.es2017.d.ts +24 -0
- package/lib/lib.es2017.date.d.ts +29 -0
- package/lib/lib.es2017.full.d.ts +21 -0
- package/lib/lib.es2017.intl.d.ts +42 -0
- package/lib/lib.es2017.object.d.ts +47 -0
- package/lib/lib.es2017.sharedmemory.d.ts +133 -0
- package/lib/lib.es2017.string.d.ts +43 -0
- package/lib/lib.es2017.typedarrays.d.ts +51 -0
- package/lib/lib.es2018.asyncgenerator.d.ts +75 -0
- package/lib/lib.es2018.asynciterable.d.ts +51 -0
- package/lib/lib.es2018.d.ts +22 -0
- package/lib/lib.es2018.full.d.ts +22 -0
- package/lib/lib.es2018.intl.d.ts +81 -0
- package/lib/lib.es2018.promise.d.ts +28 -0
- package/lib/lib.es2018.regexp.d.ts +35 -0
- package/lib/lib.es2019.array.d.ts +77 -0
- package/lib/lib.es2019.d.ts +22 -0
- package/lib/lib.es2019.full.d.ts +22 -0
- package/lib/lib.es2019.intl.d.ts +21 -0
- package/lib/lib.es2019.object.d.ts +31 -0
- package/lib/lib.es2019.string.d.ts +35 -0
- package/lib/lib.es2019.symbol.d.ts +22 -0
- package/lib/lib.es2020.bigint.d.ts +763 -0
- package/lib/lib.es2020.d.ts +25 -0
- package/lib/lib.es2020.date.d.ts +40 -0
- package/lib/lib.es2020.full.d.ts +22 -0
- package/lib/lib.es2020.intl.d.ts +472 -0
- package/lib/lib.es2020.number.d.ts +26 -0
- package/lib/lib.es2020.promise.d.ts +45 -0
- package/lib/lib.es2020.sharedmemory.d.ts +97 -0
- package/lib/lib.es2020.string.d.ts +42 -0
- package/lib/lib.es2020.symbol.wellknown.d.ts +39 -0
- package/lib/lib.es2021.d.ts +21 -0
- package/lib/lib.es2021.full.d.ts +22 -0
- package/lib/lib.es2021.intl.d.ts +164 -0
- package/lib/lib.es2021.promise.d.ts +46 -0
- package/lib/lib.es2021.string.d.ts +31 -0
- package/lib/lib.es2021.weakref.d.ts +76 -0
- package/lib/lib.es2022.array.d.ts +119 -0
- package/lib/lib.es2022.d.ts +23 -0
- package/lib/lib.es2022.error.d.ts +73 -0
- package/lib/lib.es2022.full.d.ts +22 -0
- package/lib/lib.es2022.intl.d.ts +143 -0
- package/lib/lib.es2022.object.d.ts +24 -0
- package/lib/lib.es2022.regexp.d.ts +37 -0
- package/lib/lib.es2022.string.d.ts +23 -0
- package/lib/lib.es2023.array.d.ts +922 -0
- package/lib/lib.es2023.collection.d.ts +19 -0
- package/lib/lib.es2023.d.ts +20 -0
- package/lib/lib.es2023.full.d.ts +22 -0
- package/lib/lib.es2023.intl.d.ts +62 -0
- package/lib/lib.es2024.arraybuffer.d.ts +63 -0
- package/lib/lib.es2024.collection.d.ts +27 -0
- package/lib/lib.es2024.d.ts +24 -0
- package/lib/lib.es2024.full.d.ts +22 -0
- package/lib/lib.es2024.object.d.ts +27 -0
- package/lib/lib.es2024.promise.d.ts +33 -0
- package/lib/lib.es2024.regexp.d.ts +23 -0
- package/lib/lib.es2024.sharedmemory.d.ts +66 -0
- package/lib/lib.es2024.string.d.ts +27 -0
- package/lib/lib.es2025.collection.d.ts +94 -0
- package/lib/lib.es2025.d.ts +23 -0
- package/lib/lib.es2025.float16.d.ts +443 -0
- package/lib/lib.es2025.full.d.ts +22 -0
- package/lib/lib.es2025.intl.d.ts +200 -0
- package/lib/lib.es2025.iterator.d.ts +146 -0
- package/lib/lib.es2025.promise.d.ts +32 -0
- package/lib/lib.es2025.regexp.d.ts +30 -0
- package/lib/lib.es5.d.ts +4599 -0
- package/lib/lib.es6.d.ts +21 -0
- package/lib/lib.esnext.array.d.ts +33 -0
- package/lib/lib.esnext.collection.d.ts +47 -0
- package/lib/lib.esnext.d.ts +27 -0
- package/lib/lib.esnext.date.d.ts +21 -0
- package/lib/lib.esnext.decorators.d.ts +26 -0
- package/lib/lib.esnext.disposable.d.ts +191 -0
- package/lib/lib.esnext.error.d.ts +22 -0
- package/lib/lib.esnext.full.d.ts +22 -0
- package/lib/lib.esnext.intl.d.ts +107 -0
- package/lib/lib.esnext.sharedmemory.d.ts +23 -0
- package/lib/lib.esnext.temporal.d.ts +485 -0
- package/lib/lib.esnext.typedarrays.d.ts +90 -0
- package/lib/lib.scripthost.d.ts +320 -0
- package/lib/lib.webworker.asynciterable.d.ts +18 -0
- package/lib/lib.webworker.d.ts +15606 -0
- package/lib/lib.webworker.importscripts.d.ts +21 -0
- package/lib/lib.webworker.iterable.d.ts +18 -0
- package/lib/logger.js +144 -0
- package/lib/package.json +7 -0
- package/lib/tsserver.js +57 -0
- package/lib/tsserverlibrary.js +171 -0
- package/lib/typesMap.json +497 -0
- package/lib/typescript.js +373 -0
- 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
|