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
package/StoneEngine.js ADDED
@@ -0,0 +1,879 @@
1
+ /**
2
+ * StoneEngine - Platform-agnostic Stone execution engine
3
+ *
4
+ * Provides a thin wrapper around the Stone compiler/VM/AST executor.
5
+ * Uses OutputAdapter for streaming output and FileSystemAdapter for file access.
6
+ *
7
+ * This engine is designed to work identically in:
8
+ * - CLI (with CLIOutputAdapter)
9
+ * - Web app (with WebOutputAdapter)
10
+ * - Daemon (with JSON streaming adapter)
11
+ *
12
+ * Usage:
13
+ * const engine = new StoneEngine({
14
+ * outputAdapter: new CLIOutputAdapter(),
15
+ * fileSystemAdapter: new NodeFileSystemAdapter(),
16
+ * });
17
+ *
18
+ * const result = await engine.execute(sourceCode, { filename: 'script.stn' });
19
+ */
20
+
21
+ import { Lexer } from './frontend/parsing/tokenizer.js';
22
+ import { Parser } from './frontend/parsing/astBuilder.js';
23
+ import { TypeChecker } from './frontend/type-checker/typeChecker.js';
24
+ import { ModuleResolver } from './frontend/utils/moduleResolver.js';
25
+ import { Compiler } from './backends/js-vm/compiler.js';
26
+ import { VM } from './backends/js-vm/vm/index.js';
27
+ import { CompiledFunction } from './backends/js-vm/bytecode.js';
28
+ import { BufferedOutputAdapter } from './adapters/OutputAdapter.js';
29
+ import { createTerminalConstructors } from './frontend/parsing/terminal-registry.js';
30
+ import { createTreeShaker } from './frontend/analysis/treeShaker.js';
31
+ import { setFileSystemAdapter } from './backends/js-vm/primitives/file.js';
32
+ import { registerOverload, clearDynamicOverloads } from './backends/js-vm/primitives/overloads.js';
33
+
34
+ export class StoneEngine {
35
+ /**
36
+ * Create a new Stone engine instance
37
+ * @param {Object} options - Engine configuration
38
+ * @param {OutputAdapter} options.outputAdapter - Output adapter for streaming
39
+ * @param {FileSystemAdapter} options.fileSystemAdapter - File system adapter
40
+ * @param {boolean} options.debug - Enable debug logging
41
+ */
42
+ constructor(options = {}) {
43
+ this.outputAdapter = options.outputAdapter || null;
44
+ this.fileSystemAdapter = options.fileSystemAdapter || null;
45
+ this.debug = options.debug || false;
46
+
47
+ // Core components
48
+ this.compiler = new Compiler();
49
+ this.vm = new VM();
50
+ this.moduleResolver = new ModuleResolver();
51
+
52
+ // Configure module resolver with file system adapter
53
+ if (this.fileSystemAdapter) {
54
+ this.moduleResolver.setFileLoader(async (modulePath) => {
55
+ return this.fileSystemAdapter.readFile(modulePath);
56
+ });
57
+ }
58
+
59
+ // Cached builtins.stn exports
60
+ this.stnBuiltinsExports = null;
61
+ }
62
+
63
+ /**
64
+ * Set the output adapter
65
+ * @param {OutputAdapter} adapter
66
+ */
67
+ setOutputAdapter(adapter) {
68
+ this.outputAdapter = adapter;
69
+ }
70
+
71
+ /**
72
+ * Set the file system adapter
73
+ * @param {FileSystemAdapter} adapter
74
+ */
75
+ setFileSystemAdapter(adapter) {
76
+ this.fileSystemAdapter = adapter;
77
+ if (adapter) {
78
+ this.moduleResolver.setFileLoader(async (modulePath) => {
79
+ return adapter.readFile(modulePath);
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Set the package context for stone_modules resolution
86
+ * @param {PackageContext} context
87
+ */
88
+ setPackageContext(context) {
89
+ this.moduleResolver.setPackageContext(context);
90
+ }
91
+
92
+ /**
93
+ * Recursively tag a CompiledFunction and its nested functions with moduleIdx
94
+ * This is needed for proper upvalue resolution when functions are called from different contexts
95
+ * @param {CompiledFunction} fn - The function to tag
96
+ * @param {number} moduleIdx - The module index
97
+ * @param {Set} visited - Set of already-visited functions (to avoid cycles)
98
+ */
99
+ _tagFunctionWithModuleIdx(fn, moduleIdx, visited = new Set()) {
100
+ if (!fn || !(fn instanceof CompiledFunction) || visited.has(fn)) {
101
+ return;
102
+ }
103
+ visited.add(fn);
104
+ fn.moduleIdx = moduleIdx;
105
+
106
+ // Recursively tag any nested functions in constants
107
+ if (fn.constants) {
108
+ for (const constant of fn.constants) {
109
+ if (constant instanceof CompiledFunction) {
110
+ this._tagFunctionWithModuleIdx(constant, moduleIdx, visited);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Parse source code to AST
118
+ * @param {string} source - Stone source code
119
+ * @param {Object} options - Parse options
120
+ * @returns {Object} AST
121
+ */
122
+ parse(source, options = {}) {
123
+ const { filename = '<stdin>' } = options;
124
+
125
+ try {
126
+ const lexer = new Lexer(source, filename);
127
+ const tokens = lexer.tokenize();
128
+ const parser = new Parser(tokens, filename);
129
+ return parser.parse();
130
+ } catch (error) {
131
+ throw new Error(`Parse error: ${error.message}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Type check an AST
137
+ * @param {Object} ast - AST to check
138
+ * @param {Object} options - Type check options
139
+ * @returns {Object} Type check result { success, errors }
140
+ */
141
+ typeCheck(ast, options = {}) {
142
+ const typeChecker = new TypeChecker({
143
+ moduleResolver: this.moduleResolver,
144
+ parser: (source, opts) => this.parse(source, opts),
145
+ ...options,
146
+ });
147
+
148
+ return typeChecker.check(ast);
149
+ }
150
+
151
+ /**
152
+ * Precheck Stone source code for syntax and type errors
153
+ * Does not execute - only validates
154
+ * @param {string} source - Stone source code
155
+ * @param {Object} options - Options { filename }
156
+ * @returns {Object} { valid: boolean, errors: [], warnings: [] }
157
+ */
158
+ precheck(source, options = {}) {
159
+ const { filename = '<stdin>' } = options;
160
+ const errors = [];
161
+ const warnings = [];
162
+
163
+ try {
164
+ // 1. Parse (syntax check)
165
+ const ast = this.parse(source, { filename });
166
+
167
+ // 2. Type check
168
+ const typeResult = this.typeCheck(ast, options);
169
+
170
+ if (!typeResult.success) {
171
+ errors.push(...typeResult.errors.map(err => ({
172
+ type: 'type',
173
+ message: err.message,
174
+ location: err.location || null
175
+ })));
176
+ }
177
+
178
+ if (typeResult.warnings) {
179
+ warnings.push(...typeResult.warnings);
180
+ }
181
+
182
+ return {
183
+ valid: errors.length === 0,
184
+ errors,
185
+ warnings
186
+ };
187
+ } catch (parseError) {
188
+ // Parse failed - syntax error
189
+ errors.push({
190
+ type: 'syntax',
191
+ message: parseError.message,
192
+ location: null
193
+ });
194
+
195
+ return {
196
+ valid: false,
197
+ errors,
198
+ warnings
199
+ };
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Analyze imports in an AST for tree shaking optimization
205
+ * Returns information about which exports are used from each module,
206
+ * and detects unused imports.
207
+ *
208
+ * @param {Object} ast - AST to analyze
209
+ * @returns {Object} Analysis result
210
+ * @returns {Map<string, Set<string>>} result.usedExports - Map from module path to used export names
211
+ * @returns {Array} result.unusedImports - Array of { localName, modulePath, importedName }
212
+ * @returns {boolean} result.hasUnusedImports - True if there are unused imports
213
+ * @returns {Function} result.filterExports - Function to filter module exports
214
+ */
215
+ analyzeImports(ast) {
216
+ const shaker = createTreeShaker(ast);
217
+ return {
218
+ usedExports: shaker.usedExports,
219
+ unusedImports: shaker.unusedImports,
220
+ hasUnusedImports: shaker.unusedImports.length > 0,
221
+ filterExports: shaker.filterExports,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Execute Stone source code
227
+ * @param {string} source - Stone source code
228
+ * @param {Object} options - Execution options
229
+ * @param {string} options.filename - Source filename (for error reporting)
230
+ * @param {Object} options.variables - Initial variable bindings
231
+ * @param {string} options.currentPath - Current file path (for module resolution)
232
+ * @param {OutputAdapter} options.outputAdapter - Override output adapter for this execution
233
+ * @param {number} options.maxIterations - Maximum loop iterations
234
+ * @returns {Promise<Object>} Execution result
235
+ */
236
+ async execute(source, options = {}) {
237
+ const {
238
+ filename = '<stdin>',
239
+ variables = {},
240
+ currentPath = null,
241
+ maxIterations = 100000,
242
+ } = options;
243
+
244
+ // Use per-execution adapter override or instance adapter
245
+ // If neither, create a buffered adapter for backward compat
246
+ const adapter = options.outputAdapter || this.outputAdapter || new BufferedOutputAdapter();
247
+ const startTime = Date.now();
248
+
249
+ // Clear dynamic overloads from previous executions
250
+ // This ensures a clean slate for each execution
251
+ clearDynamicOverloads();
252
+
253
+ // Set up file system adapter for file module (if available)
254
+ if (this.fileSystemAdapter) {
255
+ setFileSystemAdapter(this.fileSystemAdapter);
256
+ }
257
+
258
+ if (this.debug) {
259
+ console.log('[StoneEngine] Starting execution:', filename);
260
+ }
261
+
262
+ try {
263
+ // 1. Parse
264
+ if (this.debug) console.log('[StoneEngine] Step 1: Parsing...');
265
+ const ast = this.parse(source, { filename });
266
+ if (this.debug) {
267
+ console.log('[StoneEngine] Parsed successfully, statements:', ast.statements?.length);
268
+ console.log('[StoneEngine] Imports:', ast.imports?.map(i => i.modulePath));
269
+ }
270
+
271
+ // 2. Set up module resolution
272
+ // Use full file path for package resolution
273
+ // Handle cases where filename is already an absolute path
274
+ let fullFilePath;
275
+ if (filename.includes('/') || filename.includes('\\')) {
276
+ // filename is already a path (absolute or relative with directories)
277
+ fullFilePath = filename;
278
+ } else if (currentPath) {
279
+ // filename is just a name, combine with currentPath
280
+ fullFilePath = `${currentPath}/${filename}`;
281
+ } else {
282
+ fullFilePath = filename;
283
+ }
284
+ this.moduleResolver.setCurrentFilePath(fullFilePath);
285
+
286
+ // 3. Load builtins (auto-imported functions like sin, cos, etc.)
287
+ if (this.debug) console.log('[StoneEngine] Step 3a: Loading builtins...');
288
+ const builtinsExports = await this.loadBuiltins();
289
+ const combinedVariables = { ...builtinsExports, ...variables };
290
+ if (this.debug) {
291
+ console.log('[StoneEngine] Builtins loaded:', Object.keys(builtinsExports).length, 'exports');
292
+ }
293
+
294
+ // 3b. Load module dependencies
295
+ if (this.debug) console.log('[StoneEngine] Step 3b: Loading module dependencies...');
296
+ const hasImports = ast.imports && ast.imports.length > 0;
297
+ if (hasImports) {
298
+ await this.loadModuleDependencies(ast, fullFilePath);
299
+
300
+ // Execute imported modules
301
+ for (const importNode of ast.imports) {
302
+ const modulePath = importNode.modulePath;
303
+ if (this.debug) console.log('[StoneEngine] Processing import:', modulePath);
304
+ if (!this.moduleResolver.isBuiltinModule(modulePath)) {
305
+ if (this.debug) console.log('[StoneEngine] Executing module:', modulePath);
306
+ await this.executeModule(modulePath, { variables: combinedVariables });
307
+ } else {
308
+ if (this.debug) console.log('[StoneEngine] Skipping JS builtin:', modulePath);
309
+ }
310
+ }
311
+ }
312
+
313
+ // 4. Type check
314
+ if (this.debug) console.log('[StoneEngine] Step 4: Type checking...');
315
+ const typeResult = this.typeCheck(ast);
316
+ if (!typeResult.success) {
317
+ const errors = typeResult.errors.map(err => {
318
+ const loc = err.location
319
+ ? `[${err.location.line}:${err.location.column}] `
320
+ : '';
321
+ return `${loc}${err.message}`;
322
+ });
323
+
324
+ // Report type errors via adapter
325
+ for (const err of typeResult.errors) {
326
+ adapter.error(err.message, err.location);
327
+ }
328
+ adapter.finish(false, null);
329
+
330
+ return {
331
+ success: false,
332
+ error: 'Type check failed',
333
+ errors: typeResult.errors,
334
+ output: errors,
335
+ terminalsData: {},
336
+ duration: Date.now() - startTime,
337
+ };
338
+ }
339
+
340
+ // 5. Execute using bytecode VM (unified execution path)
341
+ if (this.debug) console.log('[StoneEngine] Step 5: Executing with VM...');
342
+ const result = await this.executeWithVM(ast, {
343
+ maxIterations,
344
+ outputAdapter: adapter,
345
+ externalVars: combinedVariables,
346
+ extensions: typeResult.extensions, // Pass extension info to compiler
347
+ });
348
+
349
+ if (this.debug) {
350
+ console.log('[StoneEngine] Execution complete:');
351
+ console.log('[StoneEngine] success:', result.success);
352
+ console.log('[StoneEngine] error:', result.error);
353
+ console.log('[StoneEngine] output:', result.output?.length, 'lines');
354
+ console.log('[StoneEngine] terminalsData keys:', Object.keys(result.terminalsData || {}));
355
+ }
356
+
357
+ return {
358
+ ...result,
359
+ duration: Date.now() - startTime,
360
+ };
361
+ } catch (error) {
362
+ // Report error via adapter
363
+ adapter.error(error.message, null);
364
+ adapter.finish(false, error);
365
+
366
+ // Include error in output and terminalsData for display
367
+ return {
368
+ success: false,
369
+ error: error.message,
370
+ output: [`Error: ${error.message}`],
371
+ terminalsData: {
372
+ main: {
373
+ type: 'console',
374
+ config: { title: 'Main', max_lines: 200 },
375
+ lines: [`Error: ${error.message}`],
376
+ }
377
+ },
378
+ duration: Date.now() - startTime,
379
+ };
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Execute using bytecode VM
385
+ * @param {Object} ast - Parsed AST
386
+ * @param {Object} options - Execution options
387
+ * @param {Object} options.externalVars - External variables to inject
388
+ * @param {OutputAdapter} options.outputAdapter - Output adapter
389
+ * @returns {Promise<Object>} Execution result
390
+ */
391
+ async executeWithVM(ast, options = {}) {
392
+ const adapter = options.outputAdapter || this.outputAdapter;
393
+ const externalVars = options.externalVars || {};
394
+ const extensions = options.extensions || new Set();
395
+
396
+ try {
397
+ // Get external variable names for compiler
398
+ const externalVarNames = Object.keys(externalVars);
399
+
400
+
401
+ // Create a new compiler instance with external var names
402
+ const compiler = new Compiler({
403
+ externalVarNames
404
+ });
405
+
406
+ // Compile AST to bytecode, passing extensions from type checking
407
+ const compiled = compiler.compile(ast, { extensions });
408
+
409
+ if (!compiled.success) {
410
+ const errorMsg = compiled.errors.map(e => e.message).join('\n');
411
+ if (adapter) {
412
+ adapter.error(errorMsg, null);
413
+ adapter.finish(false, null);
414
+ }
415
+ // Include error in output and terminalsData for display
416
+ return {
417
+ success: false,
418
+ error: errorMsg,
419
+ output: [`Error: ${errorMsg}`],
420
+ result: null,
421
+ terminalsData: {
422
+ main: {
423
+ type: 'console',
424
+ config: { title: 'Main', max_lines: 200 },
425
+ lines: [`Error: ${errorMsg}`],
426
+ }
427
+ },
428
+ };
429
+ }
430
+
431
+ // Reset VM modules before loading (indices are relative to each execution)
432
+ this.vm.modules = [];
433
+ this.vm.moduleLocals = [];
434
+
435
+ // Load imported modules into VM before execution
436
+ if (compiled.imports && compiled.imports.length > 0) {
437
+ for (const importInfo of compiled.imports) {
438
+ const modulePath = importInfo.module;
439
+ const importedNames = importInfo.names;
440
+
441
+ // Get module exports (from cache or builtin)
442
+ let moduleExports;
443
+
444
+ // Special handling for 'plots' module - needs runtime terminal constructors
445
+ if (modulePath === 'plots') {
446
+ // Create terminal constructors with the current adapter
447
+ const terminalConstructors = createTerminalConstructors({
448
+ outputAdapter: adapter,
449
+ terminalCounter: this.terminalCounter || 0
450
+ });
451
+ moduleExports = {
452
+ graph2d: terminalConstructors.graph2d,
453
+ graph3d: terminalConstructors.graph3d,
454
+ console_terminal: terminalConstructors.console_terminal
455
+ };
456
+ } else if (this.moduleResolver.isBuiltinModule(modulePath)) {
457
+ moduleExports = this.moduleResolver.getBuiltinModule(modulePath);
458
+ if (this.debug) {
459
+ console.log(`[executeWithVM] Builtin module '${modulePath}':`, Object.keys(moduleExports || {}));
460
+ }
461
+ } else {
462
+ const cachedModule = this.moduleResolver.cache.get(modulePath);
463
+ moduleExports = cachedModule?.exports;
464
+ if (this.debug) {
465
+ console.log(`[executeWithVM] Cached module '${modulePath}':`, Object.keys(moduleExports || {}));
466
+ }
467
+ }
468
+
469
+ if (!moduleExports) {
470
+ throw new Error(`Module '${modulePath}' not found or not loaded`);
471
+ }
472
+
473
+ // Create a module structure for the VM
474
+ const exportsMap = new Map();
475
+ const moduleIdx = this.vm.modules.length; // Index where this module will be stored
476
+
477
+ // Get cached module data
478
+ const cachedModule = this.moduleResolver.cache.get(modulePath);
479
+ const allLocals = cachedModule?.allLocals || [];
480
+ const exportSlots = cachedModule?.exportSlots || {};
481
+ const compiledExports = cachedModule?.compiledExports || new Map();
482
+
483
+ // Use allLocals for moduleStorage to preserve original slot positions for upvalues
484
+ const moduleStorage = allLocals.length > 0 ? [...allLocals] : [];
485
+
486
+ // Handle namespace imports (import math) vs selective imports (import x from math)
487
+ const isNamespaceImport = importedNames.length === 1 && importedNames[0] === '*';
488
+
489
+ if (isNamespaceImport) {
490
+ // For namespace imports, create an object with all exports
491
+ const namespaceObj = { ...moduleExports };
492
+ exportsMap.set('*', { slot: 0 });
493
+ moduleStorage[0] = namespaceObj;
494
+
495
+ // Tag any CompiledFunctions in the namespace
496
+ for (const value of Object.values(namespaceObj)) {
497
+ if (value instanceof CompiledFunction) {
498
+ this._tagFunctionWithModuleIdx(value, moduleIdx);
499
+ }
500
+ }
501
+ } else {
502
+ // Selective imports - original logic
503
+ for (let i = 0; i < importedNames.length; i++) {
504
+ const name = importedNames[i];
505
+ const value = moduleExports[name];
506
+ if (value === undefined && !(name in moduleExports)) {
507
+ throw new Error(`'${name}' is not exported from module '${modulePath}'`);
508
+ }
509
+
510
+ // Use full export info from compiledExports if available (preserves overload info)
511
+ const fullExportInfo = compiledExports.get(name);
512
+ if (fullExportInfo) {
513
+ exportsMap.set(name, fullExportInfo);
514
+ } else {
515
+ // Fall back to cached export slot if available, otherwise use sequential slot
516
+ const originalSlot = exportSlots[name] !== undefined ? exportSlots[name] : i;
517
+ exportsMap.set(name, { slot: originalSlot });
518
+ }
519
+
520
+ // If no allLocals, store at sequential slot
521
+ if (allLocals.length === 0) {
522
+ moduleStorage[i] = value;
523
+ }
524
+
525
+ // Tag CompiledFunctions with moduleIdx (including nested functions in constants)
526
+ if (value instanceof CompiledFunction) {
527
+ this._tagFunctionWithModuleIdx(value, moduleIdx);
528
+ }
529
+ }
530
+ }
531
+
532
+ // Tag all functions in allLocals with moduleIdx
533
+ for (const value of moduleStorage) {
534
+ if (value instanceof CompiledFunction) {
535
+ this._tagFunctionWithModuleIdx(value, moduleIdx);
536
+ }
537
+ }
538
+
539
+ // Load into VM
540
+ this.vm.modules.push({
541
+ id: modulePath,
542
+ exports: exportsMap,
543
+ initializer: null,
544
+ });
545
+ this.vm.moduleLocals.push(moduleStorage);
546
+ }
547
+ }
548
+
549
+ // Execute bytecode with adapter and external variables
550
+ const vmResult = await this.vm.execute(compiled, {
551
+ outputAdapter: adapter,
552
+ externalVars
553
+ });
554
+
555
+ // Get output from adapter if it's a BufferedOutputAdapter, otherwise from VM
556
+ const outputFromAdapter = adapter?.output || [];
557
+ const terminalsFromAdapter = adapter?.terminalsData || {};
558
+ let combinedOutput = vmResult.output?.length > 0 ? vmResult.output : outputFromAdapter;
559
+
560
+ // If execution failed, add error to output
561
+ if (!vmResult.success && vmResult.error) {
562
+ combinedOutput = [...combinedOutput, `Error: ${vmResult.error}`];
563
+ }
564
+
565
+ // Build terminalsData - prefer adapter's data if available
566
+ let terminalsData = Object.keys(terminalsFromAdapter).length > 0
567
+ ? terminalsFromAdapter
568
+ : (combinedOutput.length > 0 ? {
569
+ main: {
570
+ type: 'console',
571
+ config: { title: 'Main', max_lines: 200 },
572
+ lines: combinedOutput,
573
+ }
574
+ } : {});
575
+
576
+ // Ensure errors are in terminalsData even if no other output
577
+ if (!vmResult.success && vmResult.error && Object.keys(terminalsData).length === 0) {
578
+ terminalsData = {
579
+ main: {
580
+ type: 'console',
581
+ config: { title: 'Main', max_lines: 200 },
582
+ lines: [`Error: ${vmResult.error}`],
583
+ }
584
+ };
585
+ }
586
+
587
+ // Capture all locals for module caching (needed for closure upvalues)
588
+ const localCount = compiled.main?.localCount || 0;
589
+ const allLocals = [];
590
+ for (let i = 0; i < localCount; i++) {
591
+ allLocals[i] = this.vm.locals[i];
592
+ }
593
+
594
+ return {
595
+ success: vmResult.success,
596
+ result: vmResult.result,
597
+ output: combinedOutput,
598
+ error: vmResult.error,
599
+ variables: {},
600
+ terminalsData,
601
+ exports: vmResult.exports, // Pass through exports from VM
602
+ exportSlots: vmResult.exportSlots, // Original slot positions for exports
603
+ compiledExports: compiled.exports, // Full export info including overload data
604
+ allLocals, // All locals for module caching
605
+ };
606
+ } catch (e) {
607
+ if (adapter) {
608
+ adapter.error(e.message, null);
609
+ adapter.finish(false, e);
610
+ }
611
+ // Add error to terminalsData so it displays in viewport
612
+ const errorTerminalsData = {
613
+ main: {
614
+ type: 'console',
615
+ config: { title: 'Main', max_lines: 200 },
616
+ lines: [`Error: ${e.message}`],
617
+ }
618
+ };
619
+ return {
620
+ success: false,
621
+ error: e.message,
622
+ output: [`Error: ${e.message}`],
623
+ result: null,
624
+ terminalsData: errorTerminalsData,
625
+ };
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Load module dependencies recursively
631
+ * @param {Object} ast - AST with imports
632
+ * @param {string} currentFilePath - Current file path
633
+ * @param {Set} visited - Visited modules (for cycle detection)
634
+ */
635
+ async loadModuleDependencies(ast, currentFilePath = null, visited = new Set()) {
636
+ if (currentFilePath) {
637
+ this.moduleResolver.setCurrentFilePath(currentFilePath);
638
+ }
639
+
640
+ // Collect all modules to load (with inferredFilename for directory inference)
641
+ const modulesToLoad = [];
642
+
643
+ for (const importNode of ast.imports || []) {
644
+ modulesToLoad.push({
645
+ modulePath: importNode.modulePath,
646
+ inferredFilename: importNode.inferredFilename || null
647
+ });
648
+ }
649
+
650
+ for (const exportNode of ast.exports || []) {
651
+ if (exportNode.type === 'SelectiveReExportStatement' ||
652
+ exportNode.type === 'NamespaceReExportStatement') {
653
+ modulesToLoad.push({
654
+ modulePath: exportNode.modulePath,
655
+ inferredFilename: null // Re-exports don't use directory inference
656
+ });
657
+ }
658
+ }
659
+
660
+ // Load all modules
661
+ for (const { modulePath, inferredFilename } of modulesToLoad) {
662
+ if (visited.has(modulePath)) continue;
663
+ if (this.moduleResolver.isBuiltinModule(modulePath)) continue;
664
+ if (this.moduleResolver.cache.has(modulePath)) continue;
665
+
666
+ visited.add(modulePath);
667
+
668
+ const moduleData = await this.moduleResolver.loadModule(
669
+ modulePath,
670
+ (source, options) => this.parse(source, options),
671
+ inferredFilename // Pass inferredFilename for directory + inferred filename resolution
672
+ );
673
+
674
+ if (moduleData.ast && moduleData.fileId) {
675
+ await this.loadModuleDependencies(moduleData.ast, moduleData.fileId, visited);
676
+ }
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Execute a module and cache its exports
682
+ * @param {string} modulePath - Module path
683
+ * @param {Object} options - Execution options
684
+ * @returns {Promise<Object>} Module exports
685
+ */
686
+ async executeModule(modulePath, options = {}) {
687
+ const cachedModule = this.moduleResolver.cache.get(modulePath);
688
+ if (cachedModule && cachedModule.exports) {
689
+ return cachedModule.exports;
690
+ }
691
+
692
+ if (this.moduleResolver.isBuiltinModule(modulePath)) {
693
+ return this.moduleResolver.getBuiltinModule(modulePath);
694
+ }
695
+
696
+ if (!cachedModule) {
697
+ await this.moduleResolver.loadModule(
698
+ modulePath,
699
+ (source, opts) => this.parse(source, opts)
700
+ );
701
+ }
702
+
703
+ const moduleData = this.moduleResolver.cache.get(modulePath);
704
+ if (!moduleData || !moduleData.ast) {
705
+ throw new Error(`Failed to load module: ${modulePath}`);
706
+ }
707
+
708
+ // Load dependencies
709
+ await this.loadModuleDependencies(moduleData.ast, moduleData.fileId);
710
+
711
+ // Execute import dependencies
712
+ for (const importNode of moduleData.ast.imports || []) {
713
+ const depPath = importNode.modulePath;
714
+ if (!this.moduleResolver.isBuiltinModule(depPath)) {
715
+ const depModule = this.moduleResolver.cache.get(depPath);
716
+ if (depModule && !depModule.exports && depModule.ast) {
717
+ await this.executeModule(depPath, options);
718
+ }
719
+ }
720
+ }
721
+
722
+ // Type check module
723
+ const typeResult = this.typeCheck(moduleData.ast);
724
+ if (!typeResult.success) {
725
+ const errorMessages = typeResult.errors.map(err => {
726
+ const loc = err.location ? `[${err.location.line}:${err.location.column}] ` : '';
727
+ return `${loc}${err.message}`;
728
+ }).join('\n');
729
+ const error = new Error(`Type check failed in module '${modulePath}':\n${errorMessages}`);
730
+ error.stage = 'type_check';
731
+ error.type = 'type';
732
+ error.module = modulePath;
733
+ error.errors = typeResult.errors;
734
+ throw error;
735
+ }
736
+
737
+ // Execute module using bytecode VM
738
+ const vmResult = await this.executeWithVM(moduleData.ast, {
739
+ externalVars: options.variables || {},
740
+ });
741
+
742
+ if (!vmResult.success) {
743
+ throw new Error(`Error executing module '${modulePath}': ${vmResult.error}`);
744
+ }
745
+
746
+ const exports = vmResult.exports || {};
747
+
748
+ // Resolve re-exports to actual values
749
+ await this.resolveReExports(exports, modulePath);
750
+
751
+ moduleData.exports = exports;
752
+ // Cache all locals for closure upvalue capture when module is imported
753
+ moduleData.allLocals = vmResult.allLocals || [];
754
+ // Cache original export slots for correct IMPORT_GET resolution
755
+ moduleData.exportSlots = vmResult.exportSlots || {};
756
+ // Cache full export info for overloaded functions
757
+ moduleData.compiledExports = vmResult.compiledExports || new Map();
758
+
759
+ // Register complex module's arithmetic functions as dynamic overloads
760
+ // This enables operators (+, -, *, /, etc.) to work with complex numbers
761
+ if (modulePath === 'complex') {
762
+ this._registerComplexOverloads(exports, moduleData.allLocals, moduleData.compiledExports);
763
+ }
764
+
765
+ return exports;
766
+ }
767
+
768
+ /**
769
+ * Register complex module's arithmetic functions as dynamic overloads.
770
+ * This allows the VM's operator handlers to dispatch to Stone functions
771
+ * for complex number operations.
772
+ */
773
+ _registerComplexOverloads(exports, allLocals, compiledExports) {
774
+ // Arithmetic operations with two Complex arguments
775
+ const binaryOps = [
776
+ { name: 'add', sig: { in: ['complex', 'complex'], out: 'complex' } },
777
+ { name: 'sub', sig: { in: ['complex', 'complex'], out: 'complex' } },
778
+ { name: 'mul', sig: { in: ['complex', 'complex'], out: 'complex' } },
779
+ { name: 'div', sig: { in: ['complex', 'complex'], out: 'complex' } },
780
+ { name: 'pow', sig: { in: ['complex', 'complex'], out: 'complex' } },
781
+ ];
782
+
783
+ // Unary operations
784
+ const unaryOps = [
785
+ { name: 'neg', sig: { in: ['complex'], out: 'complex' } },
786
+ ];
787
+
788
+ const allOps = [...binaryOps, ...unaryOps];
789
+
790
+ for (const { name, sig } of allOps) {
791
+ const fn = exports[name];
792
+ if (fn instanceof CompiledFunction) {
793
+ registerOverload(name, fn, sig);
794
+ } else {
795
+ // Try to get from compiledExports which has slot info
796
+ const exportInfo = compiledExports.get(name);
797
+ if (exportInfo && allLocals[exportInfo.slot] instanceof CompiledFunction) {
798
+ registerOverload(name, allLocals[exportInfo.slot], sig);
799
+ }
800
+ }
801
+ }
802
+ }
803
+
804
+ /**
805
+ * Clear the module cache
806
+ */
807
+ clearModuleCache() {
808
+ this.moduleResolver.clearCache();
809
+ }
810
+
811
+ /**
812
+ * Resolve re-export metadata to actual values
813
+ * Re-exports are stored as { _isReExport: true, _sourceName, _sourceModule, ... }
814
+ * This method replaces them with the actual exported values from the source modules
815
+ */
816
+ async resolveReExports(exports, currentModulePath) {
817
+ for (const [exportName, exportValue] of Object.entries(exports)) {
818
+ if (exportValue && exportValue._isReExport) {
819
+ const { _reExportType, _sourceName, _sourceModule } = exportValue;
820
+
821
+ // Source modules should already be loaded and executed at this point
822
+ let sourceModule = this.moduleResolver.cache.get(_sourceModule);
823
+ let sourceExports = sourceModule?.exports;
824
+
825
+ // Check if it's a builtin module (like 'primitives')
826
+ if (!sourceExports && this.moduleResolver.isBuiltinModule(_sourceModule)) {
827
+ sourceExports = this.moduleResolver.getBuiltinModule(_sourceModule);
828
+ }
829
+
830
+ if (!sourceExports) {
831
+ throw new Error(`Re-export failed: source module '${_sourceModule}' not found or not executed`);
832
+ }
833
+
834
+ if (_reExportType === 'selective') {
835
+ // Get specific export from source module
836
+ if (!(_sourceName in sourceExports)) {
837
+ throw new Error(`Re-export failed: '${_sourceName}' is not exported from '${_sourceModule}'`);
838
+ }
839
+ exports[exportName] = sourceExports[_sourceName];
840
+ } else if (_reExportType === 'namespace') {
841
+ // Re-export entire module as namespace
842
+ exports[exportName] = Object.freeze({ ...sourceExports });
843
+ }
844
+ }
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Load builtins.stn exports (auto-imported primitives)
850
+ */
851
+ async loadBuiltins() {
852
+ if (this.stnBuiltinsExports) {
853
+ return this.stnBuiltinsExports;
854
+ }
855
+
856
+ try {
857
+ await this.moduleResolver.loadModule(
858
+ 'builtins',
859
+ (source, opts) => this.parse(source, opts)
860
+ );
861
+
862
+ const exports = await this.executeModule('builtins', {});
863
+ this.stnBuiltinsExports = exports || {};
864
+
865
+ if (this.debug) {
866
+ console.log('Loaded builtins.stn exports:', Object.keys(this.stnBuiltinsExports));
867
+ }
868
+
869
+ return this.stnBuiltinsExports;
870
+ } catch (error) {
871
+ if (this.debug) {
872
+ console.error('Failed to load builtins.stn:', error);
873
+ }
874
+ return {};
875
+ }
876
+ }
877
+ }
878
+
879
+ export default StoneEngine;