@lewin671/python-vm 0.1.1 → 0.1.3
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/README.md +58 -57
- package/README_zh-CN.md +58 -57
- package/dist/common/string-token.d.ts +5 -0
- package/dist/common/string-token.d.ts.map +1 -0
- package/dist/common/string-token.js +23 -0
- package/dist/common/string-token.js.map +1 -0
- package/dist/compiler/cfg_builder.d.ts +48 -0
- package/dist/compiler/cfg_builder.d.ts.map +1 -0
- package/dist/compiler/cfg_builder.js +1462 -0
- package/dist/compiler/cfg_builder.js.map +1 -0
- package/dist/compiler/compiler.d.ts.map +1 -0
- package/dist/compiler/compiler.js +27 -0
- package/dist/compiler/compiler.js.map +1 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/linearizer.d.ts +7 -0
- package/dist/compiler/linearizer.d.ts.map +1 -0
- package/dist/compiler/linearizer.js +112 -0
- package/dist/compiler/linearizer.js.map +1 -0
- package/dist/compiler/serializer.d.ts +17 -0
- package/dist/compiler/serializer.d.ts.map +1 -0
- package/dist/compiler/serializer.js +70 -0
- package/dist/compiler/serializer.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -8
- package/dist/index.js.map +1 -1
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +25 -11
- package/dist/lexer/lexer.js.map +1 -1
- package/dist/parser/expressions.d.ts.map +1 -1
- package/dist/parser/expressions.js +5 -2
- package/dist/parser/expressions.js.map +1 -1
- package/dist/parser/parser.js +3 -3
- package/dist/parser/parser.js.map +1 -1
- package/dist/parser/statements.d.ts.map +1 -1
- package/dist/parser/statements.js +60 -22
- package/dist/parser/statements.js.map +1 -1
- package/dist/python_compiler.d.ts +32 -0
- package/dist/python_compiler.d.ts.map +1 -0
- package/dist/{compiler.js → python_compiler.js} +19 -6
- package/dist/python_compiler.js.map +1 -0
- package/dist/types/ast.d.ts +286 -44
- package/dist/types/ast.d.ts.map +1 -1
- package/dist/types/ast.js +43 -36
- package/dist/types/ast.js.map +1 -1
- package/dist/types/bytecode.d.ts +106 -21
- package/dist/types/bytecode.d.ts.map +1 -1
- package/dist/types/bytecode.js +111 -23
- package/dist/types/bytecode.js.map +1 -1
- package/dist/types/cfg.d.ts +20 -0
- package/dist/types/cfg.d.ts.map +1 -0
- package/dist/types/cfg.js +3 -0
- package/dist/types/cfg.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/vm/builtins.d.ts.map +1 -1
- package/dist/vm/builtins.js +23 -7
- package/dist/vm/builtins.js.map +1 -1
- package/dist/vm/callable.d.ts +6 -6
- package/dist/vm/callable.d.ts.map +1 -1
- package/dist/vm/callable.js +28 -7
- package/dist/vm/callable.js.map +1 -1
- package/dist/vm/execution-helpers.d.ts +17 -0
- package/dist/vm/execution-helpers.d.ts.map +1 -0
- package/dist/vm/execution-helpers.js +334 -0
- package/dist/vm/execution-helpers.js.map +1 -0
- package/dist/vm/execution.d.ts +4 -12
- package/dist/vm/execution.d.ts.map +1 -1
- package/dist/vm/execution.js +764 -255
- package/dist/vm/execution.js.map +1 -1
- package/dist/vm/expression-generator.d.ts +3 -1
- package/dist/vm/expression-generator.d.ts.map +1 -1
- package/dist/vm/expression-generator.js.map +1 -1
- package/dist/vm/expressions.d.ts +6 -6
- package/dist/vm/expressions.d.ts.map +1 -1
- package/dist/vm/expressions.js +29 -54
- package/dist/vm/expressions.js.map +1 -1
- package/dist/vm/imports.d.ts +4 -4
- package/dist/vm/imports.d.ts.map +1 -1
- package/dist/vm/imports.js.map +1 -1
- package/dist/vm/operations.d.ts +11 -10
- package/dist/vm/operations.d.ts.map +1 -1
- package/dist/vm/operations.js +83 -22
- package/dist/vm/operations.js.map +1 -1
- package/dist/vm/runtime-types.d.ts +64 -26
- package/dist/vm/runtime-types.d.ts.map +1 -1
- package/dist/vm/runtime-types.js +166 -19
- package/dist/vm/runtime-types.js.map +1 -1
- package/dist/vm/statements.d.ts +5 -5
- package/dist/vm/statements.d.ts.map +1 -1
- package/dist/vm/statements.js +95 -10
- package/dist/vm/statements.js.map +1 -1
- package/dist/vm/truthy.d.ts +2 -2
- package/dist/vm/truthy.d.ts.map +1 -1
- package/dist/vm/truthy.js +1 -1
- package/dist/vm/truthy.js.map +1 -1
- package/dist/vm/value-utils.d.ts +23 -25
- package/dist/vm/value-utils.d.ts.map +1 -1
- package/dist/vm/value-utils.js +216 -47
- package/dist/vm/value-utils.js.map +1 -1
- package/dist/vm/vm.d.ts +7 -3
- package/dist/vm/vm.d.ts.map +1 -1
- package/dist/vm/vm.js +3 -0
- package/dist/vm/vm.js.map +1 -1
- package/package.json +3 -3
- package/dist/compiler.d.ts +0 -20
- package/dist/compiler.d.ts.map +0 -1
- package/dist/compiler.js.map +0 -1
- package/dist/compiler_module/compiler.d.ts.map +0 -1
- package/dist/compiler_module/compiler.js +0 -22
- package/dist/compiler_module/compiler.js.map +0 -1
- package/dist/compiler_module/index.d.ts.map +0 -1
- package/dist/compiler_module/index.js.map +0 -1
- /package/dist/{compiler_module → compiler}/compiler.d.ts +0 -0
- /package/dist/{compiler_module → compiler}/index.d.ts +0 -0
- /package/dist/{compiler_module → compiler}/index.js +0 -0
|
@@ -0,0 +1,1462 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CFGBuilder = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const linearizer_1 = require("./linearizer");
|
|
6
|
+
const string_token_1 = require("../common/string-token");
|
|
7
|
+
class CFGBuilder {
|
|
8
|
+
constructor(argcount = 0, initialVarnames = []) {
|
|
9
|
+
this.blocks = [];
|
|
10
|
+
this.blockIdCounter = 0;
|
|
11
|
+
this.constants = [];
|
|
12
|
+
this.names = [];
|
|
13
|
+
this.varnames = [];
|
|
14
|
+
this.argcount = 0;
|
|
15
|
+
this.globalVars = new Set(); // Track global variable declarations
|
|
16
|
+
this.nonlocalVars = new Set(); // Track nonlocal variable declarations
|
|
17
|
+
// Loop context stack for break/continue
|
|
18
|
+
this.loopStack = [];
|
|
19
|
+
this.argcount = argcount;
|
|
20
|
+
this.varnames = [...initialVarnames];
|
|
21
|
+
this.currentBlock = this.createBlock();
|
|
22
|
+
}
|
|
23
|
+
createBlock() {
|
|
24
|
+
const block = {
|
|
25
|
+
id: this.blockIdCounter++,
|
|
26
|
+
instructions: [],
|
|
27
|
+
};
|
|
28
|
+
this.blocks.push(block);
|
|
29
|
+
return block;
|
|
30
|
+
}
|
|
31
|
+
addInstruction(opcode, arg) {
|
|
32
|
+
this.currentBlock.instructions.push(arg !== undefined ? { opcode, arg } : { opcode });
|
|
33
|
+
}
|
|
34
|
+
getConstantIndex(value) {
|
|
35
|
+
const index = this.constants.findIndex(c => c === value);
|
|
36
|
+
if (index !== -1)
|
|
37
|
+
return index;
|
|
38
|
+
this.constants.push(value);
|
|
39
|
+
return this.constants.length - 1;
|
|
40
|
+
}
|
|
41
|
+
getNameIndex(name) {
|
|
42
|
+
const index = this.names.indexOf(name);
|
|
43
|
+
if (index !== -1)
|
|
44
|
+
return index;
|
|
45
|
+
this.names.push(name);
|
|
46
|
+
return this.names.length - 1;
|
|
47
|
+
}
|
|
48
|
+
getVarIndex(name) {
|
|
49
|
+
const index = this.varnames.indexOf(name);
|
|
50
|
+
if (index !== -1)
|
|
51
|
+
return index;
|
|
52
|
+
this.varnames.push(name);
|
|
53
|
+
return this.varnames.length - 1;
|
|
54
|
+
}
|
|
55
|
+
build(ast) {
|
|
56
|
+
const entry = this.currentBlock;
|
|
57
|
+
this.visit(ast);
|
|
58
|
+
if (this.currentBlock.instructions.length === 0 || this.currentBlock.instructions[this.currentBlock.instructions.length - 1].opcode !== types_1.OpCode.RETURN_VALUE) {
|
|
59
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
60
|
+
this.addInstruction(types_1.OpCode.RETURN_VALUE);
|
|
61
|
+
}
|
|
62
|
+
return { entry, blocks: this.blocks };
|
|
63
|
+
}
|
|
64
|
+
getConstants() { return this.constants; }
|
|
65
|
+
getNames() { return this.names; }
|
|
66
|
+
getVarnames() { return this.varnames; }
|
|
67
|
+
getArgcount() { return this.argcount; }
|
|
68
|
+
getGlobals() { return Array.from(this.globalVars); }
|
|
69
|
+
getNonlocals() { return Array.from(this.nonlocalVars); }
|
|
70
|
+
visit(node) {
|
|
71
|
+
if (!node)
|
|
72
|
+
return;
|
|
73
|
+
switch (node.type) {
|
|
74
|
+
case types_1.ASTNodeType.PROGRAM:
|
|
75
|
+
for (const stmt of node.body) {
|
|
76
|
+
this.visit(stmt);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case types_1.ASTNodeType.EXPRESSION_STATEMENT:
|
|
80
|
+
this.visit(node.expression);
|
|
81
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
82
|
+
break;
|
|
83
|
+
case types_1.ASTNodeType.ASSIGNMENT:
|
|
84
|
+
this.visit(node.value);
|
|
85
|
+
for (let i = 0; i < node.targets.length; i++) {
|
|
86
|
+
if (i < node.targets.length - 1) {
|
|
87
|
+
this.addInstruction(types_1.OpCode.DUP_TOP);
|
|
88
|
+
}
|
|
89
|
+
this.visitTarget(node.targets[i], 'store');
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case types_1.ASTNodeType.AUG_ASSIGNMENT:
|
|
93
|
+
// Handle augmented assignment (+=, -=, etc.)
|
|
94
|
+
// For subscripts and attributes, we need to avoid re-evaluating the target
|
|
95
|
+
if (node.target.type === types_1.ASTNodeType.SUBSCRIPT) {
|
|
96
|
+
const target = node.target;
|
|
97
|
+
this.visit(target.object);
|
|
98
|
+
this.visit(target.index);
|
|
99
|
+
this.addInstruction(types_1.OpCode.DUP_TOP_TWO);
|
|
100
|
+
this.addInstruction(types_1.OpCode.LOAD_SUBSCR);
|
|
101
|
+
this.visit(node.value);
|
|
102
|
+
this.addInplaceOperation(node.operator.slice(0, -1));
|
|
103
|
+
this.addInstruction(types_1.OpCode.ROT_THREE);
|
|
104
|
+
this.addInstruction(types_1.OpCode.STORE_SUBSCR);
|
|
105
|
+
}
|
|
106
|
+
else if (node.target.type === types_1.ASTNodeType.ATTRIBUTE) {
|
|
107
|
+
const target = node.target;
|
|
108
|
+
this.visit(target.object);
|
|
109
|
+
this.addInstruction(types_1.OpCode.DUP_TOP);
|
|
110
|
+
this.addInstruction(types_1.OpCode.LOAD_ATTR, this.getNameIndex(target.name));
|
|
111
|
+
this.visit(node.value);
|
|
112
|
+
this.addInplaceOperation(node.operator.slice(0, -1));
|
|
113
|
+
this.addInstruction(types_1.OpCode.STORE_ATTR, this.getNameIndex(target.name));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// For simple names: load, operate in-place, store
|
|
117
|
+
this.visit(node.target);
|
|
118
|
+
this.visit(node.value);
|
|
119
|
+
this.addInplaceOperation(node.operator.slice(0, -1));
|
|
120
|
+
this.visitTarget(node.target, 'store');
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case types_1.ASTNodeType.NUMBER_LITERAL: {
|
|
124
|
+
const raw = node.value;
|
|
125
|
+
if (typeof raw === 'string') {
|
|
126
|
+
const text = raw.replace(/_/g, '');
|
|
127
|
+
const lower = text.toLowerCase();
|
|
128
|
+
if (lower.endsWith('j')) {
|
|
129
|
+
const imagText = lower.slice(0, -1);
|
|
130
|
+
const imag = imagText === '' || imagText === '+' ? 1 : imagText === '-' ? -1 : parseFloat(imagText);
|
|
131
|
+
const val = { __complex__: true, re: 0, im: imag };
|
|
132
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(val));
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (/[.eE]/.test(text)) {
|
|
136
|
+
const val = new Number(parseFloat(text));
|
|
137
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(val));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const val = BigInt(text);
|
|
141
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(val));
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const val = (typeof raw === 'number' && !Number.isInteger(raw)) ? new Number(raw) : BigInt(raw);
|
|
145
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(val));
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case types_1.ASTNodeType.STRING_LITERAL: {
|
|
149
|
+
const { value, isFString } = (0, string_token_1.parseStringToken)(node.value);
|
|
150
|
+
const val = isFString ? { __fstring__: value } : value;
|
|
151
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(val));
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case types_1.ASTNodeType.BOOLEAN_LITERAL:
|
|
155
|
+
case types_1.ASTNodeType.NONE_LITERAL:
|
|
156
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(node.value));
|
|
157
|
+
break;
|
|
158
|
+
case types_1.ASTNodeType.IDENTIFIER:
|
|
159
|
+
// Check if variable is declared global
|
|
160
|
+
if (this.globalVars.has(node.name)) {
|
|
161
|
+
this.addInstruction(types_1.OpCode.LOAD_GLOBAL, this.getNameIndex(node.name));
|
|
162
|
+
}
|
|
163
|
+
else if (this.nonlocalVars.has(node.name)) {
|
|
164
|
+
// nonlocal variables use LOAD_NAME to search enclosing scopes
|
|
165
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex(node.name));
|
|
166
|
+
}
|
|
167
|
+
else if (this.varnames.includes(node.name)) {
|
|
168
|
+
this.addInstruction(types_1.OpCode.LOAD_FAST, this.getVarIndex(node.name));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex(node.name));
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case types_1.ASTNodeType.COMPREHENSION:
|
|
175
|
+
break;
|
|
176
|
+
case types_1.ASTNodeType.BINARY_OPERATION:
|
|
177
|
+
this.visit(node.left);
|
|
178
|
+
this.visit(node.right);
|
|
179
|
+
this.addBinaryOperation(node.operator);
|
|
180
|
+
break;
|
|
181
|
+
case types_1.ASTNodeType.UNARY_OPERATION:
|
|
182
|
+
this.visit(node.operand);
|
|
183
|
+
this.addUnaryOperation(node.operator);
|
|
184
|
+
break;
|
|
185
|
+
case types_1.ASTNodeType.BOOL_OPERATION:
|
|
186
|
+
this.visitBoolOp(node);
|
|
187
|
+
break;
|
|
188
|
+
case types_1.ASTNodeType.IF_EXPRESSION:
|
|
189
|
+
this.visitIfExpression(node);
|
|
190
|
+
break;
|
|
191
|
+
case types_1.ASTNodeType.COMPARE: {
|
|
192
|
+
const operators = node.ops || [];
|
|
193
|
+
if (operators.length === 0) {
|
|
194
|
+
this.visit(node.left);
|
|
195
|
+
}
|
|
196
|
+
else if (operators.length === 1) {
|
|
197
|
+
// Simple comparison: a < b
|
|
198
|
+
this.visit(node.left);
|
|
199
|
+
this.visit(node.comparators[0]);
|
|
200
|
+
this.addCompareOperation(operators[0]);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Chained comparison: a < b < c
|
|
204
|
+
// Compile with short-circuit evaluation
|
|
205
|
+
const endBlock = this.createBlock();
|
|
206
|
+
const cleanupBlock = this.createBlock();
|
|
207
|
+
this.visit(node.left);
|
|
208
|
+
for (let i = 0; i < operators.length; i++) {
|
|
209
|
+
this.visit(node.comparators[i]);
|
|
210
|
+
if (i < operators.length - 1) {
|
|
211
|
+
// Not the last: duplicate and rotate for next comparison
|
|
212
|
+
this.addInstruction(types_1.OpCode.DUP_TOP);
|
|
213
|
+
this.addInstruction(types_1.OpCode.ROT_THREE);
|
|
214
|
+
}
|
|
215
|
+
this.addCompareOperation(operators[i]);
|
|
216
|
+
if (i < operators.length - 1) {
|
|
217
|
+
// Short-circuit: if false, jump to cleanup with extra value on stack
|
|
218
|
+
const nextBlock = this.createBlock();
|
|
219
|
+
this.currentBlock.jumpCondition = 'if_false_or_pop';
|
|
220
|
+
this.currentBlock.jumpTarget = cleanupBlock;
|
|
221
|
+
this.currentBlock.next = nextBlock;
|
|
222
|
+
this.currentBlock = nextBlock;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// All comparisons succeeded, fall through to end
|
|
226
|
+
this.currentBlock.next = endBlock;
|
|
227
|
+
// Cleanup block: remove duplicate value from stack after failed comparison
|
|
228
|
+
this.currentBlock = cleanupBlock;
|
|
229
|
+
this.addInstruction(types_1.OpCode.ROT_TWO);
|
|
230
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
231
|
+
this.currentBlock.next = endBlock;
|
|
232
|
+
// End block: result is on stack
|
|
233
|
+
this.currentBlock = endBlock;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case types_1.ASTNodeType.CALL:
|
|
238
|
+
this.visit(node.callee);
|
|
239
|
+
{
|
|
240
|
+
const rawArgs = node.args;
|
|
241
|
+
const hasStar = rawArgs.some(a => a && a.type === types_1.ASTNodeType.STAR_ARG);
|
|
242
|
+
const hasKw = rawArgs.some(a => a && a.type === types_1.ASTNodeType.KW_ARG);
|
|
243
|
+
if (hasStar || hasKw) {
|
|
244
|
+
const starArg = rawArgs.find(a => a && a.type === types_1.ASTNodeType.STAR_ARG);
|
|
245
|
+
if (starArg && 'value' in starArg) {
|
|
246
|
+
this.visit(starArg.value);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.addInstruction(types_1.OpCode.BUILD_TUPLE, 0);
|
|
250
|
+
}
|
|
251
|
+
const kwArg = rawArgs.find(a => a && a.type === types_1.ASTNodeType.KW_ARG);
|
|
252
|
+
if (kwArg && 'value' in kwArg) {
|
|
253
|
+
this.visit(kwArg.value);
|
|
254
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION_EX, 1);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION_EX, 0);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const positional = [];
|
|
262
|
+
const keyword = [];
|
|
263
|
+
for (const a of rawArgs) {
|
|
264
|
+
if (a && a.type === types_1.ASTNodeType.KEYWORD_ARG) {
|
|
265
|
+
keyword.push(a);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
positional.push(a);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const a of positional) {
|
|
272
|
+
this.visit(a);
|
|
273
|
+
}
|
|
274
|
+
for (const k of keyword) {
|
|
275
|
+
this.visit(k.value);
|
|
276
|
+
}
|
|
277
|
+
if (keyword.length > 0) {
|
|
278
|
+
for (const k of keyword) {
|
|
279
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(k.name));
|
|
280
|
+
}
|
|
281
|
+
this.addInstruction(types_1.OpCode.BUILD_TUPLE, keyword.length);
|
|
282
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION_KW, positional.length + keyword.length);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, positional.length);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
case types_1.ASTNodeType.IF_STATEMENT:
|
|
291
|
+
this.visitIf(node);
|
|
292
|
+
break;
|
|
293
|
+
case types_1.ASTNodeType.WHILE_STATEMENT:
|
|
294
|
+
this.visitWhile(node);
|
|
295
|
+
break;
|
|
296
|
+
case types_1.ASTNodeType.FOR_STATEMENT:
|
|
297
|
+
this.visitFor(node);
|
|
298
|
+
break;
|
|
299
|
+
case types_1.ASTNodeType.FUNCTION_DEF:
|
|
300
|
+
this.visitFunctionDef(node);
|
|
301
|
+
break;
|
|
302
|
+
case types_1.ASTNodeType.CLASS_DEF: {
|
|
303
|
+
const subBuilder = new CFGBuilder(0, []);
|
|
304
|
+
const subCfg = subBuilder.build({ type: types_1.ASTNodeType.PROGRAM, body: node.body });
|
|
305
|
+
const linearizer = new linearizer_1.Linearizer();
|
|
306
|
+
const subBytecode = linearizer.linearize(subCfg, subBuilder.getConstants(), subBuilder.getNames(), subBuilder.getVarnames(), 0, [], subBuilder.getGlobals(), subBuilder.getNonlocals());
|
|
307
|
+
subBytecode.name = `< classbody ${node.name}> `;
|
|
308
|
+
// build_class(class_body_fn, name, *bases)
|
|
309
|
+
this.addInstruction(types_1.OpCode.LOAD_BUILD_CLASS);
|
|
310
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(subBytecode));
|
|
311
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(subBytecode.name));
|
|
312
|
+
this.addInstruction(types_1.OpCode.MAKE_FUNCTION, 0);
|
|
313
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(node.name));
|
|
314
|
+
for (const base of node.bases) {
|
|
315
|
+
this.visit(base);
|
|
316
|
+
}
|
|
317
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 2 + node.bases.length);
|
|
318
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(node.name));
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case types_1.ASTNodeType.LAMBDA:
|
|
322
|
+
this.visitLambda(node);
|
|
323
|
+
break;
|
|
324
|
+
case types_1.ASTNodeType.RETURN_STATEMENT:
|
|
325
|
+
if (node.value) {
|
|
326
|
+
this.visit(node.value);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
330
|
+
}
|
|
331
|
+
this.addInstruction(types_1.OpCode.RETURN_VALUE);
|
|
332
|
+
break;
|
|
333
|
+
case types_1.ASTNodeType.YIELD:
|
|
334
|
+
if (node.value) {
|
|
335
|
+
this.visit(node.value);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
339
|
+
}
|
|
340
|
+
this.addInstruction(types_1.OpCode.YIELD_VALUE);
|
|
341
|
+
break;
|
|
342
|
+
case types_1.ASTNodeType.ATTRIBUTE:
|
|
343
|
+
this.visit(node.object);
|
|
344
|
+
this.addInstruction(types_1.OpCode.LOAD_ATTR, this.getNameIndex(node.name));
|
|
345
|
+
break;
|
|
346
|
+
case types_1.ASTNodeType.SUBSCRIPT:
|
|
347
|
+
this.visit(node.object);
|
|
348
|
+
this.visit(node.index);
|
|
349
|
+
this.addInstruction(types_1.OpCode.LOAD_SUBSCR);
|
|
350
|
+
break;
|
|
351
|
+
case types_1.ASTNodeType.LIST_LITERAL:
|
|
352
|
+
for (const el of node.elements)
|
|
353
|
+
this.visit(el);
|
|
354
|
+
this.addInstruction(types_1.OpCode.BUILD_LIST, node.elements.length);
|
|
355
|
+
break;
|
|
356
|
+
case types_1.ASTNodeType.LIST_COMP:
|
|
357
|
+
case types_1.ASTNodeType.DICT_COMP:
|
|
358
|
+
case types_1.ASTNodeType.SET_COMP:
|
|
359
|
+
case types_1.ASTNodeType.GENERATOR_EXPR:
|
|
360
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(node));
|
|
361
|
+
this.addInstruction(types_1.OpCode.EVAL_AST);
|
|
362
|
+
break;
|
|
363
|
+
case types_1.ASTNodeType.TUPLE_LITERAL:
|
|
364
|
+
for (const el of node.elements)
|
|
365
|
+
this.visit(el);
|
|
366
|
+
this.addInstruction(types_1.OpCode.BUILD_TUPLE, node.elements.length);
|
|
367
|
+
break;
|
|
368
|
+
case types_1.ASTNodeType.DICT_LITERAL: {
|
|
369
|
+
const entries = node.entries || [];
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
this.visit(entry.key);
|
|
372
|
+
this.visit(entry.value);
|
|
373
|
+
}
|
|
374
|
+
this.addInstruction(types_1.OpCode.BUILD_MAP, entries.length);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case types_1.ASTNodeType.SET_LITERAL:
|
|
378
|
+
for (const el of node.elements)
|
|
379
|
+
this.visit(el);
|
|
380
|
+
this.addInstruction(types_1.OpCode.BUILD_SET, node.elements.length);
|
|
381
|
+
break;
|
|
382
|
+
case types_1.ASTNodeType.SLICE:
|
|
383
|
+
// Handle slice: start:end:step
|
|
384
|
+
// Be careful: 0 is falsy but valid, so check for null/undefined explicitly
|
|
385
|
+
if (node.start !== null && node.start !== undefined) {
|
|
386
|
+
this.visit(node.start);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
390
|
+
}
|
|
391
|
+
if (node.end !== null && node.end !== undefined) {
|
|
392
|
+
this.visit(node.end);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
396
|
+
}
|
|
397
|
+
if (node.step !== null && node.step !== undefined) {
|
|
398
|
+
this.visit(node.step);
|
|
399
|
+
this.addInstruction(types_1.OpCode.BUILD_SLICE, 3);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
this.addInstruction(types_1.OpCode.BUILD_SLICE, 2);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
break;
|
|
406
|
+
case types_1.ASTNodeType.ASSERT_STATEMENT: {
|
|
407
|
+
this.visit(node.test);
|
|
408
|
+
const skipBlock = this.createBlock();
|
|
409
|
+
this.currentBlock.jumpCondition = 'if_true';
|
|
410
|
+
this.currentBlock.jumpTarget = skipBlock;
|
|
411
|
+
const failBlock = this.createBlock();
|
|
412
|
+
this.currentBlock.next = failBlock;
|
|
413
|
+
this.currentBlock = failBlock;
|
|
414
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex('AssertionError'));
|
|
415
|
+
if (node.message) {
|
|
416
|
+
this.visit(node.message);
|
|
417
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 1);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 0);
|
|
421
|
+
}
|
|
422
|
+
this.addInstruction(types_1.OpCode.RAISE_VARARGS, 1);
|
|
423
|
+
this.currentBlock = skipBlock;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case types_1.ASTNodeType.RAISE_STATEMENT: {
|
|
427
|
+
if (node.exception) {
|
|
428
|
+
this.visit(node.exception);
|
|
429
|
+
this.addInstruction(types_1.OpCode.RAISE_VARARGS, 1);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
this.addInstruction(types_1.OpCode.RAISE_VARARGS, 0);
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case types_1.ASTNodeType.PASS_STATEMENT:
|
|
437
|
+
break;
|
|
438
|
+
case types_1.ASTNodeType.BREAK_STATEMENT: {
|
|
439
|
+
if (this.loopStack.length === 0) {
|
|
440
|
+
throw new Error('SyntaxError: break outside loop');
|
|
441
|
+
}
|
|
442
|
+
const loopContext = this.loopStack[this.loopStack.length - 1];
|
|
443
|
+
// For FOR loops, we need to pop the iterator before jumping
|
|
444
|
+
// (FOR_ITER pops it when exhausting, but break doesn't go through FOR_ITER)
|
|
445
|
+
if (loopContext.loopType === 'for') {
|
|
446
|
+
this.addInstruction(types_1.OpCode.POP_TOP); // Pop the iterator
|
|
447
|
+
}
|
|
448
|
+
this.currentBlock.next = loopContext.breakTarget;
|
|
449
|
+
// Create a new unreachable block for any statements after break
|
|
450
|
+
const unreachableAfterBreak = this.createBlock();
|
|
451
|
+
unreachableAfterBreak.reachable = false;
|
|
452
|
+
this.currentBlock = unreachableAfterBreak;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case types_1.ASTNodeType.CONTINUE_STATEMENT: {
|
|
456
|
+
if (this.loopStack.length === 0) {
|
|
457
|
+
throw new Error('SyntaxError: continue not properly in loop');
|
|
458
|
+
}
|
|
459
|
+
const loopCtx = this.loopStack[this.loopStack.length - 1];
|
|
460
|
+
this.currentBlock.next = loopCtx.continueTarget;
|
|
461
|
+
// Create a new unreachable block for any statements after continue
|
|
462
|
+
const unreachableAfterContinue = this.createBlock();
|
|
463
|
+
unreachableAfterContinue.reachable = false;
|
|
464
|
+
this.currentBlock = unreachableAfterContinue;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case types_1.ASTNodeType.GLOBAL_STATEMENT:
|
|
468
|
+
// Global declarations - mark variables as global
|
|
469
|
+
for (const name of node.names || []) {
|
|
470
|
+
this.globalVars.add(name);
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
case types_1.ASTNodeType.NONLOCAL_STATEMENT:
|
|
474
|
+
// Nonlocal declarations - mark variables as nonlocal
|
|
475
|
+
for (const name of node.names || []) {
|
|
476
|
+
this.nonlocalVars.add(name);
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
case types_1.ASTNodeType.DELETE_STATEMENT: {
|
|
480
|
+
// Handle both 'target' (single) and 'targets' (array) for compatibility
|
|
481
|
+
const deleteTargets = node.targets || (node.target ? [node.target] : []);
|
|
482
|
+
for (const target of deleteTargets) {
|
|
483
|
+
this.visitTarget(target, 'delete');
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case types_1.ASTNodeType.IMPORT_STATEMENT:
|
|
488
|
+
for (const name of node.names) {
|
|
489
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(0));
|
|
490
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
491
|
+
this.addInstruction(types_1.OpCode.IMPORT_NAME, this.getNameIndex(name.name));
|
|
492
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(name.asname || name.name));
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
case types_1.ASTNodeType.TRY_STATEMENT:
|
|
496
|
+
this.visitTry(node);
|
|
497
|
+
break;
|
|
498
|
+
case types_1.ASTNodeType.WITH_STATEMENT:
|
|
499
|
+
this.visitWith(node);
|
|
500
|
+
break;
|
|
501
|
+
case types_1.ASTNodeType.MATCH_STATEMENT:
|
|
502
|
+
this.visitMatch(node);
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
console.warn(`CFGBuilder: Unhandled node type ${node.type}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
addBinaryOperation(operator) {
|
|
509
|
+
switch (operator) {
|
|
510
|
+
case '+':
|
|
511
|
+
this.addInstruction(types_1.OpCode.BINARY_ADD);
|
|
512
|
+
break;
|
|
513
|
+
case '-':
|
|
514
|
+
this.addInstruction(types_1.OpCode.BINARY_SUBTRACT);
|
|
515
|
+
break;
|
|
516
|
+
case '*':
|
|
517
|
+
this.addInstruction(types_1.OpCode.BINARY_MULTIPLY);
|
|
518
|
+
break;
|
|
519
|
+
case '/':
|
|
520
|
+
this.addInstruction(types_1.OpCode.BINARY_DIVIDE);
|
|
521
|
+
break;
|
|
522
|
+
case '//':
|
|
523
|
+
this.addInstruction(types_1.OpCode.BINARY_FLOOR_DIVIDE);
|
|
524
|
+
break;
|
|
525
|
+
case '%':
|
|
526
|
+
this.addInstruction(types_1.OpCode.BINARY_MODULO);
|
|
527
|
+
break;
|
|
528
|
+
case '**':
|
|
529
|
+
this.addInstruction(types_1.OpCode.BINARY_POWER);
|
|
530
|
+
break;
|
|
531
|
+
case '<<':
|
|
532
|
+
this.addInstruction(types_1.OpCode.BINARY_LSHIFT);
|
|
533
|
+
break;
|
|
534
|
+
case '>>':
|
|
535
|
+
this.addInstruction(types_1.OpCode.BINARY_RSHIFT);
|
|
536
|
+
break;
|
|
537
|
+
case '&':
|
|
538
|
+
this.addInstruction(types_1.OpCode.BINARY_AND);
|
|
539
|
+
break;
|
|
540
|
+
case '^':
|
|
541
|
+
this.addInstruction(types_1.OpCode.BINARY_XOR);
|
|
542
|
+
break;
|
|
543
|
+
case '|':
|
|
544
|
+
this.addInstruction(types_1.OpCode.BINARY_OR);
|
|
545
|
+
break;
|
|
546
|
+
default: throw new Error(`Unknown operator: ${operator} `);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
addInplaceOperation(operator) {
|
|
550
|
+
switch (operator) {
|
|
551
|
+
case '+':
|
|
552
|
+
this.addInstruction(types_1.OpCode.INPLACE_ADD);
|
|
553
|
+
break;
|
|
554
|
+
case '-':
|
|
555
|
+
this.addInstruction(types_1.OpCode.INPLACE_SUBTRACT);
|
|
556
|
+
break;
|
|
557
|
+
case '*':
|
|
558
|
+
this.addInstruction(types_1.OpCode.INPLACE_MULTIPLY);
|
|
559
|
+
break;
|
|
560
|
+
case '/':
|
|
561
|
+
this.addInstruction(types_1.OpCode.INPLACE_DIVIDE);
|
|
562
|
+
break;
|
|
563
|
+
case '//':
|
|
564
|
+
this.addInstruction(types_1.OpCode.INPLACE_FLOOR_DIVIDE);
|
|
565
|
+
break;
|
|
566
|
+
case '%':
|
|
567
|
+
this.addInstruction(types_1.OpCode.INPLACE_MODULO);
|
|
568
|
+
break;
|
|
569
|
+
case '**':
|
|
570
|
+
this.addInstruction(types_1.OpCode.INPLACE_POWER);
|
|
571
|
+
break;
|
|
572
|
+
case '<<':
|
|
573
|
+
this.addInstruction(types_1.OpCode.INPLACE_LSHIFT);
|
|
574
|
+
break;
|
|
575
|
+
case '>>':
|
|
576
|
+
this.addInstruction(types_1.OpCode.INPLACE_RSHIFT);
|
|
577
|
+
break;
|
|
578
|
+
case '&':
|
|
579
|
+
this.addInstruction(types_1.OpCode.INPLACE_AND);
|
|
580
|
+
break;
|
|
581
|
+
case '^':
|
|
582
|
+
this.addInstruction(types_1.OpCode.INPLACE_XOR);
|
|
583
|
+
break;
|
|
584
|
+
case '|':
|
|
585
|
+
this.addInstruction(types_1.OpCode.INPLACE_OR);
|
|
586
|
+
break;
|
|
587
|
+
default: throw new Error(`Unknown operator: ${operator} `);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
addCompareOperation(operator) {
|
|
591
|
+
let op;
|
|
592
|
+
switch (operator) {
|
|
593
|
+
case '<':
|
|
594
|
+
op = types_1.CompareOp.LT;
|
|
595
|
+
break;
|
|
596
|
+
case '<=':
|
|
597
|
+
op = types_1.CompareOp.LE;
|
|
598
|
+
break;
|
|
599
|
+
case '==':
|
|
600
|
+
op = types_1.CompareOp.EQ;
|
|
601
|
+
break;
|
|
602
|
+
case '!=':
|
|
603
|
+
op = types_1.CompareOp.NE;
|
|
604
|
+
break;
|
|
605
|
+
case '>':
|
|
606
|
+
op = types_1.CompareOp.GT;
|
|
607
|
+
break;
|
|
608
|
+
case '>=':
|
|
609
|
+
op = types_1.CompareOp.GE;
|
|
610
|
+
break;
|
|
611
|
+
case 'in':
|
|
612
|
+
op = types_1.CompareOp.IN;
|
|
613
|
+
break;
|
|
614
|
+
case 'not in':
|
|
615
|
+
op = types_1.CompareOp.NOT_IN;
|
|
616
|
+
break;
|
|
617
|
+
case 'is':
|
|
618
|
+
op = types_1.CompareOp.IS;
|
|
619
|
+
break;
|
|
620
|
+
case 'is not':
|
|
621
|
+
op = types_1.CompareOp.IS_NOT;
|
|
622
|
+
break;
|
|
623
|
+
default: throw new Error(`Unknown compare operator: ${operator} `);
|
|
624
|
+
}
|
|
625
|
+
this.addInstruction(types_1.OpCode.COMPARE_OP, op);
|
|
626
|
+
}
|
|
627
|
+
visitTarget(node, mode) {
|
|
628
|
+
if (mode === 'store') {
|
|
629
|
+
switch (node.type) {
|
|
630
|
+
case types_1.ASTNodeType.IDENTIFIER:
|
|
631
|
+
// Check if variable is declared global
|
|
632
|
+
if (this.globalVars.has(node.name)) {
|
|
633
|
+
this.addInstruction(types_1.OpCode.STORE_GLOBAL, this.getNameIndex(node.name));
|
|
634
|
+
}
|
|
635
|
+
else if (this.nonlocalVars.has(node.name)) {
|
|
636
|
+
// nonlocal variables use STORE_NAME to store in enclosing scopes
|
|
637
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(node.name));
|
|
638
|
+
}
|
|
639
|
+
else if (this.varnames.includes(node.name)) {
|
|
640
|
+
this.addInstruction(types_1.OpCode.STORE_FAST, this.getVarIndex(node.name));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(node.name));
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
case types_1.ASTNodeType.STARRED:
|
|
647
|
+
this.visitTarget(node.target, mode);
|
|
648
|
+
break;
|
|
649
|
+
case types_1.ASTNodeType.ATTRIBUTE:
|
|
650
|
+
this.visit(node.object);
|
|
651
|
+
this.addInstruction(types_1.OpCode.ROT_TWO);
|
|
652
|
+
this.addInstruction(types_1.OpCode.STORE_ATTR, this.getNameIndex(node.name));
|
|
653
|
+
break;
|
|
654
|
+
case types_1.ASTNodeType.SUBSCRIPT:
|
|
655
|
+
this.visit(node.object);
|
|
656
|
+
this.visit(node.index);
|
|
657
|
+
this.addInstruction(types_1.OpCode.STORE_SUBSCR);
|
|
658
|
+
break;
|
|
659
|
+
case types_1.ASTNodeType.TUPLE_LITERAL:
|
|
660
|
+
case types_1.ASTNodeType.LIST_LITERAL: {
|
|
661
|
+
const elements = node.elements || [];
|
|
662
|
+
const starIndex = elements.findIndex((el) => el && el.type === types_1.ASTNodeType.STARRED);
|
|
663
|
+
if (starIndex === -1) {
|
|
664
|
+
this.addInstruction(types_1.OpCode.UNPACK_SEQUENCE, elements.length);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
const prefixCount = starIndex;
|
|
668
|
+
const suffixCount = elements.length - starIndex - 1;
|
|
669
|
+
// Pack prefix/suffix counts into a single arg like CPython: (prefix << 8) | suffix
|
|
670
|
+
this.addInstruction(types_1.OpCode.UNPACK_EX, (prefixCount << 8) | suffixCount);
|
|
671
|
+
}
|
|
672
|
+
for (const el of elements) {
|
|
673
|
+
this.visitTarget(el, mode);
|
|
674
|
+
}
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
default:
|
|
678
|
+
throw new Error(`Unsupported assignment target: ${node.type} `);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else if (mode === 'delete') {
|
|
682
|
+
switch (node.type) {
|
|
683
|
+
case types_1.ASTNodeType.IDENTIFIER:
|
|
684
|
+
if (this.globalVars.has(node.name)) {
|
|
685
|
+
this.addInstruction(types_1.OpCode.DELETE_GLOBAL, this.getNameIndex(node.name));
|
|
686
|
+
}
|
|
687
|
+
else if (this.nonlocalVars.has(node.name)) {
|
|
688
|
+
this.addInstruction(types_1.OpCode.DELETE_NAME, this.getNameIndex(node.name));
|
|
689
|
+
}
|
|
690
|
+
else if (this.varnames.includes(node.name)) {
|
|
691
|
+
this.addInstruction(types_1.OpCode.DELETE_FAST, this.getVarIndex(node.name));
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.addInstruction(types_1.OpCode.DELETE_NAME, this.getNameIndex(node.name));
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
case types_1.ASTNodeType.ATTRIBUTE:
|
|
698
|
+
this.visit(node.object);
|
|
699
|
+
this.addInstruction(types_1.OpCode.DELETE_ATTR, this.getNameIndex(node.name));
|
|
700
|
+
break;
|
|
701
|
+
case types_1.ASTNodeType.SUBSCRIPT:
|
|
702
|
+
this.visit(node.object);
|
|
703
|
+
this.visit(node.index);
|
|
704
|
+
this.addInstruction(types_1.OpCode.DELETE_SUBSCR);
|
|
705
|
+
break;
|
|
706
|
+
case types_1.ASTNodeType.TUPLE_LITERAL:
|
|
707
|
+
case types_1.ASTNodeType.LIST_LITERAL:
|
|
708
|
+
for (const el of node.elements || []) {
|
|
709
|
+
this.visitTarget(el, mode);
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
throw new Error(`Unsupported delete target: ${node.type} `);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
visitIf(node) {
|
|
718
|
+
const thenBlock = this.createBlock();
|
|
719
|
+
// Convert elifs to nested if statements in orelse
|
|
720
|
+
let orelse = node.orelse || [];
|
|
721
|
+
if (node.elifs && node.elifs.length > 0) {
|
|
722
|
+
// Build nested if structure from elifs
|
|
723
|
+
let currentElse = node.orelse || [];
|
|
724
|
+
for (let i = node.elifs.length - 1; i >= 0; i--) {
|
|
725
|
+
const elif = node.elifs[i];
|
|
726
|
+
currentElse = [{
|
|
727
|
+
type: types_1.ASTNodeType.IF_STATEMENT,
|
|
728
|
+
test: elif.test,
|
|
729
|
+
body: elif.body,
|
|
730
|
+
orelse: currentElse,
|
|
731
|
+
elifs: []
|
|
732
|
+
}];
|
|
733
|
+
}
|
|
734
|
+
orelse = currentElse;
|
|
735
|
+
}
|
|
736
|
+
const elseBlock = orelse && orelse.length > 0 ? this.createBlock() : null;
|
|
737
|
+
const mergeBlock = this.createBlock();
|
|
738
|
+
this.visit(node.test);
|
|
739
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
740
|
+
this.currentBlock.jumpTarget = elseBlock || mergeBlock;
|
|
741
|
+
this.currentBlock.next = thenBlock;
|
|
742
|
+
this.currentBlock = thenBlock;
|
|
743
|
+
for (const stmt of node.body) {
|
|
744
|
+
this.visit(stmt);
|
|
745
|
+
}
|
|
746
|
+
if (!this.currentBlock.next && !this.currentBlock.jumpTarget) {
|
|
747
|
+
this.currentBlock.next = mergeBlock;
|
|
748
|
+
}
|
|
749
|
+
if (elseBlock) {
|
|
750
|
+
this.currentBlock = elseBlock;
|
|
751
|
+
for (const stmt of orelse) {
|
|
752
|
+
this.visit(stmt);
|
|
753
|
+
}
|
|
754
|
+
if (!this.currentBlock.next && !this.currentBlock.jumpTarget) {
|
|
755
|
+
this.currentBlock.next = mergeBlock;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
this.currentBlock = mergeBlock;
|
|
759
|
+
}
|
|
760
|
+
visitWhile(node) {
|
|
761
|
+
const loopBlock = this.createBlock();
|
|
762
|
+
const bodyBlock = this.createBlock();
|
|
763
|
+
const endBlock = this.createBlock();
|
|
764
|
+
this.currentBlock.next = loopBlock;
|
|
765
|
+
this.currentBlock = loopBlock;
|
|
766
|
+
this.visit(node.test);
|
|
767
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
768
|
+
this.currentBlock.jumpTarget = endBlock;
|
|
769
|
+
this.currentBlock.next = bodyBlock;
|
|
770
|
+
this.currentBlock = bodyBlock;
|
|
771
|
+
// Push loop context for break/continue
|
|
772
|
+
this.loopStack.push({ breakTarget: endBlock, continueTarget: loopBlock, loopType: 'while' });
|
|
773
|
+
for (const stmt of node.body) {
|
|
774
|
+
this.visit(stmt);
|
|
775
|
+
}
|
|
776
|
+
// Pop loop context
|
|
777
|
+
this.loopStack.pop();
|
|
778
|
+
// Only add jump-back if current block is reachable and doesn't already have a transfer
|
|
779
|
+
if (this.currentBlock.reachable !== false && !this.currentBlock.next && !this.currentBlock.jumpTarget) {
|
|
780
|
+
if (process.env['DEBUG_CFG']) {
|
|
781
|
+
console.log(`WHILE: Adding jump - back from block ${this.currentBlock.id} to loop block ${loopBlock.id} `);
|
|
782
|
+
}
|
|
783
|
+
this.currentBlock.next = loopBlock;
|
|
784
|
+
}
|
|
785
|
+
else if (process.env['DEBUG_CFG']) {
|
|
786
|
+
console.log(`WHILE: NOT adding jump - back from block ${this.currentBlock.id}: reachable = ${this.currentBlock.reachable}, hasNext = ${!!this.currentBlock.next}, hasJumpTarget = ${!!this.currentBlock.jumpTarget} `);
|
|
787
|
+
}
|
|
788
|
+
this.currentBlock = endBlock;
|
|
789
|
+
}
|
|
790
|
+
visitFor(node) {
|
|
791
|
+
this.visit(node.iter);
|
|
792
|
+
this.addInstruction(types_1.OpCode.GET_ITER);
|
|
793
|
+
const loopBlock = this.createBlock();
|
|
794
|
+
const bodyBlock = this.createBlock();
|
|
795
|
+
const endBlock = this.createBlock();
|
|
796
|
+
this.currentBlock.next = loopBlock;
|
|
797
|
+
this.currentBlock = loopBlock;
|
|
798
|
+
// FOR_ITER controls loop termination by jumping to its arg when the iterator is exhausted.
|
|
799
|
+
// We encode the target as a basic-block id here and let the linearizer patch it to an
|
|
800
|
+
// instruction offset.
|
|
801
|
+
this.addInstruction(types_1.OpCode.FOR_ITER, endBlock.id);
|
|
802
|
+
// Ensure the end block is included in linearization even though it isn't a normal CFG edge.
|
|
803
|
+
this.currentBlock.exceptionTarget = endBlock;
|
|
804
|
+
this.currentBlock.next = bodyBlock;
|
|
805
|
+
this.currentBlock = bodyBlock;
|
|
806
|
+
// Push loop context for break/continue
|
|
807
|
+
this.loopStack.push({ breakTarget: endBlock, continueTarget: loopBlock, loopType: 'for' });
|
|
808
|
+
this.visitTarget(node.target, 'store');
|
|
809
|
+
for (const stmt of node.body) {
|
|
810
|
+
this.visit(stmt);
|
|
811
|
+
}
|
|
812
|
+
// Pop loop context
|
|
813
|
+
this.loopStack.pop();
|
|
814
|
+
// Only add jump-back if current block is reachable and doesn't already have a transfer
|
|
815
|
+
if (this.currentBlock.reachable !== false && !this.currentBlock.next && !this.currentBlock.jumpTarget) {
|
|
816
|
+
if (process.env['DEBUG_CFG']) {
|
|
817
|
+
console.log(`FOR: Adding jump - back from block ${this.currentBlock.id} to loop block ${loopBlock.id} `);
|
|
818
|
+
}
|
|
819
|
+
this.currentBlock.next = loopBlock;
|
|
820
|
+
}
|
|
821
|
+
else if (process.env['DEBUG_CFG']) {
|
|
822
|
+
console.log(`FOR: NOT adding jump - back from block ${this.currentBlock.id}: reachable = ${this.currentBlock.reachable}, hasNext = ${!!this.currentBlock.next}, hasJumpTarget = ${!!this.currentBlock.jumpTarget} `);
|
|
823
|
+
}
|
|
824
|
+
this.currentBlock = endBlock;
|
|
825
|
+
}
|
|
826
|
+
visitFunctionDef(node) {
|
|
827
|
+
const isGenerator = this.containsYield(node.body || []);
|
|
828
|
+
const parameterNames = node.params.map((p) => {
|
|
829
|
+
if (p.type === types_1.ASTNodeType.PARAM)
|
|
830
|
+
return p.name;
|
|
831
|
+
if (p.type === types_1.ASTNodeType.VAR_ARG)
|
|
832
|
+
return p.name;
|
|
833
|
+
if (p.type === types_1.ASTNodeType.KW_ARG)
|
|
834
|
+
return p.name || '';
|
|
835
|
+
return '';
|
|
836
|
+
});
|
|
837
|
+
// Analyze function body to find all assigned variables (for local scope detection)
|
|
838
|
+
const assignedVars = this.findAssignedVariables(node.body || []);
|
|
839
|
+
const localVars = [...parameterNames, ...assignedVars];
|
|
840
|
+
// Evaluate default arguments at function definition time
|
|
841
|
+
let defaultsCount = 0;
|
|
842
|
+
for (const p of node.params) {
|
|
843
|
+
if (p.type === types_1.ASTNodeType.PARAM && p.defaultValue) {
|
|
844
|
+
this.visit(p.defaultValue);
|
|
845
|
+
defaultsCount++;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Process decorators (load them onto stack)
|
|
849
|
+
const decorators = node.decorators || [];
|
|
850
|
+
for (const decorator of decorators) {
|
|
851
|
+
this.visit(decorator);
|
|
852
|
+
}
|
|
853
|
+
const subBuilder = new CFGBuilder(node.params.length, localVars);
|
|
854
|
+
const subCfg = subBuilder.build({ type: types_1.ASTNodeType.PROGRAM, body: node.body });
|
|
855
|
+
const linearizer = new linearizer_1.Linearizer();
|
|
856
|
+
const subBytecode = linearizer.linearize(subCfg, subBuilder.getConstants(), subBuilder.getNames(), subBuilder.getVarnames(), subBuilder.getArgcount(), node.params, subBuilder.getGlobals(), subBuilder.getNonlocals());
|
|
857
|
+
subBytecode.name = node.name;
|
|
858
|
+
subBytecode.isGenerator = isGenerator;
|
|
859
|
+
if (isGenerator) {
|
|
860
|
+
subBytecode.astBody = node.body || [];
|
|
861
|
+
}
|
|
862
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(subBytecode));
|
|
863
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(node.name));
|
|
864
|
+
// Pass the number of defaults as arg to MAKE_FUNCTION
|
|
865
|
+
this.addInstruction(types_1.OpCode.MAKE_FUNCTION, defaultsCount);
|
|
866
|
+
// Apply decorators
|
|
867
|
+
for (let i = 0; i < decorators.length; i++) {
|
|
868
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 1);
|
|
869
|
+
}
|
|
870
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(node.name));
|
|
871
|
+
}
|
|
872
|
+
containsYield(body) {
|
|
873
|
+
const visitAny = (n) => {
|
|
874
|
+
if (!n)
|
|
875
|
+
return false;
|
|
876
|
+
if (Array.isArray(n)) {
|
|
877
|
+
return n.some(visitAny);
|
|
878
|
+
}
|
|
879
|
+
if (n.type === types_1.ASTNodeType.YIELD)
|
|
880
|
+
return true;
|
|
881
|
+
for (const v of Object.values(n)) {
|
|
882
|
+
if (v && typeof v === 'object') {
|
|
883
|
+
if (visitAny(v))
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return false;
|
|
888
|
+
};
|
|
889
|
+
return visitAny(body);
|
|
890
|
+
}
|
|
891
|
+
findAssignedVariables(body) {
|
|
892
|
+
const assigned = new Set();
|
|
893
|
+
const globals = new Set();
|
|
894
|
+
const nonlocals = new Set();
|
|
895
|
+
const visit = (n) => {
|
|
896
|
+
if (!n)
|
|
897
|
+
return;
|
|
898
|
+
if (Array.isArray(n)) {
|
|
899
|
+
n.forEach(visit);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
switch (n.type) {
|
|
903
|
+
case types_1.ASTNodeType.GLOBAL_STATEMENT:
|
|
904
|
+
if (n.names) {
|
|
905
|
+
n.names.forEach((name) => globals.add(name));
|
|
906
|
+
}
|
|
907
|
+
break;
|
|
908
|
+
case types_1.ASTNodeType.NONLOCAL_STATEMENT:
|
|
909
|
+
if (n.names) {
|
|
910
|
+
n.names.forEach((name) => nonlocals.add(name));
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
case types_1.ASTNodeType.ASSIGNMENT:
|
|
914
|
+
if (n.targets) {
|
|
915
|
+
n.targets.forEach((t) => this.extractAssignedNames(t, assigned));
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
case types_1.ASTNodeType.AUG_ASSIGNMENT:
|
|
919
|
+
if (n.target) {
|
|
920
|
+
this.extractAssignedNames(n.target, assigned);
|
|
921
|
+
}
|
|
922
|
+
break;
|
|
923
|
+
case types_1.ASTNodeType.FOR_STATEMENT:
|
|
924
|
+
if (n.target) {
|
|
925
|
+
if (Array.isArray(n.target)) {
|
|
926
|
+
n.target.forEach((t) => this.extractAssignedNames(t, assigned));
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
this.extractAssignedNames(n.target, assigned);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (n.body)
|
|
933
|
+
visit(n.body);
|
|
934
|
+
if (n.orelse)
|
|
935
|
+
visit(n.orelse);
|
|
936
|
+
break;
|
|
937
|
+
case types_1.ASTNodeType.FUNCTION_DEF:
|
|
938
|
+
case types_1.ASTNodeType.CLASS_DEF:
|
|
939
|
+
// Don't recurse into nested functions/classes
|
|
940
|
+
break;
|
|
941
|
+
default:
|
|
942
|
+
// Recurse into other node types
|
|
943
|
+
for (const v of Object.values(n)) {
|
|
944
|
+
if (v && typeof v === 'object') {
|
|
945
|
+
visit(v);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
visit(body);
|
|
951
|
+
// Remove variables declared as global or nonlocal from the local variables list
|
|
952
|
+
globals.forEach(name => assigned.delete(name));
|
|
953
|
+
nonlocals.forEach(name => assigned.delete(name));
|
|
954
|
+
return Array.from(assigned);
|
|
955
|
+
}
|
|
956
|
+
extractAssignedNames(target, assigned) {
|
|
957
|
+
if (!target)
|
|
958
|
+
return;
|
|
959
|
+
if (target.type === types_1.ASTNodeType.IDENTIFIER) {
|
|
960
|
+
assigned.add(target.name);
|
|
961
|
+
}
|
|
962
|
+
else if (target.type === types_1.ASTNodeType.TUPLE_LITERAL || target.type === types_1.ASTNodeType.LIST_LITERAL) {
|
|
963
|
+
if (target.elements) {
|
|
964
|
+
target.elements.forEach((el) => this.extractAssignedNames(el, assigned));
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
else if (target.type === types_1.ASTNodeType.STARRED) {
|
|
968
|
+
this.extractAssignedNames(target.target, assigned);
|
|
969
|
+
}
|
|
970
|
+
// Don't extract from subscripts or attributes - those aren't local variable assignments
|
|
971
|
+
}
|
|
972
|
+
visitLambda(node) {
|
|
973
|
+
// Parser represents lambda params as strings (e.g. ['x'] or ['*args', '**kwargs']).
|
|
974
|
+
// Normalize to the same param shape as FunctionDef so VM call binding works.
|
|
975
|
+
const rawParams = Array.isArray(node.params) ? node.params : [];
|
|
976
|
+
const params = rawParams.map((p) => {
|
|
977
|
+
if (typeof p === 'string') {
|
|
978
|
+
if (p.startsWith('**'))
|
|
979
|
+
return { type: 'KwArg', name: p.slice(2) };
|
|
980
|
+
if (p.startsWith('*'))
|
|
981
|
+
return { type: 'VarArg', name: p.slice(1) };
|
|
982
|
+
return { type: 'Param', name: p, defaultValue: null };
|
|
983
|
+
}
|
|
984
|
+
return p;
|
|
985
|
+
});
|
|
986
|
+
const parameterNames = params.map((p) => p.name);
|
|
987
|
+
const subBuilder = new CFGBuilder(params.length, parameterNames);
|
|
988
|
+
const bodyBlock = { type: types_1.ASTNodeType.RETURN_STATEMENT, value: node.body };
|
|
989
|
+
const subCfg = subBuilder.build({ type: types_1.ASTNodeType.PROGRAM, body: [bodyBlock] });
|
|
990
|
+
const linearizer = new linearizer_1.Linearizer();
|
|
991
|
+
const subBytecode = linearizer.linearize(subCfg, subBuilder.getConstants(), subBuilder.getNames(), subBuilder.getVarnames(), subBuilder.getArgcount(), params, subBuilder.getGlobals(), subBuilder.getNonlocals());
|
|
992
|
+
subBytecode.name = '<lambda>';
|
|
993
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(subBytecode));
|
|
994
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex('<lambda>'));
|
|
995
|
+
this.addInstruction(types_1.OpCode.MAKE_FUNCTION, 0); // Lambdas have no defaults
|
|
996
|
+
}
|
|
997
|
+
addUnaryOperation(operator) {
|
|
998
|
+
switch (operator) {
|
|
999
|
+
case '+':
|
|
1000
|
+
this.addInstruction(types_1.OpCode.UNARY_POSITIVE);
|
|
1001
|
+
break;
|
|
1002
|
+
case '-':
|
|
1003
|
+
this.addInstruction(types_1.OpCode.UNARY_NEGATIVE);
|
|
1004
|
+
break;
|
|
1005
|
+
case 'not':
|
|
1006
|
+
this.addInstruction(types_1.OpCode.UNARY_NOT);
|
|
1007
|
+
break;
|
|
1008
|
+
case '~':
|
|
1009
|
+
this.addInstruction(types_1.OpCode.UNARY_INVERT);
|
|
1010
|
+
break;
|
|
1011
|
+
default: throw new Error(`Unknown unary operator: ${operator} `);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
visitBoolOp(node) {
|
|
1015
|
+
// Boolean operations with short-circuit evaluation
|
|
1016
|
+
// For 'and': if first is false, skip rest
|
|
1017
|
+
// For 'or': if first is true, skip rest
|
|
1018
|
+
const isAnd = node.operator === 'and';
|
|
1019
|
+
const values = node.values || [];
|
|
1020
|
+
if (values.length === 0) {
|
|
1021
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(true));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
this.visit(values[0]);
|
|
1025
|
+
for (let i = 1; i < values.length; i++) {
|
|
1026
|
+
const endBlock = this.createBlock();
|
|
1027
|
+
if (isAnd) {
|
|
1028
|
+
// JUMP_IF_FALSE_OR_POP: if false, jump; otherwise pop and continue
|
|
1029
|
+
this.currentBlock.jumpCondition = 'if_false_or_pop';
|
|
1030
|
+
this.currentBlock.jumpTarget = endBlock;
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
// JUMP_IF_TRUE_OR_POP: if true, jump; otherwise pop and continue
|
|
1034
|
+
this.currentBlock.jumpCondition = 'if_true_or_pop';
|
|
1035
|
+
this.currentBlock.jumpTarget = endBlock;
|
|
1036
|
+
}
|
|
1037
|
+
const nextBlock = this.createBlock();
|
|
1038
|
+
this.currentBlock.next = nextBlock;
|
|
1039
|
+
this.currentBlock = nextBlock;
|
|
1040
|
+
this.visit(values[i]);
|
|
1041
|
+
this.currentBlock.next = endBlock;
|
|
1042
|
+
this.currentBlock = endBlock;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
visitIfExpression(node) {
|
|
1046
|
+
// Ternary: <body> if <test> else <orelse>
|
|
1047
|
+
// Evaluate test first
|
|
1048
|
+
this.visit(node.test);
|
|
1049
|
+
const trueBlock = this.createBlock();
|
|
1050
|
+
const falseBlock = this.createBlock();
|
|
1051
|
+
const mergeBlock = this.createBlock();
|
|
1052
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1053
|
+
this.currentBlock.jumpTarget = falseBlock;
|
|
1054
|
+
this.currentBlock.next = trueBlock;
|
|
1055
|
+
const consequent = node.consequent;
|
|
1056
|
+
const alternate = node.alternate;
|
|
1057
|
+
// True branch
|
|
1058
|
+
this.currentBlock = trueBlock;
|
|
1059
|
+
this.visit(consequent);
|
|
1060
|
+
this.currentBlock.next = mergeBlock;
|
|
1061
|
+
// False branch
|
|
1062
|
+
this.currentBlock = falseBlock;
|
|
1063
|
+
this.visit(alternate);
|
|
1064
|
+
this.currentBlock.next = mergeBlock;
|
|
1065
|
+
this.currentBlock = mergeBlock;
|
|
1066
|
+
}
|
|
1067
|
+
visitTry(node) {
|
|
1068
|
+
const handlers = (node.handlers || []);
|
|
1069
|
+
const orelse = (node.orelse || []);
|
|
1070
|
+
const finalbody = (node.finalbody || []);
|
|
1071
|
+
const hasHandlers = handlers.length > 0;
|
|
1072
|
+
const hasElse = orelse.length > 0;
|
|
1073
|
+
const hasFinally = finalbody.length > 0;
|
|
1074
|
+
const tryBlock = this.createBlock();
|
|
1075
|
+
const afterTry = this.createBlock();
|
|
1076
|
+
const exceptDispatch = hasHandlers ? this.createBlock() : null;
|
|
1077
|
+
const finallyBlock = hasFinally ? this.createBlock() : null;
|
|
1078
|
+
// Outer finally (runs on both normal and exceptional exit)
|
|
1079
|
+
if (hasFinally) {
|
|
1080
|
+
this.currentBlock.exceptionTarget = finallyBlock;
|
|
1081
|
+
this.addInstruction(types_1.OpCode.SETUP_FINALLY, finallyBlock.id);
|
|
1082
|
+
}
|
|
1083
|
+
// Inner except handler (runs when an exception is raised in try body)
|
|
1084
|
+
if (hasHandlers) {
|
|
1085
|
+
// Chain exceptionTarget so the linearizer includes both handler blocks
|
|
1086
|
+
this.currentBlock.exceptionTarget = exceptDispatch;
|
|
1087
|
+
if (hasFinally) {
|
|
1088
|
+
exceptDispatch.exceptionTarget = finallyBlock;
|
|
1089
|
+
}
|
|
1090
|
+
this.addInstruction(types_1.OpCode.SETUP_FINALLY, exceptDispatch.id);
|
|
1091
|
+
}
|
|
1092
|
+
this.currentBlock.next = tryBlock;
|
|
1093
|
+
// Try block (normal flow)
|
|
1094
|
+
this.currentBlock = tryBlock;
|
|
1095
|
+
for (const stmt of node.body || []) {
|
|
1096
|
+
this.visit(stmt);
|
|
1097
|
+
}
|
|
1098
|
+
if (hasHandlers) {
|
|
1099
|
+
// Pop inner except handler
|
|
1100
|
+
this.addInstruction(types_1.OpCode.POP_BLOCK);
|
|
1101
|
+
}
|
|
1102
|
+
if (hasElse) {
|
|
1103
|
+
for (const stmt of orelse) {
|
|
1104
|
+
this.visit(stmt);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (hasFinally) {
|
|
1108
|
+
// Pop outer finally handler, then run finally with a null marker
|
|
1109
|
+
this.addInstruction(types_1.OpCode.POP_BLOCK);
|
|
1110
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
1111
|
+
this.currentBlock.jumpCondition = 'always';
|
|
1112
|
+
this.currentBlock.jumpTarget = finallyBlock;
|
|
1113
|
+
delete this.currentBlock.next;
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
this.currentBlock.next = afterTry;
|
|
1117
|
+
}
|
|
1118
|
+
// Except dispatch block (entered with exception object on stack)
|
|
1119
|
+
if (hasHandlers) {
|
|
1120
|
+
this.currentBlock = exceptDispatch;
|
|
1121
|
+
// Build a chain of handler checks.
|
|
1122
|
+
const noMatchBlock = this.createBlock();
|
|
1123
|
+
let cursor = exceptDispatch;
|
|
1124
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
1125
|
+
const handler = handlers[i];
|
|
1126
|
+
const isLast = i === handlers.length - 1;
|
|
1127
|
+
const nextCheck = isLast ? noMatchBlock : this.createBlock();
|
|
1128
|
+
const handlerBlock = this.createBlock();
|
|
1129
|
+
this.currentBlock = cursor;
|
|
1130
|
+
if (handler.exceptionType) {
|
|
1131
|
+
// Stack at entry: [exc]
|
|
1132
|
+
// Compute isinstance(exc, ExceptionType) leaving exc on stack.
|
|
1133
|
+
this.addInstruction(types_1.OpCode.DUP_TOP);
|
|
1134
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex('isinstance'));
|
|
1135
|
+
this.addInstruction(types_1.OpCode.ROT_TWO);
|
|
1136
|
+
this.visit(handler.exceptionType);
|
|
1137
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 2);
|
|
1138
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1139
|
+
this.currentBlock.jumpTarget = nextCheck;
|
|
1140
|
+
this.currentBlock.next = handlerBlock;
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
// Bare except: always matches
|
|
1144
|
+
this.currentBlock.next = handlerBlock;
|
|
1145
|
+
// Bare except must be last in CPython; treat it as terminal.
|
|
1146
|
+
}
|
|
1147
|
+
// Handler body block
|
|
1148
|
+
this.currentBlock = handlerBlock;
|
|
1149
|
+
if (handler.name) {
|
|
1150
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(handler.name));
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1154
|
+
}
|
|
1155
|
+
for (const stmt of handler.body || []) {
|
|
1156
|
+
this.visit(stmt);
|
|
1157
|
+
}
|
|
1158
|
+
if (hasFinally) {
|
|
1159
|
+
// Pop outer finally handler then run finally with null marker
|
|
1160
|
+
this.addInstruction(types_1.OpCode.POP_BLOCK);
|
|
1161
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
1162
|
+
this.currentBlock.jumpCondition = 'always';
|
|
1163
|
+
this.currentBlock.jumpTarget = finallyBlock;
|
|
1164
|
+
delete this.currentBlock.next;
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
this.currentBlock.next = afterTry;
|
|
1168
|
+
}
|
|
1169
|
+
// Continue chain with the next check block (exception is still on stack)
|
|
1170
|
+
cursor = nextCheck;
|
|
1171
|
+
// If this was a bare except, we are done.
|
|
1172
|
+
if (!handler.exceptionType) {
|
|
1173
|
+
cursor = noMatchBlock;
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
// No handler matched -> re-raise the exception on stack.
|
|
1178
|
+
this.currentBlock = noMatchBlock;
|
|
1179
|
+
this.addInstruction(types_1.OpCode.RAISE_VARARGS, 1);
|
|
1180
|
+
}
|
|
1181
|
+
// Finally block (entered with marker on stack: null for normal flow, or exception object)
|
|
1182
|
+
if (hasFinally) {
|
|
1183
|
+
this.currentBlock = finallyBlock;
|
|
1184
|
+
const reRaiseBlock = this.createBlock();
|
|
1185
|
+
const normalFinally = this.createBlock();
|
|
1186
|
+
// Determine whether marker is null
|
|
1187
|
+
this.addInstruction(types_1.OpCode.DUP_TOP);
|
|
1188
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null));
|
|
1189
|
+
this.addInstruction(types_1.OpCode.COMPARE_OP, types_1.CompareOp.IS);
|
|
1190
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1191
|
+
this.currentBlock.jumpTarget = reRaiseBlock;
|
|
1192
|
+
this.currentBlock.next = normalFinally;
|
|
1193
|
+
// Normal path: pop null marker
|
|
1194
|
+
this.currentBlock = normalFinally;
|
|
1195
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1196
|
+
for (const stmt of finalbody) {
|
|
1197
|
+
this.visit(stmt);
|
|
1198
|
+
}
|
|
1199
|
+
this.currentBlock.next = afterTry;
|
|
1200
|
+
// Exceptional path: keep exception marker and re-raise after finalbody
|
|
1201
|
+
this.currentBlock = reRaiseBlock;
|
|
1202
|
+
for (const stmt of finalbody) {
|
|
1203
|
+
this.visit(stmt);
|
|
1204
|
+
}
|
|
1205
|
+
this.addInstruction(types_1.OpCode.RAISE_VARARGS, 1);
|
|
1206
|
+
}
|
|
1207
|
+
this.currentBlock = afterTry;
|
|
1208
|
+
}
|
|
1209
|
+
visitWith(node) {
|
|
1210
|
+
const items = node.items || [];
|
|
1211
|
+
const body = node.body || [];
|
|
1212
|
+
this.processWithItems(items, 0, body);
|
|
1213
|
+
}
|
|
1214
|
+
processWithItems(items, index, body) {
|
|
1215
|
+
if (index >= items.length) {
|
|
1216
|
+
// All context managers entered, process body
|
|
1217
|
+
for (const stmt of body) {
|
|
1218
|
+
this.visit(stmt);
|
|
1219
|
+
}
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const item = items[index];
|
|
1223
|
+
const contextExpr = item.context;
|
|
1224
|
+
const optionalVars = item.target;
|
|
1225
|
+
// Evaluate context expression
|
|
1226
|
+
this.visit(contextExpr);
|
|
1227
|
+
// Setup block for cleanup
|
|
1228
|
+
const cleanupBlock = this.createBlock();
|
|
1229
|
+
const bodyBlock = this.createBlock();
|
|
1230
|
+
this.currentBlock.exceptionTarget = cleanupBlock;
|
|
1231
|
+
this.addInstruction(types_1.OpCode.SETUP_WITH, cleanupBlock.id);
|
|
1232
|
+
this.currentBlock.next = bodyBlock;
|
|
1233
|
+
this.currentBlock = bodyBlock;
|
|
1234
|
+
// Store result if target exists
|
|
1235
|
+
// SETUP_WITH pushes exit, result.
|
|
1236
|
+
// If optionalVars, store result. Else POP_TOP result.
|
|
1237
|
+
if (optionalVars) {
|
|
1238
|
+
this.visitTarget(optionalVars, 'store');
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1242
|
+
}
|
|
1243
|
+
// Recursively process next item or body
|
|
1244
|
+
this.processWithItems(items, index + 1, body);
|
|
1245
|
+
// Normal exit from body
|
|
1246
|
+
// Pop block (SETUP_WITH)
|
|
1247
|
+
this.addInstruction(types_1.OpCode.POP_BLOCK);
|
|
1248
|
+
// Call __exit__(None, None, None)
|
|
1249
|
+
// Stack has: [__exit__] (after POP_BLOCK removed the block frame, but __exit__ remains on stack from setup)
|
|
1250
|
+
// Actually SETUP_WITH block logic in VM: we decided stackHeight includes exit.
|
|
1251
|
+
// So POP_BLOCK pops the metadata, but `exit` remains on stack.
|
|
1252
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(null)); // None
|
|
1253
|
+
this.addInstruction(types_1.OpCode.DUP_TOP); // None, None
|
|
1254
|
+
this.addInstruction(types_1.OpCode.DUP_TOP); // None, None, None
|
|
1255
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 3); // exit(None, None, None)
|
|
1256
|
+
this.addInstruction(types_1.OpCode.POP_TOP); // Discard result of exit()
|
|
1257
|
+
const afterWithBlock = this.createBlock();
|
|
1258
|
+
this.currentBlock.next = afterWithBlock;
|
|
1259
|
+
// Cleanup block (exception handling)
|
|
1260
|
+
this.currentBlock = cleanupBlock;
|
|
1261
|
+
// Stack has: [exit, exc_norm] (pushed by dispatchException)
|
|
1262
|
+
this.addInstruction(types_1.OpCode.WITH_EXCEPT_START); // Calls exit(exc), returns handled flag
|
|
1263
|
+
// If handled (True), suppress exception and continue.
|
|
1264
|
+
// If not handled (False), re-raise.
|
|
1265
|
+
// WITH_EXCEPT_START in my VM implementation throws if not handled.
|
|
1266
|
+
// So if we are here, it was handled.
|
|
1267
|
+
// We probably need to pop the exception if handled?
|
|
1268
|
+
// My VM implementation for `WITH_EXCEPT_START`:
|
|
1269
|
+
// If handled, it returns. The exception is GONE from stack (popped inside opcode).
|
|
1270
|
+
// Wait, checked my VM implementation:
|
|
1271
|
+
// `const exc = frame.stack.pop(); const exit = frame.stack.pop(); `
|
|
1272
|
+
// So stack is empty of exc/exit.
|
|
1273
|
+
this.currentBlock.next = afterWithBlock;
|
|
1274
|
+
this.currentBlock = afterWithBlock;
|
|
1275
|
+
}
|
|
1276
|
+
visitMatch(node) {
|
|
1277
|
+
// match subject:
|
|
1278
|
+
// case pattern: body
|
|
1279
|
+
const subject = node.subject;
|
|
1280
|
+
this.visit(subject);
|
|
1281
|
+
const endBlock = this.createBlock();
|
|
1282
|
+
for (const kase of node.cases) {
|
|
1283
|
+
const pattern = kase.pattern;
|
|
1284
|
+
const body = kase.body;
|
|
1285
|
+
const guard = kase.guard;
|
|
1286
|
+
const nextCase = this.createBlock();
|
|
1287
|
+
const bodyBlock = this.createBlock();
|
|
1288
|
+
// Duplicate subject for pattern matching (keep original on stack for next cases)
|
|
1289
|
+
this.addInstruction(types_1.OpCode.DUP_TOP);
|
|
1290
|
+
this.compilePattern(pattern, nextCase, bodyBlock);
|
|
1291
|
+
// Compile the body block
|
|
1292
|
+
this.currentBlock = bodyBlock;
|
|
1293
|
+
// Handle guard if present
|
|
1294
|
+
if (guard) {
|
|
1295
|
+
const guardFailBlock = this.createBlock();
|
|
1296
|
+
this.visit(guard);
|
|
1297
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1298
|
+
this.currentBlock.jumpTarget = guardFailBlock;
|
|
1299
|
+
const guardPassBlock = this.createBlock();
|
|
1300
|
+
this.currentBlock.next = guardPassBlock;
|
|
1301
|
+
this.currentBlock = guardPassBlock;
|
|
1302
|
+
// Pop subject since we matched and guard passed
|
|
1303
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1304
|
+
// Execute body
|
|
1305
|
+
for (const stmt of body) {
|
|
1306
|
+
this.visit(stmt);
|
|
1307
|
+
}
|
|
1308
|
+
this.currentBlock.next = endBlock;
|
|
1309
|
+
// Guard failed - go to next case
|
|
1310
|
+
this.currentBlock = guardFailBlock;
|
|
1311
|
+
this.currentBlock.next = nextCase;
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
// No guard - pop subject and execute body directly
|
|
1315
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1316
|
+
for (const stmt of body) {
|
|
1317
|
+
this.visit(stmt);
|
|
1318
|
+
}
|
|
1319
|
+
this.currentBlock.next = endBlock;
|
|
1320
|
+
}
|
|
1321
|
+
// Move to next case block
|
|
1322
|
+
this.currentBlock = nextCase;
|
|
1323
|
+
}
|
|
1324
|
+
// Pop subject if no case matched
|
|
1325
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1326
|
+
this.currentBlock.next = endBlock;
|
|
1327
|
+
this.currentBlock = endBlock;
|
|
1328
|
+
}
|
|
1329
|
+
// Helper to compile basic patterns
|
|
1330
|
+
compilePattern(pattern, failBlock, successBlock) {
|
|
1331
|
+
// This is complex to implement fully without specialized opcodes.
|
|
1332
|
+
// We'll implement a fallback for the specific test case:
|
|
1333
|
+
// 0 | 1
|
|
1334
|
+
// [a, b]
|
|
1335
|
+
// _ (wildcard)
|
|
1336
|
+
// Contract for compilePattern:
|
|
1337
|
+
// Input: Stack has [..., subject_copy]
|
|
1338
|
+
// Success: Stack has [...] (subject_copy consumed), jump to successBlock
|
|
1339
|
+
// Failure: Stack has [...] (subject_copy consumed), jump to failBlock
|
|
1340
|
+
// Note: POP_JUMP_IF_FALSE pops the condition value automatically
|
|
1341
|
+
switch (pattern.type) {
|
|
1342
|
+
case types_1.ASTNodeType.MATCH_PATTERN_VALUE: {
|
|
1343
|
+
// Stack: [..., subject_copy]
|
|
1344
|
+
this.visit(pattern.value); // Stack: [..., subject_copy, value]
|
|
1345
|
+
this.addInstruction(types_1.OpCode.COMPARE_OP, types_1.CompareOp.EQ); // Stack: [..., bool]
|
|
1346
|
+
// POP_JUMP_IF_FALSE will pop the bool and jump if false
|
|
1347
|
+
// So after the jump instruction, stack is [...] in both paths
|
|
1348
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1349
|
+
this.currentBlock.jumpTarget = failBlock;
|
|
1350
|
+
this.currentBlock.next = successBlock;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
case types_1.ASTNodeType.MATCH_PATTERN_OR: {
|
|
1354
|
+
// multiple patterns. If any matches -> success.
|
|
1355
|
+
// Stack: [..., subject_copy]
|
|
1356
|
+
for (let i = 0; i < pattern.patterns.length; i++) {
|
|
1357
|
+
const isLast = i === pattern.patterns.length - 1;
|
|
1358
|
+
const nextP = isLast ? failBlock : this.createBlock();
|
|
1359
|
+
if (!isLast) {
|
|
1360
|
+
// Duplicate subject for this pattern (in case it fails)
|
|
1361
|
+
this.addInstruction(types_1.OpCode.DUP_TOP); // Stack: [..., subject_copy, subject_copy2]
|
|
1362
|
+
}
|
|
1363
|
+
// compilePattern consumes one copy
|
|
1364
|
+
this.compilePattern(pattern.patterns[i], nextP, successBlock);
|
|
1365
|
+
if (!isLast) {
|
|
1366
|
+
this.currentBlock = nextP;
|
|
1367
|
+
// On failure, subject_copy2 was consumed, but subject_copy is still there
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case types_1.ASTNodeType.MATCH_PATTERN_SEQUENCE: {
|
|
1373
|
+
// Stack: [..., subject_copy]
|
|
1374
|
+
// Check if list/tuple/sequence, check length, unpack
|
|
1375
|
+
// Check isinstance(subject, list)
|
|
1376
|
+
this.addInstruction(types_1.OpCode.DUP_TOP); // [..., subject_copy, subject_copy2]
|
|
1377
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex('isinstance'));
|
|
1378
|
+
this.addInstruction(types_1.OpCode.ROT_TWO); // [..., subject_copy, isinstance, subject_copy2]
|
|
1379
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex('list'));
|
|
1380
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 2); // [..., subject_copy, bool]
|
|
1381
|
+
// POP_JUMP_IF_FALSE will pop the bool
|
|
1382
|
+
const notListBlock = this.createBlock();
|
|
1383
|
+
const isListBlock = this.createBlock();
|
|
1384
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1385
|
+
this.currentBlock.jumpTarget = notListBlock;
|
|
1386
|
+
this.currentBlock.next = isListBlock;
|
|
1387
|
+
// Not a list - just pop subject_copy and fail
|
|
1388
|
+
// (bool was already popped by POP_JUMP_IF_FALSE)
|
|
1389
|
+
this.currentBlock = notListBlock;
|
|
1390
|
+
this.addInstruction(types_1.OpCode.POP_TOP); // pop subject_copy
|
|
1391
|
+
this.currentBlock.next = failBlock;
|
|
1392
|
+
// Is a list - check length
|
|
1393
|
+
// (bool was already popped by POP_JUMP_IF_FALSE)
|
|
1394
|
+
// Stack: [..., subject_copy]
|
|
1395
|
+
this.currentBlock = isListBlock;
|
|
1396
|
+
// Check length
|
|
1397
|
+
this.addInstruction(types_1.OpCode.DUP_TOP); // [..., subject_copy, subject_copy2]
|
|
1398
|
+
this.addInstruction(types_1.OpCode.LOAD_NAME, this.getNameIndex('len'));
|
|
1399
|
+
this.addInstruction(types_1.OpCode.ROT_TWO);
|
|
1400
|
+
this.addInstruction(types_1.OpCode.CALL_FUNCTION, 1); // [..., subject_copy, length]
|
|
1401
|
+
this.addInstruction(types_1.OpCode.LOAD_CONST, this.getConstantIndex(pattern.elements.length));
|
|
1402
|
+
this.addInstruction(types_1.OpCode.COMPARE_OP, types_1.CompareOp.EQ); // [..., subject_copy, bool]
|
|
1403
|
+
const lenNoMatchBlock = this.createBlock();
|
|
1404
|
+
const lenMatchBlock = this.createBlock();
|
|
1405
|
+
this.currentBlock.jumpCondition = 'if_false';
|
|
1406
|
+
this.currentBlock.jumpTarget = lenNoMatchBlock;
|
|
1407
|
+
this.currentBlock.next = lenMatchBlock;
|
|
1408
|
+
// Length doesn't match - pop subject and fail
|
|
1409
|
+
// (bool was already popped by POP_JUMP_IF_FALSE)
|
|
1410
|
+
this.currentBlock = lenNoMatchBlock;
|
|
1411
|
+
this.addInstruction(types_1.OpCode.POP_TOP); // pop subject_copy
|
|
1412
|
+
this.currentBlock.next = failBlock;
|
|
1413
|
+
// Length matches - unpack and bind
|
|
1414
|
+
// (bool was already popped by POP_JUMP_IF_FALSE)
|
|
1415
|
+
// Stack: [..., subject_copy]
|
|
1416
|
+
this.currentBlock = lenMatchBlock;
|
|
1417
|
+
// UNPACK_SEQUENCE consumes subject and pushes elements
|
|
1418
|
+
this.addInstruction(types_1.OpCode.UNPACK_SEQUENCE, pattern.elements.length);
|
|
1419
|
+
// Stack: [..., elem0, elem1, ...]
|
|
1420
|
+
// Store elements into pattern variables
|
|
1421
|
+
for (let i = 0; i < pattern.elements.length; i++) {
|
|
1422
|
+
const p = pattern.elements[i];
|
|
1423
|
+
if (p.type === types_1.ASTNodeType.MATCH_PATTERN_CAPTURE && p.name) {
|
|
1424
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(p.name));
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
// Stack: [...]
|
|
1431
|
+
this.currentBlock.next = successBlock;
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
case types_1.ASTNodeType.MATCH_PATTERN_CAPTURE:
|
|
1435
|
+
// Stack: [..., subject_copy]
|
|
1436
|
+
if (pattern.name) {
|
|
1437
|
+
// Bind subject to name
|
|
1438
|
+
this.addInstruction(types_1.OpCode.STORE_NAME, this.getNameIndex(pattern.name));
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
// Wildcard - just consume
|
|
1442
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1443
|
+
}
|
|
1444
|
+
// Stack: [...]
|
|
1445
|
+
this.currentBlock.next = successBlock;
|
|
1446
|
+
break;
|
|
1447
|
+
case types_1.ASTNodeType.MATCH_PATTERN_WILDCARD:
|
|
1448
|
+
// Stack: [..., subject_copy]
|
|
1449
|
+
// Wildcard - just consume
|
|
1450
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1451
|
+
// Stack: [...]
|
|
1452
|
+
this.currentBlock.next = successBlock;
|
|
1453
|
+
break;
|
|
1454
|
+
default:
|
|
1455
|
+
// Unknown pattern -> treat as match, consume subject_copy
|
|
1456
|
+
this.addInstruction(types_1.OpCode.POP_TOP);
|
|
1457
|
+
this.currentBlock.next = successBlock;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
exports.CFGBuilder = CFGBuilder;
|
|
1462
|
+
//# sourceMappingURL=cfg_builder.js.map
|