driftjs-compiler 0.0.0
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/dist/index-es.js +4804 -0
- package/package.json +28 -0
- package/src/generator.ts +1029 -0
- package/src/index.ts +36 -0
- package/src/lexer.ts +954 -0
- package/src/parser.ts +533 -0
- package/src/transformer.ts +288 -0
- package/tests/generator.test.ts +149 -0
- package/tests/if_conditionals.test.ts +371 -0
- package/tests/lexer.test.ts +221 -0
- package/tests/parser.test.ts +302 -0
- package/tests/transformer.test.ts +110 -0
- package/tsconfig.json +8 -0
- package/types/ast.ts +88 -0
- package/types/error.ts +29 -0
- package/types/index.ts +6 -0
- package/types/lexer-state.ts +118 -0
- package/types/opcodes.ts +66 -0
- package/types/token.ts +61 -0
- package/vite.config.ts +20 -0
package/src/generator.ts
ADDED
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProgramNode,
|
|
3
|
+
TemplateChildNode,
|
|
4
|
+
ElementNode,
|
|
5
|
+
TextNode,
|
|
6
|
+
InterpolationNode,
|
|
7
|
+
CommentNode,
|
|
8
|
+
AttributeNode,
|
|
9
|
+
IfNode,
|
|
10
|
+
ForNode,
|
|
11
|
+
SwitchNode,
|
|
12
|
+
CompiledModule,
|
|
13
|
+
ReactiveBinding,
|
|
14
|
+
ImportSpec,
|
|
15
|
+
} from '../types/index.js';
|
|
16
|
+
import {
|
|
17
|
+
ASTNodeType,
|
|
18
|
+
Opcode,
|
|
19
|
+
} from '../types/index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generator for Drift template compiler.
|
|
23
|
+
* Converts enriched AST (ProgramNode) into register-based Virtual Machine bytecode
|
|
24
|
+
* and a constant pool module.
|
|
25
|
+
*/
|
|
26
|
+
export class DriftGenerator {
|
|
27
|
+
private readonly ast: ProgramNode;
|
|
28
|
+
private bytecode: number[] = [];
|
|
29
|
+
private constants: any[] = [];
|
|
30
|
+
private nextRegisterId = 0;
|
|
31
|
+
private declaredVars: Set<string> = new Set();
|
|
32
|
+
private imports: ImportSpec[] = [];
|
|
33
|
+
private bindingPositions: Map<string, { pc: number; opcode: Opcode }[]> = new Map();
|
|
34
|
+
|
|
35
|
+
constructor(ast: ProgramNode) {
|
|
36
|
+
this.ast = ast;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compiles the AST into a bytecode stream and constant pool module.
|
|
41
|
+
* @returns CompiledModule containing numeric bytecode and constant pool.
|
|
42
|
+
*/
|
|
43
|
+
public generate(): CompiledModule {
|
|
44
|
+
this.bytecode = [];
|
|
45
|
+
this.constants = [];
|
|
46
|
+
this.nextRegisterId = 0;
|
|
47
|
+
this.declaredVars = new Set();
|
|
48
|
+
this.imports = [];
|
|
49
|
+
this.bindingPositions = new Map();
|
|
50
|
+
|
|
51
|
+
this.collectDeclaredVars(this.ast.body);
|
|
52
|
+
|
|
53
|
+
if (this.ast.body.length === 0) {
|
|
54
|
+
const rootReg = this.allocRegister();
|
|
55
|
+
this.emit(Opcode.CREATE_FRAGMENT, rootReg);
|
|
56
|
+
this.emit(Opcode.RETURN, rootReg);
|
|
57
|
+
return {
|
|
58
|
+
bytecode: this.bytecode,
|
|
59
|
+
constants: this.constants,
|
|
60
|
+
reactiveBindings: this.buildReactiveBindings(),
|
|
61
|
+
declaredVars: [...this.declaredVars],
|
|
62
|
+
imports: this.imports,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.ast.body.length === 1 && this.ast.body[0]?.type === ASTNodeType.Element) {
|
|
67
|
+
const rootReg = this.allocRegister();
|
|
68
|
+
this.compileElement(this.ast.body[0] as ElementNode, rootReg);
|
|
69
|
+
this.emit(Opcode.RETURN, rootReg);
|
|
70
|
+
} else {
|
|
71
|
+
const rootReg = this.allocRegister();
|
|
72
|
+
this.emit(Opcode.CREATE_FRAGMENT, rootReg);
|
|
73
|
+
for (const child of this.ast.body) {
|
|
74
|
+
this.compileNode(child, rootReg);
|
|
75
|
+
}
|
|
76
|
+
this.emit(Opcode.RETURN, rootReg);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
bytecode: this.bytecode,
|
|
81
|
+
constants: this.constants,
|
|
82
|
+
reactiveBindings: this.buildReactiveBindings(),
|
|
83
|
+
declaredVars: [...this.declaredVars],
|
|
84
|
+
imports: this.imports,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compiles a single TemplateChildNode into register bytecode operations.
|
|
90
|
+
*/
|
|
91
|
+
private compileNode(node: TemplateChildNode, parentReg: number): void {
|
|
92
|
+
switch (node.type) {
|
|
93
|
+
case ASTNodeType.Element:
|
|
94
|
+
// Script elements are not rendered into the DOM — they initialise scope.
|
|
95
|
+
if ((node as ElementNode).tagName === 'script') {
|
|
96
|
+
this.compileScriptElement(node as ElementNode);
|
|
97
|
+
} else {
|
|
98
|
+
this.compileElementNode(node, parentReg);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case ASTNodeType.Text:
|
|
102
|
+
this.compileTextNode(node, parentReg);
|
|
103
|
+
break;
|
|
104
|
+
case ASTNodeType.Interpolation:
|
|
105
|
+
this.compileInterpolationNode(node, parentReg);
|
|
106
|
+
break;
|
|
107
|
+
case ASTNodeType.Comment:
|
|
108
|
+
this.compileCommentNode(node, parentReg);
|
|
109
|
+
break;
|
|
110
|
+
case ASTNodeType.If:
|
|
111
|
+
this.compileIfNode(node, parentReg);
|
|
112
|
+
break;
|
|
113
|
+
case ASTNodeType.For:
|
|
114
|
+
this.compileForNode(node, parentReg);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private filterRuntimeScriptAst(content: any): any {
|
|
120
|
+
if (Array.isArray(content)) {
|
|
121
|
+
const filtered = content.filter((stmt: any) => stmt && stmt.type !== 'ImportDeclaration');
|
|
122
|
+
return filtered.length === 1 ? filtered[0] : filtered;
|
|
123
|
+
}
|
|
124
|
+
if (content && typeof content === 'object' && content.type === 'ImportDeclaration') {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return content;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Emits an EXEC_SCRIPT instruction for a <script> element.
|
|
132
|
+
* The script body AST is stored in the constant pool and executed by the runtime
|
|
133
|
+
* before any DOM construction, populating the component scope with declared
|
|
134
|
+
* variables and functions.
|
|
135
|
+
*/
|
|
136
|
+
private compileScriptElement(node: ElementNode): void {
|
|
137
|
+
for (const child of node.children) {
|
|
138
|
+
if (child.type === ASTNodeType.Text && typeof child.content === 'object' && child.content !== null) {
|
|
139
|
+
const filtered = this.filterRuntimeScriptAst(child.content);
|
|
140
|
+
if (filtered !== null && (!Array.isArray(filtered) || filtered.length > 0)) {
|
|
141
|
+
const scriptBodyIdx = this.addConstant(filtered);
|
|
142
|
+
this.emit(Opcode.EXEC_SCRIPT, scriptBodyIdx);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private isComponentTag(tagName: string): boolean {
|
|
149
|
+
if (this.imports.some((imp) => imp.localName === tagName)) return true;
|
|
150
|
+
const firstChar = tagName.charAt(0);
|
|
151
|
+
return firstChar !== '' && firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private compileElement(node: ElementNode, targetReg: number): void {
|
|
155
|
+
const isComp = this.isComponentTag(node.tagName);
|
|
156
|
+
const tagConstIdx = this.addConstant(node.tagName);
|
|
157
|
+
|
|
158
|
+
if (isComp) {
|
|
159
|
+
const propsSpec: Record<string, any> = { __drift_props__: true };
|
|
160
|
+
for (const attr of node.attributes) {
|
|
161
|
+
if (attr.type === ASTNodeType.Attribute) {
|
|
162
|
+
if (attr.value === null) {
|
|
163
|
+
propsSpec[attr.name] = true;
|
|
164
|
+
} else if (typeof attr.value === 'string') {
|
|
165
|
+
propsSpec[attr.name] = attr.value;
|
|
166
|
+
} else if (attr.value.type === ASTNodeType.Interpolation) {
|
|
167
|
+
propsSpec[attr.name] = attr.value.expression;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const propsSpecIdx = this.addConstant(propsSpec);
|
|
172
|
+
const pc = this.bytecode.length;
|
|
173
|
+
this.emit(Opcode.CREATE_ELEMENT, targetReg, tagConstIdx, propsSpecIdx);
|
|
174
|
+
|
|
175
|
+
for (const attr of node.attributes) {
|
|
176
|
+
if (attr.type === ASTNodeType.Attribute && attr.value !== null && typeof attr.value !== 'string' && attr.value.type === ASTNodeType.Interpolation) {
|
|
177
|
+
this.recordBindingPositions(attr.value.expression, pc, Opcode.CREATE_ELEMENT);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
this.emit(Opcode.CREATE_ELEMENT, targetReg, tagConstIdx);
|
|
182
|
+
|
|
183
|
+
for (const attr of node.attributes) {
|
|
184
|
+
this.compileAttributeNode(attr, targetReg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const child of node.children) {
|
|
188
|
+
this.compileNode(child, targetReg);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private compileElementNode(node: ElementNode, parentReg: number): void {
|
|
194
|
+
const elemReg = this.allocRegister();
|
|
195
|
+
this.compileElement(node, elemReg);
|
|
196
|
+
this.emit(Opcode.APPEND_CHILD, parentReg, elemReg);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private compileAttributeNode(attr: AttributeNode, elemReg: number): void {
|
|
200
|
+
const nameIdx = this.addConstant(attr.name);
|
|
201
|
+
|
|
202
|
+
if (attr.value === null) {
|
|
203
|
+
const valIdx = this.addConstant(true);
|
|
204
|
+
this.emit(Opcode.SET_ATTR, elemReg, nameIdx, valIdx, 0);
|
|
205
|
+
} else if (typeof attr.value === 'string') {
|
|
206
|
+
const valIdx = this.addConstant(attr.value);
|
|
207
|
+
this.emit(Opcode.SET_ATTR, elemReg, nameIdx, valIdx, 0);
|
|
208
|
+
} else if (attr.value.type === ASTNodeType.Interpolation) {
|
|
209
|
+
const exprIdx = this.addConstant(attr.value.expression);
|
|
210
|
+
const pc = this.bytecode.length;
|
|
211
|
+
this.emit(Opcode.SET_ATTR, elemReg, nameIdx, exprIdx, 1);
|
|
212
|
+
this.recordBindingPositions(attr.value.expression, pc, Opcode.SET_ATTR);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private compileTextNode(node: TextNode, parentReg: number): void {
|
|
217
|
+
const textReg = this.allocRegister();
|
|
218
|
+
const textConstIdx = this.addConstant(node.content);
|
|
219
|
+
this.emit(Opcode.CREATE_TEXT, textReg, textConstIdx);
|
|
220
|
+
this.emit(Opcode.APPEND_CHILD, parentReg, textReg);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private compileInterpolationNode(node: InterpolationNode, parentReg: number): void {
|
|
224
|
+
const textReg = this.allocRegister();
|
|
225
|
+
const exprConstIdx = this.addConstant(node.expression);
|
|
226
|
+
const pc = this.bytecode.length;
|
|
227
|
+
this.emit(Opcode.INTERPOLATE_TEXT, textReg, exprConstIdx);
|
|
228
|
+
this.emit(Opcode.APPEND_CHILD, parentReg, textReg);
|
|
229
|
+
this.recordBindingPositions(node.expression, pc, Opcode.INTERPOLATE_TEXT);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private compileCommentNode(node: CommentNode, parentReg: number): void {
|
|
233
|
+
const commentReg = this.allocRegister();
|
|
234
|
+
const commentConstIdx = this.addConstant(node.content);
|
|
235
|
+
this.emit(Opcode.CREATE_COMMENT, commentReg, commentConstIdx);
|
|
236
|
+
this.emit(Opcode.APPEND_CHILD, parentReg, commentReg);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Compiles a list of child nodes into an isolated sub-module.
|
|
241
|
+
* The sub-module shares `declaredVars` with the parent (so reactive var detection works)
|
|
242
|
+
* but has its own fresh bytecode, constants, and registers.
|
|
243
|
+
*/
|
|
244
|
+
private compileNodesToSubModule(
|
|
245
|
+
nodes: readonly TemplateChildNode[]
|
|
246
|
+
): { bytecode: number[]; constants: any[]; reactiveBindings: ReactiveBinding[] } {
|
|
247
|
+
// save parent state
|
|
248
|
+
const savedBytecode = this.bytecode;
|
|
249
|
+
const savedConstants = this.constants;
|
|
250
|
+
const savedNextReg = this.nextRegisterId;
|
|
251
|
+
const savedBindPos = this.bindingPositions;
|
|
252
|
+
|
|
253
|
+
// fresh slate for the sub-module
|
|
254
|
+
this.bytecode = [];
|
|
255
|
+
this.constants = [];
|
|
256
|
+
this.nextRegisterId = 0;
|
|
257
|
+
this.bindingPositions = new Map();
|
|
258
|
+
|
|
259
|
+
const rootReg = this.allocRegister();
|
|
260
|
+
this.emit(Opcode.CREATE_FRAGMENT, rootReg);
|
|
261
|
+
for (const node of nodes) {
|
|
262
|
+
this.compileNode(node, rootReg);
|
|
263
|
+
}
|
|
264
|
+
this.emit(Opcode.RETURN, rootReg);
|
|
265
|
+
|
|
266
|
+
const result = {
|
|
267
|
+
bytecode: this.bytecode,
|
|
268
|
+
constants: this.constants,
|
|
269
|
+
reactiveBindings: this.buildReactiveBindings(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// restore parent state
|
|
273
|
+
this.bytecode = savedBytecode;
|
|
274
|
+
this.constants = savedConstants;
|
|
275
|
+
this.nextRegisterId = savedNextReg;
|
|
276
|
+
this.bindingPositions = savedBindPos;
|
|
277
|
+
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Collects all declared-var names referenced inside a sub-module's reactive bindings
|
|
283
|
+
* plus any identifiers from an optional extra AST expression node (e.g. the @if condition
|
|
284
|
+
* or @for iterable expression).
|
|
285
|
+
*/
|
|
286
|
+
private collectDepsFromSubModule(
|
|
287
|
+
subMod: { reactiveBindings: ReactiveBinding[] },
|
|
288
|
+
extraExpr?: any
|
|
289
|
+
): string[] {
|
|
290
|
+
const deps = new Set<string>(subMod.reactiveBindings.map((b) => b.variable));
|
|
291
|
+
if (extraExpr) {
|
|
292
|
+
for (const name of this.extractIdentifiers(extraExpr)) {
|
|
293
|
+
if (this.declaredVars.has(name)) deps.add(name);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return [...deps];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private compileIfNode(node: IfNode, parentReg: number): void {
|
|
300
|
+
// Build consequent sub-module
|
|
301
|
+
const consMod = this.compileNodesToSubModule(node.consequent);
|
|
302
|
+
const consIdx = this.addConstant(consMod);
|
|
303
|
+
|
|
304
|
+
// Build alternate sub-module (may be @else or @else if)
|
|
305
|
+
let altIdx = 0xFF;
|
|
306
|
+
let altMod: any = null;
|
|
307
|
+
if (node.alternate !== null) {
|
|
308
|
+
let altNodes: readonly TemplateChildNode[];
|
|
309
|
+
if (Array.isArray(node.alternate)) {
|
|
310
|
+
altNodes = node.alternate;
|
|
311
|
+
} else {
|
|
312
|
+
// @else if: wrap the nested IfNode so it compiles correctly inside a sub-module
|
|
313
|
+
altNodes = [node.alternate as TemplateChildNode];
|
|
314
|
+
}
|
|
315
|
+
altMod = this.compileNodesToSubModule(altNodes);
|
|
316
|
+
altIdx = this.addConstant(altMod);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Deps = union of both branches' reactive vars + condition identifiers
|
|
320
|
+
const depsSet = new Set<string>(this.collectDepsFromSubModule(consMod, node.test));
|
|
321
|
+
if (altMod) {
|
|
322
|
+
for (const dep of this.collectDepsFromSubModule(altMod)) {
|
|
323
|
+
depsSet.add(dep);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const depsIdx = this.addConstant(Array.from(depsSet));
|
|
327
|
+
|
|
328
|
+
const condIdx = this.addConstant(node.test);
|
|
329
|
+
|
|
330
|
+
// REACTIVE_IF parentReg condIdx consIdx altIdx depsIdx (5 operand bytes)
|
|
331
|
+
this.emit(Opcode.REACTIVE_IF, parentReg, condIdx, consIdx, altIdx, depsIdx);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private compileForNode(node: ForNode, parentReg: number): void {
|
|
335
|
+
// Build body sub-module
|
|
336
|
+
const bodyMod = this.compileNodesToSubModule(node.body);
|
|
337
|
+
const bodyIdx = this.addConstant(bodyMod);
|
|
338
|
+
|
|
339
|
+
const iterIdx = this.addConstant(node.iterable);
|
|
340
|
+
const itemNameIdx = this.addConstant(node.item);
|
|
341
|
+
const indexNameIdx = node.index !== null ? this.addConstant(node.index) : 0xFF;
|
|
342
|
+
const keyIdx = node.key ? this.addConstant(node.key) : 0xFF;
|
|
343
|
+
|
|
344
|
+
// Deps = body's reactive vars + identifiers from iterable expression
|
|
345
|
+
const deps = this.collectDepsFromSubModule(bodyMod, node.iterable);
|
|
346
|
+
const depsIdx = this.addConstant(deps);
|
|
347
|
+
|
|
348
|
+
// REACTIVE_FOR parentReg iterIdx itemNameIdx indexNameIdx keyIdx bodyIdx depsIdx
|
|
349
|
+
this.emit(Opcode.REACTIVE_FOR, parentReg, iterIdx, itemNameIdx, indexNameIdx, keyIdx, bodyIdx, depsIdx);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private collectDeclaredVars(nodes: readonly TemplateChildNode[]): void {
|
|
353
|
+
for (const node of nodes) {
|
|
354
|
+
if (node.type === ASTNodeType.Element && node.tagName === 'script') {
|
|
355
|
+
for (const child of node.children) {
|
|
356
|
+
if (child.type === ASTNodeType.Text && typeof child.content === 'object' && child.content !== null) {
|
|
357
|
+
const astNode = child.content as any;
|
|
358
|
+
if (Array.isArray(astNode)) {
|
|
359
|
+
for (const stmt of astNode) {
|
|
360
|
+
this.extractVarNames(stmt);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
this.extractVarNames(astNode);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private extractVarNames(node: any): void {
|
|
372
|
+
if (!node || typeof node !== 'object') return;
|
|
373
|
+
if (node.type === 'VariableDeclaration') {
|
|
374
|
+
for (const decl of node.declarations) {
|
|
375
|
+
this.extractBindingIdentifiers(decl.id);
|
|
376
|
+
}
|
|
377
|
+
} else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
378
|
+
this.declaredVars.add(node.id.name);
|
|
379
|
+
} else if (node.type === 'ImportDeclaration' && Array.isArray(node.specifiers)) {
|
|
380
|
+
const source = typeof node.source?.value === 'string' ? node.source.value : '';
|
|
381
|
+
for (const spec of node.specifiers) {
|
|
382
|
+
if (spec.local?.type === 'Identifier') {
|
|
383
|
+
const localName = spec.local.name;
|
|
384
|
+
this.declaredVars.add(localName);
|
|
385
|
+
const isDefault = spec.type === 'ImportDefaultSpecifier';
|
|
386
|
+
const importedName = spec.type === 'ImportSpecifier' && spec.imported?.type === 'Identifier'
|
|
387
|
+
? spec.imported.name
|
|
388
|
+
: undefined;
|
|
389
|
+
this.imports.push({ localName, source, isDefault, importedName });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private extractBindingIdentifiers(idNode: any): void {
|
|
396
|
+
if (!idNode || typeof idNode !== 'object') return;
|
|
397
|
+
if (idNode.type === 'Identifier') {
|
|
398
|
+
this.declaredVars.add(idNode.name);
|
|
399
|
+
} else if (idNode.type === 'ObjectPattern' && Array.isArray(idNode.properties)) {
|
|
400
|
+
for (const prop of idNode.properties) {
|
|
401
|
+
if (prop.type === 'Property') {
|
|
402
|
+
this.extractBindingIdentifiers(prop.value);
|
|
403
|
+
} else if (prop.type === 'RestElement') {
|
|
404
|
+
this.extractBindingIdentifiers(prop.argument);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} else if (idNode.type === 'ArrayPattern' && Array.isArray(idNode.elements)) {
|
|
408
|
+
for (const elem of idNode.elements) {
|
|
409
|
+
if (elem) this.extractBindingIdentifiers(elem);
|
|
410
|
+
}
|
|
411
|
+
} else if (idNode.type === 'AssignmentPattern') {
|
|
412
|
+
this.extractBindingIdentifiers(idNode.left);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private extractIdentifiers(node: any): Set<string> {
|
|
417
|
+
const ids = new Set<string>();
|
|
418
|
+
if (!node || typeof node !== 'object' || !node.type) return ids;
|
|
419
|
+
|
|
420
|
+
switch (node.type) {
|
|
421
|
+
case 'Identifier':
|
|
422
|
+
ids.add(node.name);
|
|
423
|
+
break;
|
|
424
|
+
case 'BinaryExpression':
|
|
425
|
+
case 'LogicalExpression':
|
|
426
|
+
for (const id of this.extractIdentifiers(node.left)) ids.add(id);
|
|
427
|
+
for (const id of this.extractIdentifiers(node.right)) ids.add(id);
|
|
428
|
+
break;
|
|
429
|
+
case 'UnaryExpression':
|
|
430
|
+
case 'UpdateExpression':
|
|
431
|
+
for (const id of this.extractIdentifiers(node.argument)) ids.add(id);
|
|
432
|
+
break;
|
|
433
|
+
case 'MemberExpression':
|
|
434
|
+
for (const id of this.extractIdentifiers(node.object)) ids.add(id);
|
|
435
|
+
break;
|
|
436
|
+
case 'CallExpression':
|
|
437
|
+
for (const id of this.extractIdentifiers(node.callee)) ids.add(id);
|
|
438
|
+
for (const arg of node.arguments) {
|
|
439
|
+
for (const id of this.extractIdentifiers(arg)) ids.add(id);
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
case 'ConditionalExpression':
|
|
443
|
+
for (const id of this.extractIdentifiers(node.test)) ids.add(id);
|
|
444
|
+
for (const id of this.extractIdentifiers(node.consequent)) ids.add(id);
|
|
445
|
+
for (const id of this.extractIdentifiers(node.alternate)) ids.add(id);
|
|
446
|
+
break;
|
|
447
|
+
case 'AssignmentExpression':
|
|
448
|
+
for (const id of this.extractIdentifiers(node.left)) ids.add(id);
|
|
449
|
+
for (const id of this.extractIdentifiers(node.right)) ids.add(id);
|
|
450
|
+
break;
|
|
451
|
+
case 'ArrowFunctionExpression':
|
|
452
|
+
case 'FunctionExpression':
|
|
453
|
+
for (const id of this.extractIdentifiers(node.body)) ids.add(id);
|
|
454
|
+
break;
|
|
455
|
+
case 'BlockStatement':
|
|
456
|
+
for (const stmt of node.body) {
|
|
457
|
+
for (const id of this.extractIdentifiers(stmt)) ids.add(id);
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
case 'ExpressionStatement':
|
|
461
|
+
for (const id of this.extractIdentifiers(node.expression)) ids.add(id);
|
|
462
|
+
break;
|
|
463
|
+
case 'ReturnStatement':
|
|
464
|
+
if (node.argument) {
|
|
465
|
+
for (const id of this.extractIdentifiers(node.argument)) ids.add(id);
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
case 'NewExpression':
|
|
469
|
+
for (const id of this.extractIdentifiers(node.callee)) ids.add(id);
|
|
470
|
+
if (node.arguments) {
|
|
471
|
+
for (const arg of node.arguments) {
|
|
472
|
+
for (const id of this.extractIdentifiers(arg)) ids.add(id);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
case 'ForStatement':
|
|
477
|
+
if (node.init) for (const id of this.extractIdentifiers(node.init)) ids.add(id);
|
|
478
|
+
if (node.test) for (const id of this.extractIdentifiers(node.test)) ids.add(id);
|
|
479
|
+
if (node.update) for (const id of this.extractIdentifiers(node.update)) ids.add(id);
|
|
480
|
+
if (node.body) for (const id of this.extractIdentifiers(node.body)) ids.add(id);
|
|
481
|
+
break;
|
|
482
|
+
case 'ForOfStatement':
|
|
483
|
+
case 'ForInStatement':
|
|
484
|
+
if (node.left) for (const id of this.extractIdentifiers(node.left)) ids.add(id);
|
|
485
|
+
if (node.right) for (const id of this.extractIdentifiers(node.right)) ids.add(id);
|
|
486
|
+
if (node.body) for (const id of this.extractIdentifiers(node.body)) ids.add(id);
|
|
487
|
+
break;
|
|
488
|
+
case 'WhileStatement':
|
|
489
|
+
case 'DoWhileStatement':
|
|
490
|
+
if (node.test) for (const id of this.extractIdentifiers(node.test)) ids.add(id);
|
|
491
|
+
if (node.body) for (const id of this.extractIdentifiers(node.body)) ids.add(id);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
return ids;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private recordBindingPositions(expr: any, pc: number, opcode: Opcode): void {
|
|
498
|
+
if (this.declaredVars.size === 0) return;
|
|
499
|
+
const ids = this.extractIdentifiers(expr);
|
|
500
|
+
for (const name of ids) {
|
|
501
|
+
if (this.declaredVars.has(name)) {
|
|
502
|
+
if (!this.bindingPositions.has(name)) {
|
|
503
|
+
this.bindingPositions.set(name, []);
|
|
504
|
+
}
|
|
505
|
+
this.bindingPositions.get(name)!.push({ pc, opcode });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private buildReactiveBindings(): ReactiveBinding[] {
|
|
511
|
+
const bindings: ReactiveBinding[] = [];
|
|
512
|
+
for (const [variable, positions] of this.bindingPositions) {
|
|
513
|
+
bindings.push({ variable, positions });
|
|
514
|
+
}
|
|
515
|
+
return bindings;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private allocRegister(): number {
|
|
519
|
+
return this.nextRegisterId++;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private addConstant(value: any): number {
|
|
523
|
+
if (value && typeof value === 'object' && value.type && typeof value.type === 'string') {
|
|
524
|
+
const codeStr = astToJS(value);
|
|
525
|
+
value = { __drift_fn__: `(scope, declaredVars, setScopeValue, inScopeChain, resolveIterable) => (${codeStr})` };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const existingIndex = this.constants.findIndex((c) => this.isConstantEqual(c, value));
|
|
529
|
+
if (existingIndex !== -1) {
|
|
530
|
+
return existingIndex;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.constants.push(value);
|
|
534
|
+
return this.constants.length - 1;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private isConstantEqual(a: any, b: any): boolean {
|
|
538
|
+
if (a === b) return true;
|
|
539
|
+
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
540
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private emit(opcode: Opcode, ...operands: number[]): void {
|
|
546
|
+
this.bytecode.push(opcode, ...operands);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private emitJumpIfFalsePlaceholder(condReg: number): number {
|
|
550
|
+
const pos = this.bytecode.length;
|
|
551
|
+
this.bytecode.push(Opcode.JUMP_IF_FALSE, condReg, 0, 0);
|
|
552
|
+
return pos;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private emitJumpPlaceholder(): number {
|
|
556
|
+
const pos = this.bytecode.length;
|
|
557
|
+
this.bytecode.push(Opcode.JUMP, 0, 0);
|
|
558
|
+
return pos;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private emitJump(targetByte: number): void {
|
|
562
|
+
const high = (targetByte >> 8) & 0xff;
|
|
563
|
+
const low = targetByte & 0xff;
|
|
564
|
+
this.bytecode.push(Opcode.JUMP, high, low);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private emitLoopIterPlaceholder(
|
|
568
|
+
arrayReg: number,
|
|
569
|
+
itemReg: number,
|
|
570
|
+
indexReg: number,
|
|
571
|
+
itemConstIdx: number,
|
|
572
|
+
indexConstIdx: number
|
|
573
|
+
): number {
|
|
574
|
+
const pos = this.bytecode.length;
|
|
575
|
+
const itemHigh = (itemConstIdx >> 8) & 0xff;
|
|
576
|
+
const itemLow = itemConstIdx & 0xff;
|
|
577
|
+
const idxHigh = (indexConstIdx >> 8) & 0xff;
|
|
578
|
+
const idxLow = indexConstIdx & 0xff;
|
|
579
|
+
|
|
580
|
+
this.bytecode.push(
|
|
581
|
+
Opcode.LOOP_ITER,
|
|
582
|
+
arrayReg,
|
|
583
|
+
itemReg,
|
|
584
|
+
indexReg,
|
|
585
|
+
itemHigh,
|
|
586
|
+
itemLow,
|
|
587
|
+
idxHigh,
|
|
588
|
+
idxLow,
|
|
589
|
+
0,
|
|
590
|
+
0
|
|
591
|
+
);
|
|
592
|
+
return pos;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private patchJump(pos: number, targetByte: number): void {
|
|
596
|
+
const high = (targetByte >> 8) & 0xff;
|
|
597
|
+
const low = targetByte & 0xff;
|
|
598
|
+
|
|
599
|
+
const op = this.bytecode[pos];
|
|
600
|
+
if (op === Opcode.JUMP_IF_FALSE) {
|
|
601
|
+
this.bytecode[pos + 2] = high;
|
|
602
|
+
this.bytecode[pos + 3] = low;
|
|
603
|
+
} else if (op === Opcode.JUMP) {
|
|
604
|
+
this.bytecode[pos + 1] = high;
|
|
605
|
+
this.bytecode[pos + 2] = low;
|
|
606
|
+
} else if (op === Opcode.LOOP_ITER) {
|
|
607
|
+
this.bytecode[pos + 8] = high;
|
|
608
|
+
this.bytecode[pos + 9] = low;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getRootIdentifier(node: any): string | null {
|
|
614
|
+
if (!node || typeof node !== 'object') return null;
|
|
615
|
+
if (node.type === 'Identifier') return node.name;
|
|
616
|
+
if (node.type === 'MemberExpression') return getRootIdentifier(node.object);
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function extractBindingNames(node: any): string[] {
|
|
621
|
+
const names: string[] = [];
|
|
622
|
+
function walk(n: any) {
|
|
623
|
+
if (!n || typeof n !== 'object') return;
|
|
624
|
+
if (n.type === 'Identifier') names.push(n.name);
|
|
625
|
+
else if (n.type === 'ObjectPattern' && Array.isArray(n.properties)) {
|
|
626
|
+
for (const p of n.properties) {
|
|
627
|
+
if (p.type === 'Property') walk(p.value);
|
|
628
|
+
else if (p.type === 'RestElement') walk(p.argument);
|
|
629
|
+
}
|
|
630
|
+
} else if (n.type === 'ArrayPattern' && Array.isArray(n.elements)) {
|
|
631
|
+
for (const e of n.elements) if (e) walk(e);
|
|
632
|
+
} else if (n.type === 'AssignmentPattern') {
|
|
633
|
+
walk(n.left);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
walk(node);
|
|
637
|
+
return names;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Converts Acorn AST nodes or arrays of statements into valid JavaScript source code strings.
|
|
642
|
+
*/
|
|
643
|
+
export function astToJS(node: any, locals?: Set<string>): string {
|
|
644
|
+
if (node === null || node === undefined) return 'undefined';
|
|
645
|
+
if (typeof node !== 'object') return typeof node === 'string' ? JSON.stringify(node) : String(node);
|
|
646
|
+
if (Array.isArray(node)) return node.map((n) => astToJS(n, locals)).join('; ');
|
|
647
|
+
|
|
648
|
+
switch (node.type) {
|
|
649
|
+
case 'Identifier':
|
|
650
|
+
if (locals && locals.has(node.name)) return node.name;
|
|
651
|
+
return `((typeof inScopeChain === 'function' ? inScopeChain(scope, ${JSON.stringify(node.name)}) : Object.prototype.hasOwnProperty.call(scope || {}, ${JSON.stringify(node.name)})) ? scope[${JSON.stringify(node.name)}] : (typeof globalThis !== 'undefined' && Object.prototype.hasOwnProperty.call(globalThis, ${JSON.stringify(node.name)}) ? globalThis[${JSON.stringify(node.name)}] : undefined))`;
|
|
652
|
+
|
|
653
|
+
case 'Literal':
|
|
654
|
+
if (typeof node.raw === 'string') return node.raw;
|
|
655
|
+
return typeof node.value === 'string' ? JSON.stringify(node.value) : String(node.value);
|
|
656
|
+
|
|
657
|
+
case 'BinaryExpression':
|
|
658
|
+
case 'LogicalExpression':
|
|
659
|
+
return `(${astToJS(node.left, locals)} ${node.operator} ${astToJS(node.right, locals)})`;
|
|
660
|
+
|
|
661
|
+
case 'UnaryExpression':
|
|
662
|
+
return `(${node.operator} ${astToJS(node.argument, locals)})`;
|
|
663
|
+
|
|
664
|
+
case 'ConditionalExpression':
|
|
665
|
+
return `(${astToJS(node.test, locals)} ? ${astToJS(node.consequent, locals)} : ${astToJS(node.alternate, locals)})`;
|
|
666
|
+
|
|
667
|
+
case 'MemberExpression':
|
|
668
|
+
return node.computed
|
|
669
|
+
? `(${astToJS(node.object, locals)}[${astToJS(node.property, locals)}])`
|
|
670
|
+
: `(${astToJS(node.object, locals)}.${node.property.name})`;
|
|
671
|
+
|
|
672
|
+
case 'CallExpression': {
|
|
673
|
+
const calleeJS = astToJS(node.callee, locals);
|
|
674
|
+
const argsJS = node.arguments ? node.arguments.map((arg: any) => astToJS(arg, locals)).join(', ') : '';
|
|
675
|
+
const rawCall = `(${calleeJS}(${argsJS}))`;
|
|
676
|
+
if (
|
|
677
|
+
node.callee?.type === 'MemberExpression' &&
|
|
678
|
+
node.callee.property?.type === 'Identifier'
|
|
679
|
+
) {
|
|
680
|
+
const rootObjName = getRootIdentifier(node.callee.object);
|
|
681
|
+
const methodName = node.callee.property.name;
|
|
682
|
+
const arrayMutators = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
683
|
+
if (rootObjName && arrayMutators.includes(methodName) && (!locals || !locals.has(rootObjName))) {
|
|
684
|
+
return `(() => { const _res = ${rawCall}; if (typeof setScopeValue === 'function' && scope) setScopeValue(scope, ${JSON.stringify(rootObjName)}, scope[${JSON.stringify(rootObjName)}]); return _res; })()`;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return rawCall;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
case 'AssignmentExpression': {
|
|
691
|
+
const valJS = astToJS(node.right, locals);
|
|
692
|
+
if (node.left?.type === 'Identifier') {
|
|
693
|
+
const name = node.left.name;
|
|
694
|
+
if (locals && locals.has(name)) {
|
|
695
|
+
return `(${name} ${node.operator} ${valJS})`;
|
|
696
|
+
}
|
|
697
|
+
if (node.operator === '=') {
|
|
698
|
+
return `(typeof setScopeValue === 'function' ? setScopeValue(scope, ${JSON.stringify(name)}, ${valJS}) : ((scope || {})[${JSON.stringify(name)}] = ${valJS}))`;
|
|
699
|
+
} else {
|
|
700
|
+
const op = node.operator.slice(0, -1);
|
|
701
|
+
return `(typeof setScopeValue === 'function' ? setScopeValue(scope, ${JSON.stringify(name)}, (scope[${JSON.stringify(name)}] ${op} ${valJS})) : ((scope || {})[${JSON.stringify(name)}] ${node.operator} ${valJS}))`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (node.left?.type === 'MemberExpression') {
|
|
705
|
+
const rootName = getRootIdentifier(node.left);
|
|
706
|
+
const rawAssign = `(${astToJS(node.left, locals)} ${node.operator} ${valJS})`;
|
|
707
|
+
if (rootName && (!locals || !locals.has(rootName))) {
|
|
708
|
+
return `(() => { const _res = ${rawAssign}; if (typeof setScopeValue === 'function' && scope) setScopeValue(scope, ${JSON.stringify(rootName)}, scope[${JSON.stringify(rootName)}]); return _res; })()`;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (node.left?.type === 'ArrayPattern') {
|
|
712
|
+
const setCalls: string[] = [];
|
|
713
|
+
const elems = node.left.elements || [];
|
|
714
|
+
for (let i = 0; i < elems.length; i++) {
|
|
715
|
+
const el = elems[i];
|
|
716
|
+
if (el?.type === 'Identifier') {
|
|
717
|
+
const varName = el.name;
|
|
718
|
+
if (!locals || !locals.has(varName)) {
|
|
719
|
+
setCalls.push(`if (typeof setScopeValue === 'function' && scope) setScopeValue(scope, ${JSON.stringify(varName)}, _val[${i}]);`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return `(() => { const _val = ${valJS} || []; ${setCalls.join(' ')} return _val; })()`;
|
|
724
|
+
}
|
|
725
|
+
if (node.left?.type === 'ObjectPattern') {
|
|
726
|
+
const setCalls: string[] = [];
|
|
727
|
+
const props = node.left.properties || [];
|
|
728
|
+
for (const p of props) {
|
|
729
|
+
if (p.type === 'Property' && p.value?.type === 'Identifier') {
|
|
730
|
+
const propKey = p.key?.name || (typeof p.key?.value === 'string' ? p.key.value : String(p.key?.value));
|
|
731
|
+
const varName = p.value.name;
|
|
732
|
+
if (!locals || !locals.has(varName)) {
|
|
733
|
+
setCalls.push(`if (typeof setScopeValue === 'function' && scope) setScopeValue(scope, ${JSON.stringify(varName)}, _val[${JSON.stringify(propKey)}]);`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return `(() => { const _val = ${valJS} || {}; ${setCalls.join(' ')} return _val; })()`;
|
|
738
|
+
}
|
|
739
|
+
return `(${astToJS(node.left, locals)} ${node.operator} ${valJS})`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
case 'UpdateExpression': {
|
|
743
|
+
if (node.argument?.type === 'Identifier') {
|
|
744
|
+
const name = node.argument.name;
|
|
745
|
+
if (locals && locals.has(name)) {
|
|
746
|
+
return node.prefix ? `(${node.operator}${name})` : `(${name}${node.operator})`;
|
|
747
|
+
}
|
|
748
|
+
const op = node.operator === '++' ? '+' : '-';
|
|
749
|
+
if (node.prefix) {
|
|
750
|
+
return `(typeof setScopeValue === 'function' ? (setScopeValue(scope, ${JSON.stringify(name)}, (Number(scope[${JSON.stringify(name)}]) || 0) ${op} 1), scope[${JSON.stringify(name)}]) : ((scope || {})[${JSON.stringify(name)}] = (Number((scope || {})[${JSON.stringify(name)}]) || 0) ${op} 1))`;
|
|
751
|
+
} else {
|
|
752
|
+
return `(() => { const _v = Number(scope[${JSON.stringify(name)}]) || 0; if (typeof setScopeValue === 'function') setScopeValue(scope, ${JSON.stringify(name)}, _v ${op} 1); else (scope || {})[${JSON.stringify(name)}] = _v ${op} 1; return _v; })()`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (node.argument?.type === 'MemberExpression') {
|
|
756
|
+
const rootName = getRootIdentifier(node.argument);
|
|
757
|
+
const rawUpdate = node.prefix
|
|
758
|
+
? `(${node.operator}${astToJS(node.argument, locals)})`
|
|
759
|
+
: `(${astToJS(node.argument, locals)}${node.operator})`;
|
|
760
|
+
if (rootName && (!locals || !locals.has(rootName))) {
|
|
761
|
+
return `(() => { const _res = ${rawUpdate}; if (typeof setScopeValue === 'function' && scope) setScopeValue(scope, ${JSON.stringify(rootName)}, scope[${JSON.stringify(rootName)}]); return _res; })()`;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return node.prefix
|
|
765
|
+
? `(${node.operator}${astToJS(node.argument, locals)})`
|
|
766
|
+
: `(${astToJS(node.argument, locals)}${node.operator})`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
case 'SequenceExpression':
|
|
770
|
+
return `(${node.expressions ? node.expressions.map((e: any) => astToJS(e, locals)).join(', ') : ''})`;
|
|
771
|
+
|
|
772
|
+
case 'ArrayExpression':
|
|
773
|
+
return `[${node.elements ? node.elements.map((el: any) => el?.type === 'SpreadElement' ? '...' + astToJS(el.argument, locals) : astToJS(el, locals)).join(', ') : ''}]`;
|
|
774
|
+
|
|
775
|
+
case 'ObjectExpression':
|
|
776
|
+
return `{${node.properties ? node.properties.map((prop: any) => prop.type === 'SpreadElement' ? '...' + astToJS(prop.argument, locals) : `${prop.computed ? '[' + astToJS(prop.key, locals) + ']' : prop.key.name}: ${astToJS(prop.value, locals)}`).join(', ') : ''}}`;
|
|
777
|
+
|
|
778
|
+
case 'SpreadElement':
|
|
779
|
+
return `...${astToJS(node.argument, locals)}`;
|
|
780
|
+
|
|
781
|
+
case 'TemplateLiteral':
|
|
782
|
+
return `\`${node.quasis ? node.quasis.map((q: any, i: number) => (q.value?.raw ?? '') + (node.expressions && node.expressions[i] ? '\${' + astToJS(node.expressions[i], locals) + '}' : '')).join('') : ''}\``;
|
|
783
|
+
|
|
784
|
+
case 'TaggedTemplateExpression': {
|
|
785
|
+
const tagJS = astToJS(node.tag, locals);
|
|
786
|
+
const quasiJS = astToJS(node.quasi, locals);
|
|
787
|
+
return `${tagJS}${quasiJS}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
case 'ThisExpression':
|
|
791
|
+
return 'scope';
|
|
792
|
+
|
|
793
|
+
case 'BlockStatement': {
|
|
794
|
+
const newLocals = new Set(locals);
|
|
795
|
+
const stmts = node.body ? node.body.map((stmt: any) => astToJS(stmt, newLocals)).filter(Boolean) : [];
|
|
796
|
+
// Emit as a real block `{ }` so that `return`/`break`/`continue` inside are valid statements.
|
|
797
|
+
return `{ ${stmts.join('; ')}; }`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
case 'ExpressionStatement':
|
|
801
|
+
return astToJS(node.expression, locals);
|
|
802
|
+
|
|
803
|
+
case 'ReturnStatement':
|
|
804
|
+
return `return ${node.argument ? astToJS(node.argument, locals) : ''}`;
|
|
805
|
+
|
|
806
|
+
case 'BreakStatement':
|
|
807
|
+
return node.label ? `break ${node.label.name}` : 'break';
|
|
808
|
+
|
|
809
|
+
case 'ContinueStatement':
|
|
810
|
+
return node.label ? `continue ${node.label.name}` : 'continue';
|
|
811
|
+
|
|
812
|
+
case 'IfStatement': {
|
|
813
|
+
// Emit a real if/else statement (not a ternary) so that `return`, `break`,
|
|
814
|
+
// and `continue` inside branches are valid in their enclosing function/loop.
|
|
815
|
+
const testJS = astToJS(node.test, locals);
|
|
816
|
+
const consJS = node.consequent.type === 'BlockStatement'
|
|
817
|
+
? astToJS(node.consequent, locals)
|
|
818
|
+
: `{ ${astToJS(node.consequent, locals)}; }`;
|
|
819
|
+
const altJS = node.alternate
|
|
820
|
+
? ` else ${node.alternate.type === 'BlockStatement' || node.alternate.type === 'IfStatement'
|
|
821
|
+
? astToJS(node.alternate, locals)
|
|
822
|
+
: `{ ${astToJS(node.alternate, locals)}; }`}`
|
|
823
|
+
: '';
|
|
824
|
+
return `if (${testJS}) ${consJS}${altJS}`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
case 'ForStatement': {
|
|
828
|
+
const newLocals = new Set(locals);
|
|
829
|
+
if (node.init?.type === 'VariableDeclaration' && node.init.declarations) {
|
|
830
|
+
for (const d of node.init.declarations) {
|
|
831
|
+
const varName = d.id?.name;
|
|
832
|
+
if (varName) newLocals.add(varName);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
let initJS = '';
|
|
836
|
+
if (node.init?.type === 'VariableDeclaration' && node.init.declarations) {
|
|
837
|
+
initJS = 'let ' + node.init.declarations.map((d: any) => `${d.id.name} = ${d.init ? astToJS(d.init, newLocals) : 'undefined'}`).join(', ');
|
|
838
|
+
} else if (node.init) {
|
|
839
|
+
initJS = astToJS(node.init, newLocals);
|
|
840
|
+
}
|
|
841
|
+
const testJS = node.test ? astToJS(node.test, newLocals) : '';
|
|
842
|
+
const updateJS = node.update ? astToJS(node.update, newLocals) : '';
|
|
843
|
+
const bodyJS = node.body ? astToJS(node.body, newLocals) : '';
|
|
844
|
+
return `(() => { for (${initJS}; ${testJS}; ${updateJS}) ${bodyJS}; })()`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
case 'ForOfStatement': {
|
|
848
|
+
const newLocals = new Set(locals);
|
|
849
|
+
const varName = node.left?.type === 'VariableDeclaration' ? node.left.declarations[0]?.id?.name : node.left?.name;
|
|
850
|
+
if (varName) newLocals.add(varName);
|
|
851
|
+
return `(() => { const _iter = (typeof resolveIterable === 'function' ? resolveIterable(${astToJS(node.right, locals)}) : (${astToJS(node.right, locals)} || [])); for (let ${varName} of _iter) { if (scope) scope[${JSON.stringify(varName)}] = ${varName}; ${astToJS(node.body, newLocals)}; } })()`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
case 'ForInStatement': {
|
|
855
|
+
const newLocals = new Set(locals);
|
|
856
|
+
const varName = node.left?.type === 'VariableDeclaration' ? node.left.declarations[0]?.id?.name : node.left?.name;
|
|
857
|
+
if (varName) newLocals.add(varName);
|
|
858
|
+
return `(() => { const _obj = ${astToJS(node.right, locals)}; if (_obj) { for (let ${varName} in _obj) { if (scope) scope[${JSON.stringify(varName)}] = ${varName}; ${astToJS(node.body, newLocals)}; } } })()`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
case 'WhileStatement':
|
|
862
|
+
return `(() => { while (${astToJS(node.test, locals)}) ${astToJS(node.body, locals)}; })()`;
|
|
863
|
+
|
|
864
|
+
case 'DoWhileStatement':
|
|
865
|
+
return `(() => { do ${astToJS(node.body, locals)} while (${astToJS(node.test, locals)}); })()`;
|
|
866
|
+
|
|
867
|
+
case 'ArrayPattern': {
|
|
868
|
+
const elemsJS = node.elements
|
|
869
|
+
? node.elements.map((el: any) => el ? (el.type === 'RestElement' ? '...' + astToJS(el.argument, locals) : (el.type === 'AssignmentPattern' ? `${astToJS(el.left, locals)} = ${astToJS(el.right, locals)}` : (el.name || astToJS(el, locals)))) : '').join(', ')
|
|
870
|
+
: '';
|
|
871
|
+
return `[ ${elemsJS} ]`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
case 'ObjectPattern': {
|
|
875
|
+
const propsJS = node.properties ? node.properties.map((p: any) => {
|
|
876
|
+
if (p.type === 'Property') {
|
|
877
|
+
const k = p.key?.name || astToJS(p.key, locals);
|
|
878
|
+
const v = p.value?.name || astToJS(p.value, locals);
|
|
879
|
+
return k === v ? k : `${k}: ${v}`;
|
|
880
|
+
}
|
|
881
|
+
if (p.type === 'RestElement') {
|
|
882
|
+
return `...${astToJS(p.argument, locals)}`;
|
|
883
|
+
}
|
|
884
|
+
return '';
|
|
885
|
+
}).filter(Boolean).join(', ') : '';
|
|
886
|
+
return `{ ${propsJS} }`;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
case 'VariableDeclaration': {
|
|
890
|
+
const newLocals = new Set(locals);
|
|
891
|
+
const declsArr: string[] = [];
|
|
892
|
+
if (node.declarations) {
|
|
893
|
+
for (const d of node.declarations) {
|
|
894
|
+
if (d.id?.type === 'ObjectPattern') {
|
|
895
|
+
const valJS = d.init ? astToJS(d.init, locals) : 'undefined';
|
|
896
|
+
for (const prop of (d.id.properties || [])) {
|
|
897
|
+
if (prop.type === 'Property') {
|
|
898
|
+
const propKey = prop.key?.name || (typeof prop.key?.value === 'string' ? prop.key.value : astToJS(prop.key, locals));
|
|
899
|
+
let varName = prop.value?.name;
|
|
900
|
+
let defaultValJS: string | null = null;
|
|
901
|
+
if (prop.value?.type === 'AssignmentPattern') {
|
|
902
|
+
varName = prop.value.left?.name || astToJS(prop.value.left, locals);
|
|
903
|
+
defaultValJS = astToJS(prop.value.right, locals);
|
|
904
|
+
} else if (!varName) {
|
|
905
|
+
varName = astToJS(prop.value, locals);
|
|
906
|
+
}
|
|
907
|
+
if (varName) {
|
|
908
|
+
newLocals.add(varName);
|
|
909
|
+
const expr = `((${valJS} && (${JSON.stringify(propKey)} in ${valJS})) ? ${valJS}[${JSON.stringify(propKey)}] : ${defaultValJS ?? 'undefined'})`;
|
|
910
|
+
declsArr.push(`(typeof setScopeValue === 'function' ? setScopeValue(scope, ${JSON.stringify(varName)}, ${expr}) : ((scope || {})[${JSON.stringify(varName)}] = ${expr}))`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
const name = d.id?.name || astToJS(d.id, locals);
|
|
916
|
+
if (d.id?.name) newLocals.add(d.id.name);
|
|
917
|
+
const valJS = d.init ? astToJS(d.init, locals) : 'undefined';
|
|
918
|
+
if (locals && d.id?.name && locals.has(d.id.name)) {
|
|
919
|
+
declsArr.push(`${d.id.name} = ${valJS}`);
|
|
920
|
+
} else {
|
|
921
|
+
declsArr.push(`(typeof setScopeValue === 'function' ? setScopeValue(scope, ${JSON.stringify(name)}, ${valJS}) : ((scope || {})[${JSON.stringify(name)}] = ${valJS}))`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return `(${declsArr.filter(Boolean).join(', ') || 'undefined'})`;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
case 'FunctionDeclaration': {
|
|
930
|
+
const name = node.id?.name;
|
|
931
|
+
const newLocals = new Set(locals);
|
|
932
|
+
if (name) newLocals.add(name);
|
|
933
|
+
const paramNames: string[] = [];
|
|
934
|
+
if (node.params) {
|
|
935
|
+
for (const p of node.params) {
|
|
936
|
+
if (p.type === 'AssignmentPattern') {
|
|
937
|
+
const pName = p.left?.name || astToJS(p.left, locals);
|
|
938
|
+
const defaultVal = astToJS(p.right, locals);
|
|
939
|
+
if (p.left?.name) newLocals.add(p.left.name);
|
|
940
|
+
paramNames.push(`${pName} = ${defaultVal}`);
|
|
941
|
+
} else {
|
|
942
|
+
const pName = p.name || p.id?.name || (p.left && p.left.name) || astToJS(p, locals);
|
|
943
|
+
if (pName) {
|
|
944
|
+
paramNames.push(pName);
|
|
945
|
+
newLocals.add(pName);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (node.body?.type === 'BlockStatement' && Array.isArray(node.body.body)) {
|
|
951
|
+
for (const stmt of node.body.body) {
|
|
952
|
+
if (stmt.type === 'VariableDeclaration' && stmt.declarations) {
|
|
953
|
+
for (const d of stmt.declarations) {
|
|
954
|
+
if (d.id?.name) newLocals.add(d.id.name);
|
|
955
|
+
}
|
|
956
|
+
} else if (stmt.type === 'FunctionDeclaration' && stmt.id?.name) {
|
|
957
|
+
newLocals.add(stmt.id.name);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const paramsJS = paramNames.join(', ');
|
|
962
|
+
const bodyCode = node.body?.type === 'BlockStatement'
|
|
963
|
+
? `{ ${node.body.body.map((s: any) => astToJS(s, newLocals)).filter(Boolean).join('; ')}; }`
|
|
964
|
+
: `{ ${astToJS(node.body, newLocals)}; }`;
|
|
965
|
+
const fnCode = `function ${name || ''}(${paramsJS}) ${bodyCode}`;
|
|
966
|
+
if (name) {
|
|
967
|
+
return `(typeof setScopeValue === 'function' ? setScopeValue(scope, ${JSON.stringify(name)}, ${fnCode}) : ((scope || {})[${JSON.stringify(name)}] = ${fnCode}))`;
|
|
968
|
+
}
|
|
969
|
+
return fnCode;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
case 'ArrowFunctionExpression':
|
|
973
|
+
case 'FunctionExpression': {
|
|
974
|
+
const newLocals = new Set(locals);
|
|
975
|
+
const paramNames: string[] = [];
|
|
976
|
+
if (node.params) {
|
|
977
|
+
for (const p of node.params) {
|
|
978
|
+
if (p.type === 'AssignmentPattern') {
|
|
979
|
+
const pName = p.left?.name || astToJS(p.left, locals);
|
|
980
|
+
const defaultVal = astToJS(p.right, locals);
|
|
981
|
+
if (p.left?.name) newLocals.add(p.left.name);
|
|
982
|
+
paramNames.push(`${pName} = ${defaultVal}`);
|
|
983
|
+
} else {
|
|
984
|
+
const pName = p.name || p.id?.name || (p.left && p.left.name) || astToJS(p, locals);
|
|
985
|
+
if (pName) {
|
|
986
|
+
paramNames.push(pName);
|
|
987
|
+
newLocals.add(pName);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (node.body?.type === 'BlockStatement' && Array.isArray(node.body.body)) {
|
|
993
|
+
for (const stmt of node.body.body) {
|
|
994
|
+
if (stmt.type === 'VariableDeclaration' && stmt.declarations) {
|
|
995
|
+
for (const d of stmt.declarations) {
|
|
996
|
+
if (d.id?.name) newLocals.add(d.id.name);
|
|
997
|
+
}
|
|
998
|
+
} else if (stmt.type === 'FunctionDeclaration' && stmt.id?.name) {
|
|
999
|
+
newLocals.add(stmt.id.name);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const paramsJS = paramNames.join(', ');
|
|
1004
|
+
if (node.body?.type === 'BlockStatement') {
|
|
1005
|
+
const bodyCode = `{ ${node.body.body.map((s: any) => astToJS(s, newLocals)).filter(Boolean).join('; ')}; }`;
|
|
1006
|
+
return `((${paramsJS}) => ${bodyCode})`;
|
|
1007
|
+
}
|
|
1008
|
+
const bodyJS = astToJS(node.body, newLocals);
|
|
1009
|
+
return `((${paramsJS}) => ${bodyJS})`;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
case 'NewExpression':
|
|
1013
|
+
return `new (${astToJS(node.callee, locals)})(${node.arguments ? node.arguments.map((a: any) => astToJS(a, locals)).join(', ') : ''})`;
|
|
1014
|
+
|
|
1015
|
+
case 'EmptyStatement':
|
|
1016
|
+
case 'ImportDeclaration':
|
|
1017
|
+
case 'ImportSpecifier':
|
|
1018
|
+
case 'ImportDefaultSpecifier':
|
|
1019
|
+
case 'ImportNamespaceSpecifier':
|
|
1020
|
+
return '';
|
|
1021
|
+
|
|
1022
|
+
case 'ParenthesizedExpression':
|
|
1023
|
+
case 'ChainExpression':
|
|
1024
|
+
return astToJS(node.expression, locals);
|
|
1025
|
+
|
|
1026
|
+
default:
|
|
1027
|
+
return '';
|
|
1028
|
+
}
|
|
1029
|
+
}
|