stone-lang 0.1.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/README.md +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- package/packages/semver.js +635 -0
|
@@ -0,0 +1,1727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stone Engine Service
|
|
3
|
+
*
|
|
4
|
+
* Central service for Stone language parsing, graph building, and execution.
|
|
5
|
+
* Provides a unified API for all Stone engine operations with caching and storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Lexer } from './frontend/parsing/tokenizer.js';
|
|
9
|
+
import { Parser } from './frontend/parsing/astBuilder.js';
|
|
10
|
+
import { TypeChecker } from './frontend/type-checker/typeChecker.js';
|
|
11
|
+
import { ModuleResolver } from './frontend/utils/moduleResolver.js';
|
|
12
|
+
import { buildFilePath } from '../utils/filePathResolver.js';
|
|
13
|
+
import { Compiler } from './backends/js-vm/compiler.js';
|
|
14
|
+
import { CompiledFunction } from './backends/js-vm/bytecode.js';
|
|
15
|
+
import { VM } from './backends/js-vm/vm/index.js';
|
|
16
|
+
import { BufferedOutputAdapter } from './adapters/OutputAdapter.js';
|
|
17
|
+
import { createTerminalConstructors } from './frontend/parsing/terminal-registry.js';
|
|
18
|
+
import { setFileSystemAdapter } from './backends/js-vm/primitives/file.js';
|
|
19
|
+
import { registerOverload, clearDynamicOverloads } from './backends/js-vm/primitives/overloads.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stone Engine Service
|
|
23
|
+
*
|
|
24
|
+
* Central service class for Stone language parsing, type checking, compilation,
|
|
25
|
+
* and execution. Provides a unified API for all Stone engine operations with
|
|
26
|
+
* built-in caching and storage capabilities.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Basic usage
|
|
30
|
+
* const service = new StoneEngineService();
|
|
31
|
+
* const result = service.compileAndExecute('let x = 2 + 3; print(x)');
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // With modules
|
|
35
|
+
* service.setFilesSource(filesMap);
|
|
36
|
+
* const result = await service.executeWithModules(source, { currentPath: '/scripts/main.stn' });
|
|
37
|
+
*/
|
|
38
|
+
class StoneEngineService {
|
|
39
|
+
/**
|
|
40
|
+
* Create a new StoneEngineService instance
|
|
41
|
+
*/
|
|
42
|
+
constructor() {
|
|
43
|
+
/**
|
|
44
|
+
* Map of currently running scripts (fileId -> execution state)
|
|
45
|
+
* @type {Map<string, {status: string, startTime: number, fileId: string, source: string}>}
|
|
46
|
+
*/
|
|
47
|
+
this.runningScripts = new Map();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map of execution results with metadata (fileId -> result)
|
|
51
|
+
* @type {Map<string, Object>}
|
|
52
|
+
*/
|
|
53
|
+
this.executionResults = new Map();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* AST cache (sourceHash -> AST)
|
|
57
|
+
* @type {Map<string, Object>}
|
|
58
|
+
*/
|
|
59
|
+
this.astCache = new Map();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execution graph cache (sourceHash+nodeInsertsHash -> graph)
|
|
63
|
+
* @type {Map<string, Object>}
|
|
64
|
+
*/
|
|
65
|
+
this.graphCache = new Map();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Maximum number of entries in caches
|
|
69
|
+
* @type {number}
|
|
70
|
+
*/
|
|
71
|
+
this.maxCacheSize = 100;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Execution history per file (fileId -> array of executions)
|
|
75
|
+
* @type {Map<string, Array>}
|
|
76
|
+
*/
|
|
77
|
+
this.executionHistory = new Map();
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Maximum number of history entries per file
|
|
81
|
+
* @type {number}
|
|
82
|
+
*/
|
|
83
|
+
this.maxHistoryPerFile = 10;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Module resolver for handling imports and module loading
|
|
87
|
+
* @type {ModuleResolver}
|
|
88
|
+
*/
|
|
89
|
+
this.moduleResolver = new ModuleResolver();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* File system state (set by consumer)
|
|
93
|
+
* @type {Map|Array|null}
|
|
94
|
+
*/
|
|
95
|
+
this.filesSource = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Function to load file content by ID (set by consumer)
|
|
99
|
+
* @type {Function|null}
|
|
100
|
+
*/
|
|
101
|
+
this.fileLoader = null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* File system adapter for file module (set by consumer)
|
|
105
|
+
* Must have readFile(path) and writeFile(path, content) methods
|
|
106
|
+
* @type {Object|null}
|
|
107
|
+
*/
|
|
108
|
+
this.fileSystemAdapter = null;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Cached builtins.stn exports (auto-imported into every script)
|
|
112
|
+
* @type {Object|null}
|
|
113
|
+
*/
|
|
114
|
+
this.stnBuiltinsExports = null;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Bytecode compiler instance
|
|
118
|
+
* @type {Compiler}
|
|
119
|
+
*/
|
|
120
|
+
this.compiler = new Compiler();
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bytecode VM instance
|
|
124
|
+
* @type {VM}
|
|
125
|
+
*/
|
|
126
|
+
this.vm = new VM();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Bytecode cache (sourceHash -> compiled bytecode)
|
|
130
|
+
* @type {Map<string, Object>}
|
|
131
|
+
*/
|
|
132
|
+
this.bytecodeCache = new Map();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Tag a CompiledFunction and all nested functions with moduleIdx
|
|
137
|
+
* @param {CompiledFunction} fn - Function to tag
|
|
138
|
+
* @param {number} moduleIdx - Module index to assign
|
|
139
|
+
* @param {Set} visited - Already visited functions (to avoid cycles)
|
|
140
|
+
*/
|
|
141
|
+
_tagFunctionWithModuleIdx(fn, moduleIdx, visited = new Set()) {
|
|
142
|
+
if (!fn || !(fn instanceof CompiledFunction) || visited.has(fn)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
visited.add(fn);
|
|
146
|
+
fn.moduleIdx = moduleIdx;
|
|
147
|
+
|
|
148
|
+
// Recursively tag any nested functions in constants
|
|
149
|
+
if (fn.constants) {
|
|
150
|
+
for (const constant of fn.constants) {
|
|
151
|
+
if (constant instanceof CompiledFunction) {
|
|
152
|
+
this._tagFunctionWithModuleIdx(constant, moduleIdx, visited);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Load builtins.stn exports (auto-imported primitives)
|
|
160
|
+
* Cached after first load
|
|
161
|
+
*/
|
|
162
|
+
async loadStnBuiltins() {
|
|
163
|
+
if (this.stnBuiltinsExports) {
|
|
164
|
+
return this.stnBuiltinsExports;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Load builtins.stn as a module
|
|
169
|
+
await this.moduleResolver.loadModule(
|
|
170
|
+
'builtins',
|
|
171
|
+
(source, opts) => this.parse(source, opts)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Execute builtins.stn to get exports
|
|
175
|
+
const exports = await this.executeModule('builtins', {});
|
|
176
|
+
this.stnBuiltinsExports = exports || {};
|
|
177
|
+
|
|
178
|
+
console.log('📦 Loaded builtins.stn exports:', Object.keys(this.stnBuiltinsExports));
|
|
179
|
+
return this.stnBuiltinsExports;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Failed to load builtins.stn:', error);
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ========================================================================
|
|
187
|
+
// Parser API
|
|
188
|
+
// ========================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parse Stone source code into an Abstract Syntax Tree (AST)
|
|
192
|
+
*
|
|
193
|
+
* @param {string} source - Stone source code to parse
|
|
194
|
+
* @param {Object} [options={}] - Parse options
|
|
195
|
+
* @param {string} [options.filename='<stdin>'] - Filename for error messages
|
|
196
|
+
* @returns {Object} Parsed AST (Program node)
|
|
197
|
+
* @throws {Error} If parsing fails due to syntax errors
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* const ast = service.parse('fn add(a, b) = a + b');
|
|
201
|
+
* // ast.type === 'Program'
|
|
202
|
+
* // ast.statements contains the function definition
|
|
203
|
+
*/
|
|
204
|
+
parse(source, options = {}) {
|
|
205
|
+
const { filename = '<stdin>' } = options;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const lexer = new Lexer(source, filename);
|
|
209
|
+
const tokens = lexer.tokenize();
|
|
210
|
+
const parser = new Parser(tokens, filename);
|
|
211
|
+
return parser.parse();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw new Error(`Parse error: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Tokenize source code
|
|
219
|
+
* @param {string} source - Stone source code
|
|
220
|
+
* @param {Object} options - Lexer options
|
|
221
|
+
* @returns {Array} Token array
|
|
222
|
+
*/
|
|
223
|
+
tokenize(source, options = {}) {
|
|
224
|
+
const { filename = '<stdin>' } = options;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const lexer = new Lexer(source, filename);
|
|
228
|
+
return lexer.tokenize();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw new Error(`Tokenize error: ${error.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Validate syntax without building full AST
|
|
236
|
+
* @param {string} source - Stone source code
|
|
237
|
+
* @returns {Object} { valid: boolean, errors: Array }
|
|
238
|
+
*/
|
|
239
|
+
validateSyntax(source) {
|
|
240
|
+
try {
|
|
241
|
+
this.parse(source);
|
|
242
|
+
return { valid: true, errors: [] };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return {
|
|
245
|
+
valid: false,
|
|
246
|
+
errors: [{ message: error.message, type: 'syntax' }]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Precheck Stone source code for syntax, type errors, AND imported modules
|
|
253
|
+
* Does not execute - only validates
|
|
254
|
+
* @param {string} source - Stone source code
|
|
255
|
+
* @param {Object} options - Options { filename }
|
|
256
|
+
* @returns {Promise<Object>} { valid: boolean, errors: [], warnings: [] }
|
|
257
|
+
*/
|
|
258
|
+
async precheck(source, options = {}) {
|
|
259
|
+
const { filename = '<stdin>' } = options;
|
|
260
|
+
const errors = [];
|
|
261
|
+
const warnings = [];
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// 1. Parse (syntax check)
|
|
265
|
+
const ast = this.parse(source, { filename });
|
|
266
|
+
|
|
267
|
+
// 2. Load and validate imported modules (if any)
|
|
268
|
+
if (ast.imports && ast.imports.length > 0) {
|
|
269
|
+
const moduleErrors = await this.precheckModules(ast, filename);
|
|
270
|
+
errors.push(...moduleErrors);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 3. Type check main file
|
|
274
|
+
const typeChecker = new TypeChecker({
|
|
275
|
+
moduleResolver: this.moduleResolver,
|
|
276
|
+
parser: (src, opts) => this.parse(src, opts),
|
|
277
|
+
});
|
|
278
|
+
const typeResult = typeChecker.check(ast);
|
|
279
|
+
|
|
280
|
+
if (!typeResult.success) {
|
|
281
|
+
// Preserve stage info from errors (already set in typeChecker)
|
|
282
|
+
errors.push(...typeResult.errors.map(err => ({
|
|
283
|
+
stage: err.stage || 'type_check',
|
|
284
|
+
type: err.type || 'type',
|
|
285
|
+
message: err.message,
|
|
286
|
+
location: err.location || null
|
|
287
|
+
})));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (typeResult.warnings) {
|
|
291
|
+
warnings.push(...typeResult.warnings);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
valid: errors.length === 0,
|
|
296
|
+
errors,
|
|
297
|
+
warnings
|
|
298
|
+
};
|
|
299
|
+
} catch (parseError) {
|
|
300
|
+
// Parse failed - preserve stage info if available
|
|
301
|
+
errors.push({
|
|
302
|
+
stage: parseError.stage || 'parse',
|
|
303
|
+
type: parseError.type || 'syntax',
|
|
304
|
+
message: parseError.message,
|
|
305
|
+
location: parseError.location || null
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
valid: false,
|
|
310
|
+
errors,
|
|
311
|
+
warnings
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Load and type-check all imported modules (without executing)
|
|
318
|
+
* @param {Object} ast - Parsed AST with imports
|
|
319
|
+
* @param {string} currentPath - Current file path for resolution
|
|
320
|
+
* @returns {Promise<Array>} Array of error objects
|
|
321
|
+
*/
|
|
322
|
+
async precheckModules(ast, currentPath = null) {
|
|
323
|
+
const errors = [];
|
|
324
|
+
const visited = new Set();
|
|
325
|
+
|
|
326
|
+
if (currentPath) {
|
|
327
|
+
this.moduleResolver.setCurrentFilePath(currentPath);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const importNode of ast.imports || []) {
|
|
331
|
+
const modulePath = importNode.modulePath;
|
|
332
|
+
|
|
333
|
+
// Skip builtins and already visited
|
|
334
|
+
if (visited.has(modulePath)) continue;
|
|
335
|
+
if (this.moduleResolver.isBuiltinModule(modulePath)) continue;
|
|
336
|
+
|
|
337
|
+
visited.add(modulePath);
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
// Load module AST (parse only, no execution)
|
|
341
|
+
const moduleData = await this.moduleResolver.loadModule(
|
|
342
|
+
modulePath,
|
|
343
|
+
(source, opts) => this.parse(source, opts)
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (moduleData.ast) {
|
|
347
|
+
// Recursively check module's imports
|
|
348
|
+
const nestedErrors = await this.precheckModules(moduleData.ast, modulePath);
|
|
349
|
+
errors.push(...nestedErrors);
|
|
350
|
+
|
|
351
|
+
// Type check the module
|
|
352
|
+
const typeChecker = new TypeChecker({
|
|
353
|
+
moduleResolver: this.moduleResolver,
|
|
354
|
+
parser: (src, opts) => this.parse(src, opts),
|
|
355
|
+
});
|
|
356
|
+
const typeResult = typeChecker.check(moduleData.ast);
|
|
357
|
+
|
|
358
|
+
if (!typeResult.success) {
|
|
359
|
+
errors.push(...typeResult.errors.map(err => ({
|
|
360
|
+
stage: err.stage || 'type_check',
|
|
361
|
+
type: err.type || 'type',
|
|
362
|
+
module: modulePath,
|
|
363
|
+
message: err.message,
|
|
364
|
+
location: err.location || null
|
|
365
|
+
})));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (loadError) {
|
|
369
|
+
errors.push({
|
|
370
|
+
stage: loadError.stage || 'module_load',
|
|
371
|
+
type: loadError.type || 'module',
|
|
372
|
+
module: modulePath,
|
|
373
|
+
message: `Failed to load module '${modulePath}': ${loadError.message}`,
|
|
374
|
+
location: importNode.location || null
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return errors;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ========================================================================
|
|
383
|
+
// Graph Building API
|
|
384
|
+
// ========================================================================
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Build execution graph from source code
|
|
388
|
+
* @param {string} source - Stone source code
|
|
389
|
+
* @param {Object} nodeInserts - Node insert definitions
|
|
390
|
+
* @param {Object} options - Build options
|
|
391
|
+
* @returns {Object} Execution graph
|
|
392
|
+
*/
|
|
393
|
+
buildGraph(source, nodeInserts, options = {}) {
|
|
394
|
+
try {
|
|
395
|
+
// Parse source to AST
|
|
396
|
+
const ast = this.parse(source, options);
|
|
397
|
+
|
|
398
|
+
// Build graph from AST
|
|
399
|
+
return this.buildGraphFromAst(ast, nodeInserts, options);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
throw new Error(`Build graph error: ${error.message}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Build execution graph from AST
|
|
407
|
+
* @param {Object} ast - Abstract Syntax Tree
|
|
408
|
+
* @param {Object} nodeInserts - Node insert definitions
|
|
409
|
+
* @param {Object} options - Build options
|
|
410
|
+
* @returns {Object} Execution graph
|
|
411
|
+
*/
|
|
412
|
+
buildGraphFromAst(ast, nodeInserts, options = {}) {
|
|
413
|
+
try {
|
|
414
|
+
// Build graph without caching to avoid circular reference issues
|
|
415
|
+
const graph = buildGraphFromAst(ast, nodeInserts);
|
|
416
|
+
return graph;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
throw new Error(`Build graph from AST error: ${error.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Build layout graph for visualization
|
|
424
|
+
* @param {Array} nodes - Graph nodes
|
|
425
|
+
* @returns {Object} Layout graph
|
|
426
|
+
*/
|
|
427
|
+
buildLayoutGraph(nodes) {
|
|
428
|
+
try {
|
|
429
|
+
return buildLayoutGraph(nodes);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
throw new Error(`Build layout graph error: ${error.message}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Calculate layout for nodes
|
|
437
|
+
* @param {Array} nodes - Graph nodes
|
|
438
|
+
* @param {Object} config - Layout configuration
|
|
439
|
+
* @returns {Array} Nodes with layout positions
|
|
440
|
+
*/
|
|
441
|
+
calculateLayout(nodes, config = {}) {
|
|
442
|
+
try {
|
|
443
|
+
const layoutGraph = this.buildLayoutGraph(nodes);
|
|
444
|
+
// Layout calculation would happen here
|
|
445
|
+
// For now, return the layout graph
|
|
446
|
+
return layoutGraph;
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw new Error(`Calculate layout error: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ========================================================================
|
|
453
|
+
// Execution API
|
|
454
|
+
// ========================================================================
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Execute a Stone script with full pipeline (type check, compile, run)
|
|
458
|
+
*
|
|
459
|
+
* This is the primary execution method that handles the complete execution flow:
|
|
460
|
+
* 1. Loads module dependencies if any imports exist
|
|
461
|
+
* 2. Performs type checking
|
|
462
|
+
* 3. Compiles to bytecode
|
|
463
|
+
* 4. Executes in the VM
|
|
464
|
+
*
|
|
465
|
+
* @param {string} fileId - Unique identifier for the file/script
|
|
466
|
+
* @param {Object} scriptGraph - Script graph containing AST or source reference
|
|
467
|
+
* @param {Object} [scriptGraph.ast] - Pre-parsed AST (optional)
|
|
468
|
+
* @param {Object} inputs - Input variables to inject into execution context
|
|
469
|
+
* @param {Object} [options={}] - Execution options
|
|
470
|
+
* @param {string} [options.source='unknown'] - Source identifier for tracking
|
|
471
|
+
* @param {string} [options.sourceCode] - Source code if AST not provided
|
|
472
|
+
* @param {string} [options.currentPath] - Current file path for module resolution
|
|
473
|
+
* @param {boolean} [options.debug=false] - Enable debug output
|
|
474
|
+
* @param {number} [options.maxIterations=100000] - Maximum loop iterations
|
|
475
|
+
* @returns {Promise<Object>} Execution result with output, variables, and metadata
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* const result = await service.execute('script1', { ast }, { x: 10 });
|
|
479
|
+
* if (result.status === 'completed') {
|
|
480
|
+
* console.log(result.output);
|
|
481
|
+
* }
|
|
482
|
+
*/
|
|
483
|
+
async execute(fileId, scriptGraph, inputs, options = {}) {
|
|
484
|
+
const { source = 'unknown', sourceCode } = options;
|
|
485
|
+
const startTime = Date.now();
|
|
486
|
+
|
|
487
|
+
// Set up file system adapter for file module (if available)
|
|
488
|
+
if (this.fileSystemAdapter) {
|
|
489
|
+
setFileSystemAdapter(this.fileSystemAdapter);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
this.runningScripts.set(fileId, {
|
|
494
|
+
status: 'running',
|
|
495
|
+
startTime,
|
|
496
|
+
fileId,
|
|
497
|
+
source
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
let result;
|
|
501
|
+
|
|
502
|
+
const executionOptions = {
|
|
503
|
+
...options,
|
|
504
|
+
variables: inputs || {},
|
|
505
|
+
debug: options.debug || false,
|
|
506
|
+
maxIterations: options.maxIterations || 100000,
|
|
507
|
+
moduleResolver: this.moduleResolver,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
let ast;
|
|
511
|
+
if (scriptGraph.ast) {
|
|
512
|
+
ast = scriptGraph.ast;
|
|
513
|
+
} else if (sourceCode) {
|
|
514
|
+
ast = this.parse(sourceCode, { filename: fileId });
|
|
515
|
+
} else {
|
|
516
|
+
throw new Error('No AST or source code provided for execution');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Clear dynamic overloads from previous executions
|
|
520
|
+
clearDynamicOverloads();
|
|
521
|
+
|
|
522
|
+
if (ast.imports && ast.imports.length > 0) {
|
|
523
|
+
this.clearModuleCache();
|
|
524
|
+
if (options.currentPath) {
|
|
525
|
+
this.moduleResolver.setCurrentFilePath(options.currentPath);
|
|
526
|
+
}
|
|
527
|
+
await this.loadModuleDependencies(ast, options.currentPath);
|
|
528
|
+
for (const importNode of ast.imports) {
|
|
529
|
+
const modulePath = importNode.modulePath;
|
|
530
|
+
if (!this.moduleResolver.isBuiltinModule(modulePath)) {
|
|
531
|
+
await this.executeModule(modulePath, executionOptions);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Type check pass - resolves overloads and validates types before execution
|
|
537
|
+
// Pass moduleResolver and parser for static type analysis of imports
|
|
538
|
+
const typeChecker = new TypeChecker({
|
|
539
|
+
moduleResolver: this.moduleResolver,
|
|
540
|
+
parser: (source, opts) => this.parse(source, opts),
|
|
541
|
+
});
|
|
542
|
+
const typeResult = typeChecker.check(ast);
|
|
543
|
+
|
|
544
|
+
if (!typeResult.success) {
|
|
545
|
+
// Type errors found - return early with error info for UI to display
|
|
546
|
+
const endTime = Date.now();
|
|
547
|
+
|
|
548
|
+
// Format errors with stage info for terminal display
|
|
549
|
+
const typeErrors = typeResult.errors.map(err => {
|
|
550
|
+
const stage = err.stage ? `[${err.stage}] ` : '';
|
|
551
|
+
const loc = err.location
|
|
552
|
+
? `[${err.location.line}:${err.location.column}] `
|
|
553
|
+
: '';
|
|
554
|
+
return `${stage}${loc}${err.message}`;
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Create terminal data with type error messages for Stone console display
|
|
558
|
+
const errorTerminalsData = {
|
|
559
|
+
main: {
|
|
560
|
+
type: 'console',
|
|
561
|
+
config: { title: 'Main', max_lines: 200 },
|
|
562
|
+
lines: ['TYPE ERROR', ...typeErrors]
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const executionResult = {
|
|
567
|
+
fileId,
|
|
568
|
+
source,
|
|
569
|
+
output: typeErrors,
|
|
570
|
+
result: null,
|
|
571
|
+
variables: {},
|
|
572
|
+
terminalsData: errorTerminalsData,
|
|
573
|
+
startTime,
|
|
574
|
+
endTime,
|
|
575
|
+
duration: endTime - startTime,
|
|
576
|
+
status: 'error',
|
|
577
|
+
error: {
|
|
578
|
+
message: 'Type check failed',
|
|
579
|
+
errors: typeResult.errors, // Preserve full error objects with stage
|
|
580
|
+
stage: 'type_check'
|
|
581
|
+
},
|
|
582
|
+
metadata: {
|
|
583
|
+
requestSource: source,
|
|
584
|
+
inputs,
|
|
585
|
+
timestamp: new Date().toISOString(),
|
|
586
|
+
executionMode: 'vm'
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
this.executionResults.set(fileId, executionResult);
|
|
591
|
+
this.runningScripts.delete(fileId);
|
|
592
|
+
return executionResult;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Load builtins (auto-imported functions like sin, cos, etc.)
|
|
596
|
+
const builtinsExports = await this.loadStnBuiltins();
|
|
597
|
+
const combinedVariables = { ...builtinsExports, ...(inputs || {}) };
|
|
598
|
+
|
|
599
|
+
// Execute using bytecode VM (unified execution path)
|
|
600
|
+
const vmResult = await this.executeWithVM(ast, {
|
|
601
|
+
...executionOptions,
|
|
602
|
+
externalVars: combinedVariables,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Build terminalsData: use adapter's terminalsData, and add main console if there's print output
|
|
606
|
+
let terminalsData = vmResult.terminalsData || {};
|
|
607
|
+
if (vmResult.output?.length > 0 && !terminalsData.main) {
|
|
608
|
+
terminalsData.main = {
|
|
609
|
+
type: 'console',
|
|
610
|
+
config: { title: 'Main', max_lines: 200 },
|
|
611
|
+
lines: vmResult.output
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
result = {
|
|
616
|
+
success: vmResult.success,
|
|
617
|
+
result: vmResult.result,
|
|
618
|
+
output: vmResult.output,
|
|
619
|
+
error: vmResult.error,
|
|
620
|
+
variables: {},
|
|
621
|
+
terminalsData,
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const endTime = Date.now();
|
|
625
|
+
const output = result.output || result.result;
|
|
626
|
+
|
|
627
|
+
// Create result with metadata
|
|
628
|
+
const executionResult = {
|
|
629
|
+
fileId,
|
|
630
|
+
source,
|
|
631
|
+
output,
|
|
632
|
+
result: result.result,
|
|
633
|
+
variables: result.variables || {},
|
|
634
|
+
terminalsData: result.terminalsData || {},
|
|
635
|
+
startTime,
|
|
636
|
+
endTime,
|
|
637
|
+
duration: endTime - startTime,
|
|
638
|
+
status: result.success ? 'completed' : 'error',
|
|
639
|
+
error: result.error || null,
|
|
640
|
+
metadata: {
|
|
641
|
+
requestSource: source,
|
|
642
|
+
inputs,
|
|
643
|
+
timestamp: new Date().toISOString(),
|
|
644
|
+
executionMode: 'vm'
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Store result
|
|
649
|
+
this.executionResults.set(fileId, executionResult);
|
|
650
|
+
this.runningScripts.delete(fileId);
|
|
651
|
+
|
|
652
|
+
// Add to history
|
|
653
|
+
this._addToHistory(fileId, executionResult);
|
|
654
|
+
|
|
655
|
+
return executionResult;
|
|
656
|
+
} catch (error) {
|
|
657
|
+
const endTime = Date.now();
|
|
658
|
+
|
|
659
|
+
// Preserve stage info if available, otherwise infer from context
|
|
660
|
+
const stage = error.stage || 'parse';
|
|
661
|
+
const errorModule = error.module || null;
|
|
662
|
+
|
|
663
|
+
// Format error message with stage info for terminal
|
|
664
|
+
const errorMsg = `[${stage}]${errorModule ? ` in ${errorModule}:` : ''} ${error.message}`;
|
|
665
|
+
|
|
666
|
+
// Create terminal data with error message (errors here are typically parse/module errors before executor runs)
|
|
667
|
+
const errorTerminalsData = {
|
|
668
|
+
main: {
|
|
669
|
+
type: 'console',
|
|
670
|
+
config: { title: 'Main', max_lines: 200 },
|
|
671
|
+
lines: [errorMsg]
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// Create error result
|
|
676
|
+
const result = {
|
|
677
|
+
fileId,
|
|
678
|
+
source,
|
|
679
|
+
output: null,
|
|
680
|
+
terminalsData: errorTerminalsData,
|
|
681
|
+
startTime,
|
|
682
|
+
endTime,
|
|
683
|
+
duration: endTime - startTime,
|
|
684
|
+
status: 'error',
|
|
685
|
+
error: {
|
|
686
|
+
message: error.message,
|
|
687
|
+
stage: stage,
|
|
688
|
+
module: errorModule,
|
|
689
|
+
location: error.location || null,
|
|
690
|
+
stack: error.stack
|
|
691
|
+
},
|
|
692
|
+
metadata: {
|
|
693
|
+
requestSource: source,
|
|
694
|
+
inputs,
|
|
695
|
+
timestamp: new Date().toISOString(),
|
|
696
|
+
executionMode: 'vm'
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// Store error result
|
|
701
|
+
this.executionResults.set(fileId, result);
|
|
702
|
+
this.runningScripts.delete(fileId);
|
|
703
|
+
|
|
704
|
+
// Return result instead of throwing so terminal output is visible
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Execute with callback for progress updates
|
|
711
|
+
* @param {string} fileId - File identifier
|
|
712
|
+
* @param {Object} scriptGraph - Execution graph
|
|
713
|
+
* @param {Array} inputs - Input values
|
|
714
|
+
* @param {Object} nodeInserts - Node insert definitions
|
|
715
|
+
* @param {Object} callbacks - Callback functions
|
|
716
|
+
* @returns {Promise<Object>} Execution result
|
|
717
|
+
*/
|
|
718
|
+
async executeWithCallback(fileId, scriptGraph, inputs, nodeInserts, callbacks = {}) {
|
|
719
|
+
// TODO: Implement callback-based execution for progress tracking
|
|
720
|
+
return this.execute(fileId, scriptGraph, inputs, nodeInserts);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Execute using bytecode VM
|
|
725
|
+
* Compiles AST to bytecode and runs in the VM for faster execution
|
|
726
|
+
* @param {Object} ast - Parsed AST
|
|
727
|
+
* @param {Object} options - Execution options
|
|
728
|
+
* @param {Object} options.externalVars - External variables to inject
|
|
729
|
+
* @param {OutputAdapter} options.outputAdapter - Output adapter
|
|
730
|
+
* @returns {Promise<Object>} Execution result
|
|
731
|
+
*/
|
|
732
|
+
async executeWithVM(ast, options = {}) {
|
|
733
|
+
const startTime = performance.now();
|
|
734
|
+
const adapter = options.outputAdapter || new BufferedOutputAdapter();
|
|
735
|
+
const externalVars = { ...(options.externalVars || {}) };
|
|
736
|
+
|
|
737
|
+
// Pre-scan for namespace imports to add them to externalVars
|
|
738
|
+
// This ensures the compiler knows about these variables
|
|
739
|
+
const namespaceImports = [];
|
|
740
|
+
for (const stmt of ast.statements || []) {
|
|
741
|
+
if (stmt.type === 'NamespaceImportStatement') {
|
|
742
|
+
// Use alias if provided, otherwise use the module name (last segment of path)
|
|
743
|
+
const alias = stmt.alias || stmt.modulePath.split('/').pop();
|
|
744
|
+
namespaceImports.push({
|
|
745
|
+
modulePath: stmt.modulePath,
|
|
746
|
+
alias
|
|
747
|
+
});
|
|
748
|
+
// Pre-register with null, will be populated after module loading
|
|
749
|
+
externalVars[alias] = null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
// Get external variable names for compiler
|
|
755
|
+
const externalVarNames = Object.keys(externalVars);
|
|
756
|
+
|
|
757
|
+
// Create a new compiler instance with external var names
|
|
758
|
+
const compiler = new Compiler({
|
|
759
|
+
externalVarNames
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Compile AST to bytecode
|
|
763
|
+
const compiled = compiler.compile(ast);
|
|
764
|
+
|
|
765
|
+
if (!compiled.success) {
|
|
766
|
+
const errorMsg = compiled.errors.map(e => e.message).join('\n');
|
|
767
|
+
adapter.error(errorMsg, null);
|
|
768
|
+
adapter.finish(false, null);
|
|
769
|
+
|
|
770
|
+
// Include error in output and terminalsData for display
|
|
771
|
+
return {
|
|
772
|
+
success: false,
|
|
773
|
+
error: errorMsg,
|
|
774
|
+
output: [`Error: ${errorMsg}`],
|
|
775
|
+
result: null,
|
|
776
|
+
terminalsData: {
|
|
777
|
+
main: {
|
|
778
|
+
type: 'console',
|
|
779
|
+
config: { title: 'Main', max_lines: 200 },
|
|
780
|
+
lines: [`Error: ${errorMsg}`],
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
compilationTime: performance.now() - startTime,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const compileTime = performance.now() - startTime;
|
|
788
|
+
|
|
789
|
+
// Reset VM modules before loading (indices are relative to each execution)
|
|
790
|
+
this.vm.modules = [];
|
|
791
|
+
this.vm.moduleLocals = [];
|
|
792
|
+
|
|
793
|
+
// Load imported modules into VM before execution
|
|
794
|
+
// The compiler tracks imports in compiled.imports: [{ module: string, names: string[] }]
|
|
795
|
+
if (compiled.imports && compiled.imports.length > 0) {
|
|
796
|
+
for (const importInfo of compiled.imports) {
|
|
797
|
+
const modulePath = importInfo.module;
|
|
798
|
+
const importedNames = importInfo.names;
|
|
799
|
+
|
|
800
|
+
// Get module exports (from cache or builtin)
|
|
801
|
+
let moduleExports;
|
|
802
|
+
|
|
803
|
+
// Special handling for 'plots' module - needs runtime terminal constructors
|
|
804
|
+
if (modulePath === 'plots') {
|
|
805
|
+
// Create terminal constructors with the current adapter
|
|
806
|
+
const terminalConstructors = createTerminalConstructors({
|
|
807
|
+
outputAdapter: adapter,
|
|
808
|
+
terminalCounter: this.terminalCounter || 0
|
|
809
|
+
});
|
|
810
|
+
moduleExports = {
|
|
811
|
+
graph2d: terminalConstructors.graph2d,
|
|
812
|
+
graph3d: terminalConstructors.graph3d,
|
|
813
|
+
console_terminal: terminalConstructors.console_terminal
|
|
814
|
+
};
|
|
815
|
+
} else if (this.moduleResolver.isBuiltinModule(modulePath)) {
|
|
816
|
+
moduleExports = this.moduleResolver.getBuiltinModule(modulePath);
|
|
817
|
+
} else {
|
|
818
|
+
const cachedModule = this.moduleResolver.cache.get(modulePath);
|
|
819
|
+
moduleExports = cachedModule?.exports;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!moduleExports) {
|
|
823
|
+
throw new Error(`Module '${modulePath}' not found or not loaded`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check if this is a namespace import (import module as alias)
|
|
827
|
+
if (importedNames.length === 1 && importedNames[0] === '*') {
|
|
828
|
+
// Namespace import - create namespace object with all exports
|
|
829
|
+
const namespaceObj = { ...moduleExports };
|
|
830
|
+
const exportsMap = new Map();
|
|
831
|
+
exportsMap.set('*', { slot: 0 });
|
|
832
|
+
|
|
833
|
+
// Store namespace object in moduleLocals for IMPORT_GET
|
|
834
|
+
const moduleStorage = [namespaceObj];
|
|
835
|
+
|
|
836
|
+
// Tag any CompiledFunctions in the namespace
|
|
837
|
+
const moduleIdx = this.vm.modules.length;
|
|
838
|
+
for (const value of Object.values(namespaceObj)) {
|
|
839
|
+
if (value instanceof CompiledFunction) {
|
|
840
|
+
this._tagFunctionWithModuleIdx(value, moduleIdx);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
this.vm.modules.push({
|
|
845
|
+
id: modulePath,
|
|
846
|
+
exports: exportsMap,
|
|
847
|
+
initializer: null,
|
|
848
|
+
});
|
|
849
|
+
this.vm.moduleLocals.push(moduleStorage);
|
|
850
|
+
|
|
851
|
+
// Update externalVars with the actual namespace object
|
|
852
|
+
// The namespace was pre-registered as null, now populate it
|
|
853
|
+
const alias = importInfo.alias || modulePath.split('/').pop();
|
|
854
|
+
if (alias in externalVars) {
|
|
855
|
+
externalVars[alias] = namespaceObj;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Create a module structure for the VM
|
|
862
|
+
// The VM expects: { id, exports: Map<name, {slot}>, initializer: null }
|
|
863
|
+
const exportsMap = new Map();
|
|
864
|
+
const moduleIdx = this.vm.modules.length;
|
|
865
|
+
|
|
866
|
+
// Get cached module data for proper upvalue resolution
|
|
867
|
+
const cachedModule = this.moduleResolver.cache.get(modulePath);
|
|
868
|
+
const allLocals = cachedModule?.allLocals || [];
|
|
869
|
+
const exportSlots = cachedModule?.exportSlots || {};
|
|
870
|
+
const compiledExports = cachedModule?.compiledExports || new Map();
|
|
871
|
+
|
|
872
|
+
// Use allLocals for moduleStorage to preserve original slot positions for upvalues
|
|
873
|
+
const moduleStorage = allLocals.length > 0 ? [...allLocals] : [];
|
|
874
|
+
|
|
875
|
+
// Selective imports (namespace imports handled above with continue)
|
|
876
|
+
for (let i = 0; i < importedNames.length; i++) {
|
|
877
|
+
const name = importedNames[i];
|
|
878
|
+
const value = moduleExports[name];
|
|
879
|
+
if (value === undefined && !(name in moduleExports)) {
|
|
880
|
+
throw new Error(`'${name}' is not exported from module '${modulePath}'`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Use full export info from compiledExports if available (preserves overload info)
|
|
884
|
+
const fullExportInfo = compiledExports.get(name);
|
|
885
|
+
if (fullExportInfo) {
|
|
886
|
+
exportsMap.set(name, fullExportInfo);
|
|
887
|
+
} else {
|
|
888
|
+
// Fall back to cached export slot if available, otherwise use sequential slot
|
|
889
|
+
const originalSlot = exportSlots[name] !== undefined ? exportSlots[name] : i;
|
|
890
|
+
exportsMap.set(name, { slot: originalSlot });
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// If no allLocals, store at sequential slot
|
|
894
|
+
if (allLocals.length === 0) {
|
|
895
|
+
moduleStorage[i] = value;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Tag CompiledFunctions with moduleIdx (including nested functions in constants)
|
|
899
|
+
if (value instanceof CompiledFunction) {
|
|
900
|
+
this._tagFunctionWithModuleIdx(value, moduleIdx);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Tag all functions in allLocals with moduleIdx
|
|
905
|
+
for (const value of moduleStorage) {
|
|
906
|
+
if (value instanceof CompiledFunction) {
|
|
907
|
+
this._tagFunctionWithModuleIdx(value, moduleIdx);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Load into VM
|
|
912
|
+
this.vm.modules.push({
|
|
913
|
+
id: modulePath,
|
|
914
|
+
exports: exportsMap,
|
|
915
|
+
initializer: null,
|
|
916
|
+
});
|
|
917
|
+
this.vm.moduleLocals.push(moduleStorage);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Execute bytecode with adapter and external variables
|
|
922
|
+
const execStart = performance.now();
|
|
923
|
+
const result = await this.vm.execute(compiled, {
|
|
924
|
+
outputAdapter: adapter,
|
|
925
|
+
externalVars
|
|
926
|
+
});
|
|
927
|
+
const execTime = performance.now() - execStart;
|
|
928
|
+
|
|
929
|
+
// Capture all locals for module caching (needed for closure upvalues)
|
|
930
|
+
const localCount = compiled.main?.localCount || 0;
|
|
931
|
+
const allLocals = [];
|
|
932
|
+
for (let i = 0; i < localCount; i++) {
|
|
933
|
+
allLocals[i] = this.vm.locals[i];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Get output from adapter (print output goes to adapter, not vmResult.output)
|
|
937
|
+
const adapterOutput = adapter.getOutput();
|
|
938
|
+
|
|
939
|
+
// If there's an error, ensure it's added to the main console terminal
|
|
940
|
+
if (!result.success && result.error) {
|
|
941
|
+
// Ensure terminalsData exists
|
|
942
|
+
if (!adapterOutput.terminalsData) {
|
|
943
|
+
adapterOutput.terminalsData = {};
|
|
944
|
+
}
|
|
945
|
+
// Ensure main console exists
|
|
946
|
+
if (!adapterOutput.terminalsData.main) {
|
|
947
|
+
adapterOutput.terminalsData.main = {
|
|
948
|
+
type: 'console',
|
|
949
|
+
config: { title: 'Main', max_lines: 200 },
|
|
950
|
+
lines: []
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
// Add error to main console lines
|
|
954
|
+
const errorLine = `Error: ${result.error}`;
|
|
955
|
+
if (!adapterOutput.terminalsData.main.lines) {
|
|
956
|
+
adapterOutput.terminalsData.main.lines = [];
|
|
957
|
+
}
|
|
958
|
+
// Only add if not already present (avoid duplicates)
|
|
959
|
+
if (!adapterOutput.terminalsData.main.lines.includes(errorLine)) {
|
|
960
|
+
adapterOutput.terminalsData.main.lines.push(errorLine);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Debug logging for terminal data
|
|
965
|
+
console.log('[StoneEngineService.executeWithVM] terminalsData keys:', Object.keys(adapterOutput.terminalsData || {}));
|
|
966
|
+
if (adapterOutput.terminalsData) {
|
|
967
|
+
for (const [termId, termData] of Object.entries(adapterOutput.terminalsData)) {
|
|
968
|
+
const firstPlot = termData.plots?.[0];
|
|
969
|
+
console.log(`[StoneEngineService.executeWithVM] Terminal ${termId}:`, {
|
|
970
|
+
type: termData.type,
|
|
971
|
+
hasPlots: !!termData.plots,
|
|
972
|
+
plotsLength: termData.plots?.length,
|
|
973
|
+
firstPlot: firstPlot ? {
|
|
974
|
+
type: firstPlot.type,
|
|
975
|
+
label: firstPlot.label,
|
|
976
|
+
x: firstPlot.x,
|
|
977
|
+
xType: firstPlot.x?.constructor?.name,
|
|
978
|
+
xSize: firstPlot.x?.size,
|
|
979
|
+
xLength: firstPlot.x?.length,
|
|
980
|
+
xData: firstPlot.x?.data,
|
|
981
|
+
y: firstPlot.y,
|
|
982
|
+
yType: firstPlot.y?.constructor?.name,
|
|
983
|
+
ySize: firstPlot.y?.size,
|
|
984
|
+
yLength: firstPlot.y?.length,
|
|
985
|
+
yData: firstPlot.y?.data,
|
|
986
|
+
} : null
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
success: result.success,
|
|
993
|
+
result: result.result,
|
|
994
|
+
output: adapterOutput.output,
|
|
995
|
+
terminalsData: adapterOutput.terminalsData,
|
|
996
|
+
error: result.error,
|
|
997
|
+
exports: result.exports, // Pass through exports from VM
|
|
998
|
+
exportSlots: result.exportSlots, // Original slot positions for exports
|
|
999
|
+
compiledExports: compiled.exports, // Full export info including overload data
|
|
1000
|
+
allLocals, // All locals for module caching
|
|
1001
|
+
compilationTime: compileTime,
|
|
1002
|
+
executionTime: execTime,
|
|
1003
|
+
totalTime: compileTime + execTime,
|
|
1004
|
+
};
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
adapter.error(e.message, null);
|
|
1007
|
+
adapter.finish(false, e);
|
|
1008
|
+
|
|
1009
|
+
// Include error in output and terminalsData for display
|
|
1010
|
+
return {
|
|
1011
|
+
success: false,
|
|
1012
|
+
error: e.message,
|
|
1013
|
+
output: [`Error: ${e.message}`],
|
|
1014
|
+
result: null,
|
|
1015
|
+
terminalsData: {
|
|
1016
|
+
main: {
|
|
1017
|
+
type: 'console',
|
|
1018
|
+
config: { title: 'Main', max_lines: 200 },
|
|
1019
|
+
lines: [`Error: ${e.message}`],
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
totalTime: performance.now() - startTime,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Compile and execute source code using VM
|
|
1029
|
+
* Includes parsing, type checking, compilation, and execution
|
|
1030
|
+
* @param {string} source - Stone source code
|
|
1031
|
+
* @param {Object} options - Options
|
|
1032
|
+
* @returns {Promise<Object>} Execution result with timing info
|
|
1033
|
+
*/
|
|
1034
|
+
async compileAndExecute(source, options = {}) {
|
|
1035
|
+
const { filename = '<stdin>' } = options;
|
|
1036
|
+
const startTime = performance.now();
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
// Parse
|
|
1040
|
+
const parseStart = performance.now();
|
|
1041
|
+
const ast = this.parse(source, { filename });
|
|
1042
|
+
const parseTime = performance.now() - parseStart;
|
|
1043
|
+
|
|
1044
|
+
// Type check
|
|
1045
|
+
const typeStart = performance.now();
|
|
1046
|
+
const typeChecker = new TypeChecker({ moduleResolver: this.moduleResolver });
|
|
1047
|
+
const typeResult = typeChecker.check(ast);
|
|
1048
|
+
const typeTime = performance.now() - typeStart;
|
|
1049
|
+
|
|
1050
|
+
if (!typeResult.success) {
|
|
1051
|
+
return {
|
|
1052
|
+
success: false,
|
|
1053
|
+
error: 'Type check failed: ' + typeResult.errors.map(e => e.message).join('; '),
|
|
1054
|
+
parseTime,
|
|
1055
|
+
typeCheckTime: typeTime,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Compile and execute
|
|
1060
|
+
const vmResult = await this.executeWithVM(ast, options);
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
...vmResult,
|
|
1064
|
+
parseTime,
|
|
1065
|
+
typeCheckTime: typeTime,
|
|
1066
|
+
};
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
return {
|
|
1069
|
+
success: false,
|
|
1070
|
+
error: e.message,
|
|
1071
|
+
totalTime: performance.now() - startTime,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Pause script execution
|
|
1078
|
+
* @param {string} fileId - File identifier
|
|
1079
|
+
*/
|
|
1080
|
+
pause(fileId) {
|
|
1081
|
+
const state = this.runningScripts.get(fileId);
|
|
1082
|
+
if (state) {
|
|
1083
|
+
state.status = 'paused';
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Resume script execution
|
|
1089
|
+
* @param {string} fileId - File identifier
|
|
1090
|
+
*/
|
|
1091
|
+
resume(fileId) {
|
|
1092
|
+
const state = this.runningScripts.get(fileId);
|
|
1093
|
+
if (state && state.status === 'paused') {
|
|
1094
|
+
state.status = 'running';
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Stop script execution
|
|
1100
|
+
* @param {string} fileId - File identifier
|
|
1101
|
+
*/
|
|
1102
|
+
stop(fileId) {
|
|
1103
|
+
this.runningScripts.delete(fileId);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// ========================================================================
|
|
1107
|
+
// Storage API
|
|
1108
|
+
// ========================================================================
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Store execution result
|
|
1112
|
+
* @param {string} fileId - File identifier
|
|
1113
|
+
* @param {string} source - Source code
|
|
1114
|
+
* @param {Object} result - Execution result
|
|
1115
|
+
* @param {Object} metadata - Additional metadata
|
|
1116
|
+
*/
|
|
1117
|
+
storeExecutionResult(fileId, source, result, metadata = {}) {
|
|
1118
|
+
const executionData = {
|
|
1119
|
+
fileId,
|
|
1120
|
+
source,
|
|
1121
|
+
result,
|
|
1122
|
+
metadata: {
|
|
1123
|
+
...metadata,
|
|
1124
|
+
executedAt: new Date().toISOString()
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
this.executionResults.set(fileId, executionData);
|
|
1129
|
+
this._addToHistory(fileId, executionData);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Get execution history for a file
|
|
1134
|
+
* @param {string} fileId - File identifier
|
|
1135
|
+
* @param {number} limit - Maximum number of results
|
|
1136
|
+
* @returns {Array} Execution history
|
|
1137
|
+
*/
|
|
1138
|
+
getExecutionHistory(fileId, limit = 10) {
|
|
1139
|
+
const history = this.executionHistory.get(fileId) || [];
|
|
1140
|
+
return history.slice(0, limit);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Get stored source code
|
|
1145
|
+
* @param {string} fileId - File identifier
|
|
1146
|
+
* @returns {string|null} Source code
|
|
1147
|
+
*/
|
|
1148
|
+
getStoredSource(fileId) {
|
|
1149
|
+
const result = this.executionResults.get(fileId);
|
|
1150
|
+
return result?.source || null;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Get stored result
|
|
1155
|
+
* @param {string} fileId - File identifier
|
|
1156
|
+
* @returns {Object|null} Execution result
|
|
1157
|
+
*/
|
|
1158
|
+
getStoredResult(fileId) {
|
|
1159
|
+
return this.executionResults.get(fileId) || null;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Clear stored data for a file
|
|
1164
|
+
* @param {string} fileId - File identifier
|
|
1165
|
+
*/
|
|
1166
|
+
clearStoredData(fileId) {
|
|
1167
|
+
this.executionResults.delete(fileId);
|
|
1168
|
+
this.executionHistory.delete(fileId);
|
|
1169
|
+
this.runningScripts.delete(fileId);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* List all stored executions
|
|
1174
|
+
* @returns {Array} Array of file IDs with stored data
|
|
1175
|
+
*/
|
|
1176
|
+
listStoredExecutions() {
|
|
1177
|
+
return Array.from(this.executionResults.keys());
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// ========================================================================
|
|
1181
|
+
// Caching API
|
|
1182
|
+
// ========================================================================
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Get cached AST
|
|
1186
|
+
* @param {string} sourceHash - Source code hash
|
|
1187
|
+
* @returns {Object|null} Cached AST
|
|
1188
|
+
*/
|
|
1189
|
+
getCachedAst(sourceHash) {
|
|
1190
|
+
return this.astCache.get(sourceHash) || null;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Set cached AST
|
|
1195
|
+
* @param {string} sourceHash - Source code hash
|
|
1196
|
+
* @param {Object} ast - AST to cache
|
|
1197
|
+
*/
|
|
1198
|
+
setCachedAst(sourceHash, ast) {
|
|
1199
|
+
this._manageCacheSize(this.astCache);
|
|
1200
|
+
this.astCache.set(sourceHash, ast);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Get cached graph
|
|
1205
|
+
* @param {string} sourceHash - Source code hash
|
|
1206
|
+
* @param {string} nodeInsertsHash - Node inserts hash
|
|
1207
|
+
* @returns {Object|null} Cached graph
|
|
1208
|
+
*/
|
|
1209
|
+
getCachedGraph(sourceHash, nodeInsertsHash) {
|
|
1210
|
+
const cacheKey = `${sourceHash}_${nodeInsertsHash}`;
|
|
1211
|
+
return this.graphCache.get(cacheKey) || null;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Set cached graph
|
|
1216
|
+
* @param {string} sourceHash - Source code hash
|
|
1217
|
+
* @param {string} nodeInsertsHash - Node inserts hash
|
|
1218
|
+
* @param {Object} graph - Graph to cache
|
|
1219
|
+
*/
|
|
1220
|
+
setCachedGraph(sourceHash, nodeInsertsHash, graph) {
|
|
1221
|
+
this._manageCacheSize(this.graphCache);
|
|
1222
|
+
const cacheKey = `${sourceHash}_${nodeInsertsHash}`;
|
|
1223
|
+
this.graphCache.set(cacheKey, graph);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Clear all caches
|
|
1228
|
+
*/
|
|
1229
|
+
clearCache() {
|
|
1230
|
+
this.astCache.clear();
|
|
1231
|
+
this.graphCache.clear();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// ========================================================================
|
|
1235
|
+
// Utility API
|
|
1236
|
+
// ========================================================================
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Get execution state for a file
|
|
1240
|
+
* @param {string} fileId - File identifier
|
|
1241
|
+
* @returns {Object|null} Execution state
|
|
1242
|
+
*/
|
|
1243
|
+
getExecutionState(fileId) {
|
|
1244
|
+
return this.runningScripts.get(fileId) || null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Reset service state
|
|
1249
|
+
*/
|
|
1250
|
+
reset() {
|
|
1251
|
+
this.runningScripts.clear();
|
|
1252
|
+
this.executionResults.clear();
|
|
1253
|
+
this.executionHistory.clear();
|
|
1254
|
+
this.clearCache();
|
|
1255
|
+
this.clearModuleCache();
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Get all errors from recent executions
|
|
1260
|
+
* @returns {Array} Array of errors
|
|
1261
|
+
*/
|
|
1262
|
+
getErrors() {
|
|
1263
|
+
const errors = [];
|
|
1264
|
+
for (const [fileId, result] of this.executionResults.entries()) {
|
|
1265
|
+
if (result.status === 'error' && result.error) {
|
|
1266
|
+
errors.push({
|
|
1267
|
+
fileId,
|
|
1268
|
+
error: result.error,
|
|
1269
|
+
timestamp: result.metadata?.timestamp
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return errors;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Validate graph structure
|
|
1278
|
+
* @param {Object} graph - Graph to validate
|
|
1279
|
+
* @returns {Object} { valid: boolean, errors: Array }
|
|
1280
|
+
*/
|
|
1281
|
+
validateGraph(graph) {
|
|
1282
|
+
const errors = [];
|
|
1283
|
+
|
|
1284
|
+
if (!graph || !graph.nodes) {
|
|
1285
|
+
errors.push({ message: 'Graph must have nodes array', type: 'structure' });
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (!graph.edges) {
|
|
1289
|
+
errors.push({ message: 'Graph must have edges array', type: 'structure' });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Additional validation logic here
|
|
1293
|
+
|
|
1294
|
+
return {
|
|
1295
|
+
valid: errors.length === 0,
|
|
1296
|
+
errors
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ========================================================================
|
|
1301
|
+
// Module System API
|
|
1302
|
+
// ========================================================================
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Configure the file system source for module resolution
|
|
1306
|
+
* @param {Map|Array} filesSource - Map or Array of file objects from FileSystemContext
|
|
1307
|
+
*/
|
|
1308
|
+
setFilesSource(filesSource) {
|
|
1309
|
+
this.filesSource = filesSource;
|
|
1310
|
+
this.moduleResolver.setFilesSource(filesSource);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Configure the file loader function for module resolution
|
|
1315
|
+
* @param {Function} loader - Function that takes fileId and returns file content
|
|
1316
|
+
*/
|
|
1317
|
+
setFileLoader(loader) {
|
|
1318
|
+
this.fileLoader = loader;
|
|
1319
|
+
this.moduleResolver.setFileLoader(loader);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Configure the file system adapter for the file module
|
|
1324
|
+
* @param {Object} adapter - Object with readFile(path) and writeFile(path, content) methods
|
|
1325
|
+
*/
|
|
1326
|
+
setFileSystemAdapter(adapter) {
|
|
1327
|
+
this.fileSystemAdapter = adapter;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
setCurrentFilePath(path) {
|
|
1331
|
+
this.moduleResolver.setCurrentFilePath(path);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
async loadModuleDependencies(ast, currentFilePath = null, visited = new Set()) {
|
|
1335
|
+
if (currentFilePath) {
|
|
1336
|
+
this.moduleResolver.setCurrentFilePath(currentFilePath);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Collect all module paths to load (from imports and re-exports)
|
|
1340
|
+
const modulesToLoad = [];
|
|
1341
|
+
|
|
1342
|
+
// Add import dependencies
|
|
1343
|
+
for (const importNode of ast.imports || []) {
|
|
1344
|
+
modulesToLoad.push(importNode.modulePath);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Add re-export source modules
|
|
1348
|
+
for (const exportNode of ast.exports || []) {
|
|
1349
|
+
if (exportNode.type === 'SelectiveReExportStatement' || exportNode.type === 'NamespaceReExportStatement') {
|
|
1350
|
+
modulesToLoad.push(exportNode.modulePath);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Load all modules
|
|
1355
|
+
for (const modulePath of modulesToLoad) {
|
|
1356
|
+
if (visited.has(modulePath)) continue;
|
|
1357
|
+
if (this.moduleResolver.isBuiltinModule(modulePath)) continue;
|
|
1358
|
+
if (this.moduleResolver.cache.has(modulePath)) continue;
|
|
1359
|
+
|
|
1360
|
+
visited.add(modulePath);
|
|
1361
|
+
|
|
1362
|
+
const moduleData = await this.moduleResolver.loadModule(
|
|
1363
|
+
modulePath,
|
|
1364
|
+
(source, options) => this.parse(source, options)
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
if (moduleData.ast && moduleData.fileId) {
|
|
1368
|
+
const depModulePath = buildFilePath(moduleData.fileId, this.filesSource);
|
|
1369
|
+
await this.loadModuleDependencies(moduleData.ast, depModulePath, visited);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
async executeModule(modulePath, options = {}) {
|
|
1375
|
+
const cachedModule = this.moduleResolver.cache.get(modulePath);
|
|
1376
|
+
if (cachedModule && cachedModule.exports) {
|
|
1377
|
+
return cachedModule.exports;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (this.moduleResolver.isBuiltinModule(modulePath)) {
|
|
1381
|
+
return this.moduleResolver.getBuiltinModule(modulePath);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (!cachedModule) {
|
|
1385
|
+
await this.moduleResolver.loadModule(
|
|
1386
|
+
modulePath,
|
|
1387
|
+
(source, opts) => this.parse(source, opts)
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const moduleData = this.moduleResolver.cache.get(modulePath);
|
|
1392
|
+
if (!moduleData || !moduleData.ast) {
|
|
1393
|
+
throw new Error(`Failed to load module: ${modulePath}`);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const moduleFilePath = moduleData.fileId ? buildFilePath(moduleData.fileId, this.filesSource) : modulePath;
|
|
1397
|
+
await this.loadModuleDependencies(moduleData.ast, moduleFilePath);
|
|
1398
|
+
|
|
1399
|
+
// Execute import dependencies
|
|
1400
|
+
for (const importNode of moduleData.ast.imports || []) {
|
|
1401
|
+
const depPath = importNode.modulePath;
|
|
1402
|
+
if (!this.moduleResolver.isBuiltinModule(depPath)) {
|
|
1403
|
+
const depModule = this.moduleResolver.cache.get(depPath);
|
|
1404
|
+
if (depModule && !depModule.exports && depModule.ast) {
|
|
1405
|
+
await this.executeModule(depPath, options);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Execute re-export source modules
|
|
1411
|
+
for (const exportNode of moduleData.ast.exports || []) {
|
|
1412
|
+
if (exportNode.type === 'SelectiveReExportStatement' || exportNode.type === 'NamespaceReExportStatement') {
|
|
1413
|
+
const depPath = exportNode.modulePath;
|
|
1414
|
+
if (!this.moduleResolver.isBuiltinModule(depPath)) {
|
|
1415
|
+
// Modules are cached under their raw paths
|
|
1416
|
+
const depModule = this.moduleResolver.cache.get(depPath);
|
|
1417
|
+
if (depModule && !depModule.exports && depModule.ast) {
|
|
1418
|
+
await this.executeModule(depPath, options);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Type check module before execution
|
|
1425
|
+
// Pass moduleResolver and parser for static type analysis of imports
|
|
1426
|
+
const typeChecker = new TypeChecker({
|
|
1427
|
+
moduleResolver: this.moduleResolver,
|
|
1428
|
+
parser: (source, opts) => this.parse(source, opts),
|
|
1429
|
+
});
|
|
1430
|
+
const typeResult = typeChecker.check(moduleData.ast);
|
|
1431
|
+
|
|
1432
|
+
if (!typeResult.success) {
|
|
1433
|
+
// Print detailed type errors to console
|
|
1434
|
+
console.error('\n\x1b[31m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
1435
|
+
console.error('\x1b[31m║ TYPE ERROR ║\x1b[0m');
|
|
1436
|
+
console.error('\x1b[31m╚══════════════════════════════════════════════════════════════╝\x1b[0m\n');
|
|
1437
|
+
console.error(`\x1b[33mModule:\x1b[0m ${modulePath}\n`);
|
|
1438
|
+
|
|
1439
|
+
for (const err of typeResult.errors) {
|
|
1440
|
+
const loc = err.location
|
|
1441
|
+
? `\x1b[36m${modulePath}:${err.location.line}:${err.location.column}\x1b[0m`
|
|
1442
|
+
: `\x1b[36m${modulePath}\x1b[0m`;
|
|
1443
|
+
console.error(`${loc}`);
|
|
1444
|
+
console.error(`\x1b[31mError:\x1b[0m ${err.message}\n`);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const errorMessages = typeResult.errors.map(err => {
|
|
1448
|
+
const loc = err.location ? `[${err.location.line}:${err.location.column}] ` : '';
|
|
1449
|
+
return `${loc}${err.message}`;
|
|
1450
|
+
}).join('\n');
|
|
1451
|
+
const error = new Error(`Type check failed in module '${modulePath}':\n${errorMessages}`);
|
|
1452
|
+
error.stage = 'type_check';
|
|
1453
|
+
error.type = 'type';
|
|
1454
|
+
error.module = modulePath;
|
|
1455
|
+
error.errors = typeResult.errors;
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// Execute module using bytecode VM
|
|
1460
|
+
const vmResult = await this.executeWithVM(moduleData.ast, {
|
|
1461
|
+
...options,
|
|
1462
|
+
externalVars: options.variables || {},
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
if (!vmResult.success) {
|
|
1466
|
+
// Format error properly - may be string or array
|
|
1467
|
+
const errorMsg = Array.isArray(vmResult.error)
|
|
1468
|
+
? vmResult.error.join('\n')
|
|
1469
|
+
: vmResult.error;
|
|
1470
|
+
const error = new Error(`Error executing module '${modulePath}': ${errorMsg}`);
|
|
1471
|
+
error.stage = 'execute';
|
|
1472
|
+
error.module = modulePath;
|
|
1473
|
+
throw error;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const exports = vmResult.exports || {};
|
|
1477
|
+
|
|
1478
|
+
// Resolve re-exports to actual values
|
|
1479
|
+
await this.resolveReExports(exports, modulePath, options);
|
|
1480
|
+
|
|
1481
|
+
// Cache exports, allLocals, and exportSlots for later import resolution
|
|
1482
|
+
moduleData.exports = exports;
|
|
1483
|
+
moduleData.allLocals = vmResult.allLocals || [];
|
|
1484
|
+
moduleData.exportSlots = vmResult.exportSlots || {};
|
|
1485
|
+
moduleData.compiledExports = vmResult.compiledExports || new Map();
|
|
1486
|
+
|
|
1487
|
+
// Register complex module's arithmetic functions as dynamic overloads
|
|
1488
|
+
// This enables operators (+, -, *, /, etc.) to work with complex numbers
|
|
1489
|
+
if (modulePath === 'complex') {
|
|
1490
|
+
this._registerComplexOverloads(exports, moduleData.allLocals, moduleData.compiledExports);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return exports;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Register complex module's arithmetic functions as dynamic overloads.
|
|
1498
|
+
* This allows the VM's operator handlers to dispatch to Stone functions
|
|
1499
|
+
* for complex number operations.
|
|
1500
|
+
*/
|
|
1501
|
+
_registerComplexOverloads(exports, allLocals, compiledExports) {
|
|
1502
|
+
const binaryOps = [
|
|
1503
|
+
{ name: 'add', sig: { in: ['complex', 'complex'], out: 'complex' } },
|
|
1504
|
+
{ name: 'sub', sig: { in: ['complex', 'complex'], out: 'complex' } },
|
|
1505
|
+
{ name: 'mul', sig: { in: ['complex', 'complex'], out: 'complex' } },
|
|
1506
|
+
{ name: 'div', sig: { in: ['complex', 'complex'], out: 'complex' } },
|
|
1507
|
+
{ name: 'pow', sig: { in: ['complex', 'complex'], out: 'complex' } },
|
|
1508
|
+
];
|
|
1509
|
+
|
|
1510
|
+
const unaryOps = [
|
|
1511
|
+
{ name: 'neg', sig: { in: ['complex'], out: 'complex' } },
|
|
1512
|
+
];
|
|
1513
|
+
|
|
1514
|
+
const allOps = [...binaryOps, ...unaryOps];
|
|
1515
|
+
|
|
1516
|
+
for (const { name, sig } of allOps) {
|
|
1517
|
+
const fn = exports[name];
|
|
1518
|
+
if (fn instanceof CompiledFunction) {
|
|
1519
|
+
registerOverload(name, fn, sig);
|
|
1520
|
+
} else {
|
|
1521
|
+
const exportInfo = compiledExports.get(name);
|
|
1522
|
+
if (exportInfo && allLocals[exportInfo.slot] instanceof CompiledFunction) {
|
|
1523
|
+
registerOverload(name, allLocals[exportInfo.slot], sig);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Resolve re-export metadata to actual values
|
|
1531
|
+
* Re-exports are stored as { _isReExport: true, _sourceName, _sourceModule, ... }
|
|
1532
|
+
* This method replaces them with the actual exported values from the source modules
|
|
1533
|
+
*/
|
|
1534
|
+
async resolveReExports(exports, currentModulePath, options = {}) {
|
|
1535
|
+
for (const [exportName, exportValue] of Object.entries(exports)) {
|
|
1536
|
+
if (exportValue && exportValue._isReExport) {
|
|
1537
|
+
const { _reExportType, _sourceName, _sourceModule } = exportValue;
|
|
1538
|
+
|
|
1539
|
+
// Source modules should already be loaded and executed at this point
|
|
1540
|
+
// They are cached under their raw paths (e.g., './test_modules_math')
|
|
1541
|
+
// Also check builtin modules (like 'primitives') which are not in the cache
|
|
1542
|
+
let sourceModule = this.moduleResolver.cache.get(_sourceModule);
|
|
1543
|
+
let sourceExports = sourceModule?.exports;
|
|
1544
|
+
|
|
1545
|
+
// Check if it's a builtin module (like 'primitives')
|
|
1546
|
+
if (!sourceExports && this.moduleResolver.isBuiltinModule(_sourceModule)) {
|
|
1547
|
+
sourceExports = this.moduleResolver.getBuiltinModule(_sourceModule);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (!sourceExports) {
|
|
1551
|
+
throw new Error(`Re-export failed: source module '${_sourceModule}' not found or not executed`);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
if (_reExportType === 'selective') {
|
|
1555
|
+
// Get specific export from source module
|
|
1556
|
+
if (!(_sourceName in sourceExports)) {
|
|
1557
|
+
throw new Error(`Re-export failed: '${_sourceName}' is not exported from '${_sourceModule}'`);
|
|
1558
|
+
}
|
|
1559
|
+
exports[exportName] = sourceExports[_sourceName];
|
|
1560
|
+
} else if (_reExportType === 'namespace') {
|
|
1561
|
+
// Re-export entire module as namespace
|
|
1562
|
+
exports[exportName] = Object.freeze({ ...sourceExports });
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
console.log(`📤 Resolved re-export '${exportName}' from '${_sourceModule}':`, typeof exports[exportName]);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
async executeWithModules(source, options = {}) {
|
|
1571
|
+
const { currentPath = null, variables = {} } = options;
|
|
1572
|
+
|
|
1573
|
+
// Clear dynamic overloads from previous executions
|
|
1574
|
+
clearDynamicOverloads();
|
|
1575
|
+
|
|
1576
|
+
const ast = this.parse(source, { filename: currentPath || '<stdin>' });
|
|
1577
|
+
|
|
1578
|
+
if (currentPath) {
|
|
1579
|
+
this.moduleResolver.setCurrentFilePath(currentPath);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
await this.loadModuleDependencies(ast, currentPath);
|
|
1583
|
+
|
|
1584
|
+
for (const importNode of ast.imports || []) {
|
|
1585
|
+
const modulePath = importNode.modulePath;
|
|
1586
|
+
if (!this.moduleResolver.isBuiltinModule(modulePath)) {
|
|
1587
|
+
await this.executeModule(modulePath, options);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// Type check before execution
|
|
1592
|
+
// Pass moduleResolver and parser for static type analysis of imports
|
|
1593
|
+
const typeChecker = new TypeChecker({
|
|
1594
|
+
moduleResolver: this.moduleResolver,
|
|
1595
|
+
parser: (source, opts) => this.parse(source, opts),
|
|
1596
|
+
});
|
|
1597
|
+
const typeResult = typeChecker.check(ast);
|
|
1598
|
+
|
|
1599
|
+
if (!typeResult.success) {
|
|
1600
|
+
// Print detailed type errors to console
|
|
1601
|
+
const fileName = currentPath || '<stdin>';
|
|
1602
|
+
console.error('\n\x1b[31m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
1603
|
+
console.error('\x1b[31m║ TYPE ERROR ║\x1b[0m');
|
|
1604
|
+
console.error('\x1b[31m╚══════════════════════════════════════════════════════════════╝\x1b[0m\n');
|
|
1605
|
+
|
|
1606
|
+
for (const err of typeResult.errors) {
|
|
1607
|
+
const loc = err.location
|
|
1608
|
+
? `\x1b[36m${fileName}:${err.location.line}:${err.location.column}\x1b[0m`
|
|
1609
|
+
: `\x1b[36m${fileName}\x1b[0m`;
|
|
1610
|
+
console.error(`${loc}`);
|
|
1611
|
+
console.error(`\x1b[31mError:\x1b[0m ${err.message}\n`);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const errorMessages = typeResult.errors.map(err => {
|
|
1615
|
+
const loc = err.location ? `[${err.location.line}:${err.location.column}] ` : '';
|
|
1616
|
+
return `${loc}${err.message}`;
|
|
1617
|
+
});
|
|
1618
|
+
return {
|
|
1619
|
+
success: false,
|
|
1620
|
+
error: 'Type check failed',
|
|
1621
|
+
output: errorMessages,
|
|
1622
|
+
variables: {},
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Execute using bytecode VM
|
|
1627
|
+
const vmResult = await this.executeWithVM(ast, {
|
|
1628
|
+
...options,
|
|
1629
|
+
externalVars: variables,
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
return {
|
|
1633
|
+
success: vmResult.success,
|
|
1634
|
+
result: vmResult.result,
|
|
1635
|
+
output: vmResult.output,
|
|
1636
|
+
terminalsData: vmResult.terminalsData, // Include terminal data (plots, etc.)
|
|
1637
|
+
error: vmResult.error,
|
|
1638
|
+
variables: {},
|
|
1639
|
+
exports: vmResult.exports || {},
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Clear the module cache
|
|
1645
|
+
*/
|
|
1646
|
+
clearModuleCache() {
|
|
1647
|
+
this.moduleResolver.clearCache();
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Get the module resolver instance
|
|
1652
|
+
* @returns {ModuleResolver}
|
|
1653
|
+
*/
|
|
1654
|
+
getModuleResolver() {
|
|
1655
|
+
return this.moduleResolver;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// ========================================================================
|
|
1659
|
+
// Private Helper Methods
|
|
1660
|
+
// ========================================================================
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Add execution to history
|
|
1664
|
+
* @private
|
|
1665
|
+
*/
|
|
1666
|
+
_addToHistory(fileId, executionData) {
|
|
1667
|
+
if (!this.executionHistory.has(fileId)) {
|
|
1668
|
+
this.executionHistory.set(fileId, []);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const history = this.executionHistory.get(fileId);
|
|
1672
|
+
history.unshift(executionData);
|
|
1673
|
+
|
|
1674
|
+
// Limit history size
|
|
1675
|
+
if (history.length > this.maxHistoryPerFile) {
|
|
1676
|
+
history.pop();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Manage cache size (LRU eviction)
|
|
1682
|
+
* @private
|
|
1683
|
+
*/
|
|
1684
|
+
_manageCacheSize(cache) {
|
|
1685
|
+
if (cache.size >= this.maxCacheSize) {
|
|
1686
|
+
// Remove oldest entry (first key)
|
|
1687
|
+
const firstKey = cache.keys().next().value;
|
|
1688
|
+
cache.delete(firstKey);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Hash a string
|
|
1694
|
+
* @private
|
|
1695
|
+
*/
|
|
1696
|
+
_hashString(str) {
|
|
1697
|
+
let hash = 0;
|
|
1698
|
+
for (let i = 0; i < str.length; i++) {
|
|
1699
|
+
const char = str.charCodeAt(i);
|
|
1700
|
+
hash = ((hash << 5) - hash) + char;
|
|
1701
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
1702
|
+
}
|
|
1703
|
+
return hash.toString(36);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* Hash an object
|
|
1708
|
+
* @private
|
|
1709
|
+
*/
|
|
1710
|
+
_hashObject(obj) {
|
|
1711
|
+
try {
|
|
1712
|
+
// Use a simpler hash for large objects to avoid "Invalid string length" errors
|
|
1713
|
+
const str = JSON.stringify(obj);
|
|
1714
|
+
return this._hashString(str);
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
// If JSON.stringify fails (circular reference or too large), use a fallback
|
|
1717
|
+
console.warn('Failed to hash object, using fallback:', error.message);
|
|
1718
|
+
return this._hashString(String(Date.now()) + String(Math.random()));
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Export singleton instance
|
|
1724
|
+
export const stoneEngineService = new StoneEngineService();
|
|
1725
|
+
|
|
1726
|
+
// Export class for testing
|
|
1727
|
+
export { StoneEngineService };
|