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.
Files changed (68) hide show
  1. package/README.md +52 -0
  2. package/StoneEngine.js +879 -0
  3. package/StoneEngineService.js +1727 -0
  4. package/adapters/FileSystemAdapter.js +230 -0
  5. package/adapters/OutputAdapter.js +208 -0
  6. package/adapters/index.js +6 -0
  7. package/cli/CLIOutputAdapter.js +196 -0
  8. package/cli/DaemonClient.js +349 -0
  9. package/cli/JSONOutputAdapter.js +135 -0
  10. package/cli/ReplSession.js +567 -0
  11. package/cli/ViewerServer.js +590 -0
  12. package/cli/commands/check.js +84 -0
  13. package/cli/commands/daemon.js +189 -0
  14. package/cli/commands/kill.js +66 -0
  15. package/cli/commands/package.js +713 -0
  16. package/cli/commands/ps.js +65 -0
  17. package/cli/commands/run.js +537 -0
  18. package/cli/entry.js +169 -0
  19. package/cli/index.js +14 -0
  20. package/cli/stonec.js +358 -0
  21. package/cli/test-compiler.js +181 -0
  22. package/cli/viewer/index.html +495 -0
  23. package/daemon/IPCServer.js +455 -0
  24. package/daemon/ProcessManager.js +327 -0
  25. package/daemon/ProcessRunner.js +307 -0
  26. package/daemon/daemon.js +398 -0
  27. package/daemon/index.js +16 -0
  28. package/frontend/analysis/index.js +5 -0
  29. package/frontend/analysis/livenessAnalyzer.js +568 -0
  30. package/frontend/analysis/treeShaker.js +265 -0
  31. package/frontend/index.js +20 -0
  32. package/frontend/parsing/astBuilder.js +2196 -0
  33. package/frontend/parsing/index.js +7 -0
  34. package/frontend/parsing/sonParser.js +592 -0
  35. package/frontend/parsing/stoneAstTypes.js +703 -0
  36. package/frontend/parsing/terminal-registry.js +435 -0
  37. package/frontend/parsing/tokenizer.js +692 -0
  38. package/frontend/type-checker/OverloadedFunctionType.js +43 -0
  39. package/frontend/type-checker/TypeEnvironment.js +165 -0
  40. package/frontend/type-checker/bidirectionalInference.js +149 -0
  41. package/frontend/type-checker/index.js +10 -0
  42. package/frontend/type-checker/moduleAnalysis.js +248 -0
  43. package/frontend/type-checker/operatorMappings.js +35 -0
  44. package/frontend/type-checker/overloadResolution.js +605 -0
  45. package/frontend/type-checker/typeChecker.js +452 -0
  46. package/frontend/type-checker/typeCompatibility.js +389 -0
  47. package/frontend/type-checker/visitors/controlFlow.js +483 -0
  48. package/frontend/type-checker/visitors/functions.js +604 -0
  49. package/frontend/type-checker/visitors/index.js +38 -0
  50. package/frontend/type-checker/visitors/literals.js +341 -0
  51. package/frontend/type-checker/visitors/modules.js +159 -0
  52. package/frontend/type-checker/visitors/operators.js +109 -0
  53. package/frontend/type-checker/visitors/statements.js +768 -0
  54. package/frontend/types/index.js +5 -0
  55. package/frontend/types/operatorMap.js +134 -0
  56. package/frontend/types/types.js +2046 -0
  57. package/frontend/utils/errorCollector.js +244 -0
  58. package/frontend/utils/index.js +5 -0
  59. package/frontend/utils/moduleResolver.js +479 -0
  60. package/package.json +50 -0
  61. package/packages/browserCache.js +359 -0
  62. package/packages/fetcher.js +236 -0
  63. package/packages/index.js +130 -0
  64. package/packages/lockfile.js +271 -0
  65. package/packages/manifest.js +291 -0
  66. package/packages/packageResolver.js +356 -0
  67. package/packages/resolver.js +310 -0
  68. 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 };