stone-lang 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- package/packages/semver.js +635 -0
|
@@ -0,0 +1,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;
|