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,590 @@
1
+ /**
2
+ * ViewerServer - Minimal HTTP + WebSocket server for graph viewing
3
+ *
4
+ * Provides a lightweight browser-based viewer for Stone graphs:
5
+ * - HTTP server for serving the viewer HTML
6
+ * - WebSocket server for live updates
7
+ * - REST API for initial state
8
+ *
9
+ * The viewer is a minimal HTML page that loads Plotly/Three.js from CDN
10
+ * and connects via WebSocket for live updates.
11
+ */
12
+
13
+ import http from 'http';
14
+ import { WebSocketServer } from 'ws';
15
+ import { EventEmitter } from 'events';
16
+ import path from 'path';
17
+ import fs from 'fs';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+
22
+ // Default port range for viewer server
23
+ const DEFAULT_PORT_START = 3847;
24
+ const DEFAULT_PORT_END = 3857;
25
+
26
+ /**
27
+ * Session - Holds state for a viewing session
28
+ */
29
+ class ViewerSession {
30
+ constructor(id) {
31
+ this.id = id;
32
+ this.terminals = {};
33
+ this.created = Date.now();
34
+ this.clients = new Set(); // WebSocket clients
35
+ }
36
+
37
+ addTerminal(id, type, config) {
38
+ this.terminals[id] = {
39
+ type,
40
+ config,
41
+ data: [],
42
+ };
43
+ }
44
+
45
+ addToTerminal(id, data) {
46
+ const terminal = this.terminals[id];
47
+ if (terminal) {
48
+ const items = Array.isArray(data) ? data : [data];
49
+ terminal.data.push(...items);
50
+ }
51
+ }
52
+
53
+ setTerminal(id, data) {
54
+ const terminal = this.terminals[id];
55
+ if (terminal) {
56
+ terminal.data = Array.isArray(data) ? data : [data];
57
+ }
58
+ }
59
+
60
+ clearTerminal(id) {
61
+ const terminal = this.terminals[id];
62
+ if (terminal) {
63
+ terminal.data = [];
64
+ }
65
+ }
66
+
67
+ broadcast(message) {
68
+ const data = JSON.stringify(message);
69
+ for (const client of this.clients) {
70
+ if (client.readyState === 1) { // WebSocket.OPEN
71
+ client.send(data);
72
+ }
73
+ }
74
+ }
75
+
76
+ getState() {
77
+ return {
78
+ id: this.id,
79
+ terminals: this.terminals,
80
+ created: this.created,
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * ViewerServer - HTTP + WebSocket server for graph viewing
87
+ */
88
+ export class ViewerServer extends EventEmitter {
89
+ constructor(options = {}) {
90
+ super();
91
+ this.port = options.port || DEFAULT_PORT_START;
92
+ this.host = options.host || 'localhost';
93
+ this.server = null;
94
+ this.wss = null;
95
+ this.sessions = new Map(); // sessionId -> ViewerSession
96
+ this.sessionCounter = 0;
97
+ }
98
+
99
+ /**
100
+ * Start the server
101
+ * @returns {Promise<number>} The port the server is listening on
102
+ */
103
+ async start() {
104
+ return new Promise((resolve, reject) => {
105
+ this.server = http.createServer((req, res) => {
106
+ this._handleRequest(req, res);
107
+ });
108
+
109
+ // Set up WebSocket server once, before trying ports
110
+ this.wss = new WebSocketServer({ noServer: true });
111
+ this.wss.on('connection', (ws, req) => {
112
+ this._handleWebSocket(ws, req);
113
+ });
114
+
115
+ // Track sockets being upgraded to prevent double handling
116
+ const upgradingSocketsSet = new WeakSet();
117
+
118
+ // Handle upgrade requests - only add once
119
+ this.server.on('upgrade', (req, socket, head) => {
120
+ // Prevent double-handling of the same socket
121
+ if (upgradingSocketsSet.has(socket)) {
122
+ return;
123
+ }
124
+ upgradingSocketsSet.add(socket);
125
+
126
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
127
+ this.wss.emit('connection', ws, req);
128
+ });
129
+ });
130
+
131
+ // Try ports in range
132
+ const tryPort = (port) => {
133
+ if (port > DEFAULT_PORT_END) {
134
+ reject(new Error('No available ports'));
135
+ return;
136
+ }
137
+
138
+ this.server.once('error', (err) => {
139
+ if (err.code === 'EADDRINUSE') {
140
+ tryPort(port + 1);
141
+ } else {
142
+ reject(err);
143
+ }
144
+ });
145
+
146
+ this.server.listen(port, this.host, () => {
147
+ this.port = port;
148
+ this.emit('listening', this.port);
149
+ resolve(this.port);
150
+ });
151
+ };
152
+
153
+ tryPort(this.port);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Stop the server
159
+ */
160
+ async stop() {
161
+ // Close all WebSocket connections
162
+ if (this.wss) {
163
+ for (const client of this.wss.clients) {
164
+ client.close();
165
+ }
166
+ this.wss.close();
167
+ }
168
+
169
+ // Close HTTP server
170
+ if (this.server) {
171
+ return new Promise((resolve) => {
172
+ this.server.close(resolve);
173
+ });
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Create a new viewing session
179
+ * @returns {string} Session ID
180
+ */
181
+ createSession() {
182
+ const id = `s${++this.sessionCounter}_${Date.now().toString(36)}`;
183
+ const session = new ViewerSession(id);
184
+ this.sessions.set(id, session);
185
+ return id;
186
+ }
187
+
188
+ /**
189
+ * Get a session by ID
190
+ * @param {string} id - Session ID
191
+ * @returns {ViewerSession|null}
192
+ */
193
+ getSession(id) {
194
+ return this.sessions.get(id) || null;
195
+ }
196
+
197
+ /**
198
+ * Get the URL for a session
199
+ * @param {string} sessionId - Session ID
200
+ * @returns {string}
201
+ */
202
+ getViewUrl(sessionId) {
203
+ return `http://${this.host}:${this.port}/view/${sessionId}`;
204
+ }
205
+
206
+ /**
207
+ * Handle HTTP requests
208
+ */
209
+ _handleRequest(req, res) {
210
+ const url = new URL(req.url, `http://${req.headers.host}`);
211
+ const pathname = url.pathname;
212
+
213
+ // Serve viewer HTML
214
+ if (pathname.startsWith('/view/')) {
215
+ const sessionId = pathname.split('/')[2];
216
+ this._serveViewer(res, sessionId);
217
+ return;
218
+ }
219
+
220
+ // API: Get session state
221
+ if (pathname.startsWith('/api/terminals/')) {
222
+ const sessionId = pathname.split('/')[3];
223
+ this._serveSessionState(res, sessionId);
224
+ return;
225
+ }
226
+
227
+ // 404
228
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
229
+ res.end('Not Found');
230
+ }
231
+
232
+ /**
233
+ * Serve the viewer HTML
234
+ */
235
+ _serveViewer(res, sessionId) {
236
+ const session = this.sessions.get(sessionId);
237
+ if (!session) {
238
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
239
+ res.end('Session not found');
240
+ return;
241
+ }
242
+
243
+ // Check for custom viewer file
244
+ const viewerPath = path.join(__dirname, 'viewer', 'index.html');
245
+ if (fs.existsSync(viewerPath)) {
246
+ const html = fs.readFileSync(viewerPath, 'utf8');
247
+ res.writeHead(200, { 'Content-Type': 'text/html' });
248
+ res.end(html);
249
+ return;
250
+ }
251
+
252
+ // Fallback: inline minimal viewer
253
+ const html = this._getInlineViewer(sessionId);
254
+ res.writeHead(200, { 'Content-Type': 'text/html' });
255
+ res.end(html);
256
+ }
257
+
258
+ /**
259
+ * Serve session state as JSON
260
+ */
261
+ _serveSessionState(res, sessionId) {
262
+ const session = this.sessions.get(sessionId);
263
+ if (!session) {
264
+ res.writeHead(404, { 'Content-Type': 'application/json' });
265
+ res.end(JSON.stringify({ error: 'Session not found' }));
266
+ return;
267
+ }
268
+
269
+ res.writeHead(200, { 'Content-Type': 'application/json' });
270
+ res.end(JSON.stringify(session.getState()));
271
+ }
272
+
273
+ /**
274
+ * Handle WebSocket connections
275
+ */
276
+ _handleWebSocket(ws, req) {
277
+ const url = new URL(req.url, `http://${req.headers.host}`);
278
+ const pathname = url.pathname;
279
+
280
+ // Extract session ID from /ws/:sessionId
281
+ if (!pathname.startsWith('/ws/')) {
282
+ ws.close(4000, 'Invalid path');
283
+ return;
284
+ }
285
+
286
+ const sessionId = pathname.split('/')[2];
287
+ const session = this.sessions.get(sessionId);
288
+
289
+ if (!session) {
290
+ ws.close(4001, 'Session not found');
291
+ return;
292
+ }
293
+
294
+ // Add client to session
295
+ session.clients.add(ws);
296
+
297
+ // Send initial state
298
+ ws.send(JSON.stringify({
299
+ type: 'init',
300
+ ...session.getState(),
301
+ }));
302
+
303
+ ws.on('close', () => {
304
+ session.clients.delete(ws);
305
+ });
306
+
307
+ ws.on('error', () => {
308
+ session.clients.delete(ws);
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Generate inline minimal viewer HTML
314
+ */
315
+ _getInlineViewer(sessionId) {
316
+ return `<!DOCTYPE html>
317
+ <html lang="en">
318
+ <head>
319
+ <meta charset="UTF-8">
320
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
321
+ <title>Stone Viewer</title>
322
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
323
+ <style>
324
+ * { margin: 0; padding: 0; box-sizing: border-box; }
325
+ body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a1a; color: #fff; }
326
+ .container { display: flex; flex-wrap: wrap; gap: 16px; padding: 16px; }
327
+ .terminal { background: #2a2a2a; border-radius: 8px; padding: 16px; min-width: 400px; flex: 1; }
328
+ .terminal-title { font-size: 14px; color: #888; margin-bottom: 8px; }
329
+ .console-lines { font-family: monospace; font-size: 13px; max-height: 400px; overflow-y: auto; }
330
+ .console-line { padding: 2px 0; }
331
+ .graph { min-height: 300px; }
332
+ .status { position: fixed; bottom: 16px; right: 16px; padding: 8px 16px; background: #333; border-radius: 4px; font-size: 12px; }
333
+ .status.connected { background: #1a472a; }
334
+ .status.disconnected { background: #472a1a; }
335
+ </style>
336
+ </head>
337
+ <body>
338
+ <div class="container" id="terminals"></div>
339
+ <div class="status" id="status">Connecting...</div>
340
+
341
+ <script>
342
+ const sessionId = '${sessionId}';
343
+ const container = document.getElementById('terminals');
344
+ const statusEl = document.getElementById('status');
345
+ let terminals = {};
346
+
347
+ // Connect WebSocket
348
+ const wsUrl = \`ws://\${location.host}/ws/\${sessionId}\`;
349
+ let ws;
350
+
351
+ function connect() {
352
+ ws = new WebSocket(wsUrl);
353
+
354
+ ws.onopen = () => {
355
+ statusEl.textContent = 'Connected';
356
+ statusEl.className = 'status connected';
357
+ };
358
+
359
+ ws.onclose = () => {
360
+ statusEl.textContent = 'Disconnected';
361
+ statusEl.className = 'status disconnected';
362
+ // Reconnect after delay
363
+ setTimeout(connect, 2000);
364
+ };
365
+
366
+ ws.onerror = () => {
367
+ statusEl.textContent = 'Error';
368
+ statusEl.className = 'status disconnected';
369
+ };
370
+
371
+ ws.onmessage = (e) => {
372
+ const msg = JSON.parse(e.data);
373
+ handleMessage(msg);
374
+ };
375
+ }
376
+
377
+ function handleMessage(msg) {
378
+ switch (msg.type) {
379
+ case 'init':
380
+ // Initialize all terminals
381
+ for (const [id, data] of Object.entries(msg.terminals)) {
382
+ createTerminal(id, data.type, data.config);
383
+ if (data.data && data.data.length > 0) {
384
+ updateTerminal(id, data.type, data.data, false);
385
+ }
386
+ }
387
+ break;
388
+
389
+ case 'terminal:create':
390
+ createTerminal(msg.id, msg.terminalType, msg.config);
391
+ break;
392
+
393
+ case 'terminal:add':
394
+ updateTerminal(msg.id, terminals[msg.id]?.type, msg.data, true);
395
+ break;
396
+
397
+ case 'terminal:set':
398
+ updateTerminal(msg.id, terminals[msg.id]?.type, msg.data, false);
399
+ break;
400
+
401
+ case 'terminal:clear':
402
+ clearTerminal(msg.id);
403
+ break;
404
+ }
405
+ }
406
+
407
+ function createTerminal(id, type, config) {
408
+ if (terminals[id]) return;
409
+
410
+ const div = document.createElement('div');
411
+ div.className = 'terminal';
412
+ div.id = \`terminal-\${id}\`;
413
+
414
+ const title = document.createElement('div');
415
+ title.className = 'terminal-title';
416
+ title.textContent = config.title || id;
417
+ div.appendChild(title);
418
+
419
+ if (type === 'console') {
420
+ const lines = document.createElement('div');
421
+ lines.className = 'console-lines';
422
+ lines.id = \`lines-\${id}\`;
423
+ div.appendChild(lines);
424
+ } else {
425
+ const graph = document.createElement('div');
426
+ graph.className = 'graph';
427
+ graph.id = \`graph-\${id}\`;
428
+ div.appendChild(graph);
429
+ }
430
+
431
+ container.appendChild(div);
432
+ terminals[id] = { type, config, element: div, data: [] };
433
+ }
434
+
435
+ function updateTerminal(id, type, data, append) {
436
+ const terminal = terminals[id];
437
+ if (!terminal) return;
438
+
439
+ const items = Array.isArray(data) ? data : [data];
440
+
441
+ if (append) {
442
+ terminal.data.push(...items);
443
+ } else {
444
+ terminal.data = items;
445
+ }
446
+
447
+ if (type === 'console') {
448
+ const linesEl = document.getElementById(\`lines-\${id}\`);
449
+ if (linesEl) {
450
+ if (!append) linesEl.innerHTML = '';
451
+ for (const item of items) {
452
+ const line = document.createElement('div');
453
+ line.className = 'console-line';
454
+ line.textContent = String(item);
455
+ linesEl.appendChild(line);
456
+ }
457
+ linesEl.scrollTop = linesEl.scrollHeight;
458
+ }
459
+ } else if (type === 'graph2d') {
460
+ renderGraph2D(id, terminal.data, terminal.config);
461
+ } else if (type === 'graph3d') {
462
+ renderGraph3D(id, terminal.data, terminal.config);
463
+ }
464
+ }
465
+
466
+ function clearTerminal(id) {
467
+ const terminal = terminals[id];
468
+ if (!terminal) return;
469
+
470
+ terminal.data = [];
471
+
472
+ if (terminal.type === 'console') {
473
+ const linesEl = document.getElementById(\`lines-\${id}\`);
474
+ if (linesEl) linesEl.innerHTML = '';
475
+ } else {
476
+ const graphEl = document.getElementById(\`graph-\${id}\`);
477
+ if (graphEl) Plotly.purge(graphEl);
478
+ }
479
+ }
480
+
481
+ // Convert StoneArray to regular JS array
482
+ function stoneArrayToJS(arr) {
483
+ if (!arr || arr._type !== 'StoneArray') return arr;
484
+ const data = arr.data;
485
+ if (ArrayBuffer.isView(data)) return Array.from(data);
486
+ if (Array.isArray(data)) return data;
487
+ if (typeof data === 'object') {
488
+ const size = arr.size || Object.keys(data).length;
489
+ const result = [];
490
+ for (let i = 0; i < size; i++) result.push(data[i]);
491
+ return result;
492
+ }
493
+ return [];
494
+ }
495
+
496
+ // Process Stone plot data to Plotly format
497
+ function processPlotData(plot) {
498
+ const type = plot.type || 'line';
499
+ let x = plot.x;
500
+ let y = plot.y;
501
+
502
+ // Handle StoneArray format first
503
+ if (x && x._type === 'StoneArray') x = stoneArrayToJS(x);
504
+ if (y && y._type === 'StoneArray') y = stoneArrayToJS(y);
505
+
506
+ // If only y is provided, generate x as indices
507
+ if (y && !x) {
508
+ const yArr = Array.isArray(y) ? y : [];
509
+ x = yArr.map((_, i) => i);
510
+ }
511
+
512
+ const color = plot.style?.color || plot.color || null;
513
+
514
+ switch (type) {
515
+ case 'line':
516
+ return { x: x || [], y: y || [], type: 'scatter', mode: 'lines+markers', name: plot.title || plot.name || 'Line', line: { color }, marker: { color } };
517
+ case 'scatter':
518
+ return { x: x || [], y: y || [], type: 'scatter', mode: 'markers', name: plot.title || plot.name || 'Scatter', marker: { color, size: 8 } };
519
+ case 'bar':
520
+ return { x: x || plot.labels || [], y: y || plot.values || [], type: 'bar', name: plot.title || plot.name || 'Bar', marker: { color } };
521
+ case 'area':
522
+ return { x: x || [], y: y || [], type: 'scatter', mode: 'lines', fill: 'tozeroy', name: plot.title || plot.name || 'Area', line: { color } };
523
+ default:
524
+ return { x: x || [], y: y || [], type: plot.type || 'scatter', mode: plot.mode || 'lines+markers', name: plot.title || plot.name || 'Plot', line: { color }, marker: { color } };
525
+ }
526
+ }
527
+
528
+ function renderGraph2D(id, plots, config) {
529
+ const graphEl = document.getElementById(\`graph-\${id}\`);
530
+ if (!graphEl) return;
531
+
532
+ const traces = plots.map((plot, i) => {
533
+ const processed = processPlotData(plot);
534
+ if (!processed.name || processed.name === 'Line' || processed.name === 'Plot') processed.name = \`Series \${i + 1}\`;
535
+ return processed;
536
+ });
537
+
538
+ const layout = {
539
+ title: config.title || '',
540
+ xaxis: { title: config.x_label || 'x' },
541
+ yaxis: { title: config.y_label || 'y' },
542
+ paper_bgcolor: '#2a2a2a',
543
+ plot_bgcolor: '#2a2a2a',
544
+ font: { color: '#fff' },
545
+ margin: { t: 40, r: 20, b: 40, l: 50 },
546
+ };
547
+
548
+ Plotly.react(graphEl, traces, layout, { responsive: true });
549
+ }
550
+
551
+ function renderGraph3D(id, objects, config) {
552
+ const graphEl = document.getElementById(\`graph-\${id}\`);
553
+ if (!graphEl) return;
554
+
555
+ // Basic 3D scatter plot
556
+ const traces = objects.map((obj, i) => ({
557
+ x: obj.x || [],
558
+ y: obj.y || [],
559
+ z: obj.z || [],
560
+ name: obj.label || \`Object \${i + 1}\`,
561
+ type: 'scatter3d',
562
+ mode: 'markers',
563
+ marker: { size: 3 },
564
+ }));
565
+
566
+ const layout = {
567
+ title: config.title || '',
568
+ paper_bgcolor: '#2a2a2a',
569
+ scene: {
570
+ bgcolor: '#2a2a2a',
571
+ xaxis: { title: 'x', gridcolor: '#444' },
572
+ yaxis: { title: 'y', gridcolor: '#444' },
573
+ zaxis: { title: 'z', gridcolor: '#444' },
574
+ },
575
+ font: { color: '#fff' },
576
+ margin: { t: 40, r: 20, b: 20, l: 20 },
577
+ };
578
+
579
+ Plotly.react(graphEl, traces, layout, { responsive: true });
580
+ }
581
+
582
+ // Start connection
583
+ connect();
584
+ </script>
585
+ </body>
586
+ </html>`;
587
+ }
588
+ }
589
+
590
+ export default ViewerServer;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * check command - Type-check a Stone script without executing
3
+ *
4
+ * Usage:
5
+ * stone check <file.stn>
6
+ * stone check <file.stn> --json
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { StoneEngine } from '../../StoneEngine.js';
12
+
13
+ /**
14
+ * Type-check a Stone script
15
+ * @param {string} scriptPath - Path to script file
16
+ * @param {Object} options - Command options
17
+ */
18
+ export async function checkCommand(scriptPath, options = {}) {
19
+ const { json = false, verbose = false } = options;
20
+
21
+ // Resolve script path
22
+ const fullPath = path.isAbsolute(scriptPath)
23
+ ? scriptPath
24
+ : path.resolve(process.cwd(), scriptPath);
25
+
26
+ // Check file exists
27
+ if (!fs.existsSync(fullPath)) {
28
+ if (json) {
29
+ console.log(JSON.stringify({ valid: false, error: `Script not found: ${scriptPath}` }));
30
+ } else {
31
+ console.error(`Error: Script not found: ${scriptPath}`);
32
+ }
33
+ process.exit(1);
34
+ }
35
+
36
+ // Read source code
37
+ const source = fs.readFileSync(fullPath, 'utf8');
38
+
39
+ // Create engine
40
+ const engine = new StoneEngine({ debug: verbose });
41
+
42
+ try {
43
+ // Parse
44
+ const ast = engine.parse(source, { filename: scriptPath });
45
+
46
+ // Type check
47
+ const result = engine.typeCheck(ast);
48
+
49
+ if (json) {
50
+ console.log(JSON.stringify({
51
+ valid: result.success,
52
+ errors: result.success ? [] : result.errors.map(e => ({
53
+ message: e.message,
54
+ location: e.location,
55
+ })),
56
+ }));
57
+ } else {
58
+ if (result.success) {
59
+ console.log(`✓ ${scriptPath}: No type errors`);
60
+ } else {
61
+ console.error(`✗ ${scriptPath}: ${result.errors.length} type error(s)\n`);
62
+
63
+ for (const err of result.errors) {
64
+ const loc = err.location
65
+ ? `${scriptPath}:${err.location.line}:${err.location.column}`
66
+ : scriptPath;
67
+ console.error(` ${loc}`);
68
+ console.error(` ${err.message}\n`);
69
+ }
70
+ }
71
+ }
72
+
73
+ process.exit(result.success ? 0 : 1);
74
+ } catch (err) {
75
+ if (json) {
76
+ console.log(JSON.stringify({ valid: false, error: err.message }));
77
+ } else {
78
+ console.error(`Error: ${err.message}`);
79
+ }
80
+ process.exit(1);
81
+ }
82
+ }
83
+
84
+ export default checkCommand;