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,327 @@
1
+ /**
2
+ * ProcessManager - Manages Stone script execution processes
3
+ *
4
+ * Tracks running scripts, their owners, and provides lifecycle management.
5
+ * When a client disconnects, all its owned processes are killed.
6
+ *
7
+ * Process states:
8
+ * - pending: Waiting to start
9
+ * - running: Currently executing
10
+ * - completed: Finished successfully
11
+ * - failed: Finished with error
12
+ * - killed: Terminated by user
13
+ */
14
+
15
+ import { EventEmitter } from 'events';
16
+
17
+ /**
18
+ * Process entry in the process table
19
+ */
20
+ class StoneProcess {
21
+ constructor(id, script, owner, options = {}) {
22
+ this.id = id;
23
+ this.script = script; // Script path or source
24
+ this.owner = owner; // Client ID that started this process
25
+ this.status = 'pending';
26
+ this.startTime = null;
27
+ this.endTime = null;
28
+ this.result = null;
29
+ this.error = null;
30
+ this.terminalsData = {};
31
+
32
+ // Execution options
33
+ this.cwd = options.cwd || process.cwd();
34
+ this.variables = options.variables || {};
35
+ this.maxIterations = options.maxIterations || 100000;
36
+
37
+ // Runner reference (set when execution starts)
38
+ this.runner = null;
39
+
40
+ // Kill signal
41
+ this._killRequested = false;
42
+ }
43
+
44
+ toJSON() {
45
+ return {
46
+ id: this.id,
47
+ script: this.script,
48
+ owner: this.owner,
49
+ status: this.status,
50
+ startTime: this.startTime,
51
+ endTime: this.endTime,
52
+ duration: this.getDuration(),
53
+ };
54
+ }
55
+
56
+ getDuration() {
57
+ if (!this.startTime) return 0;
58
+ const end = this.endTime || Date.now();
59
+ return end - this.startTime;
60
+ }
61
+
62
+ requestKill() {
63
+ this._killRequested = true;
64
+ if (this.runner && typeof this.runner.kill === 'function') {
65
+ this.runner.kill();
66
+ }
67
+ }
68
+
69
+ isKillRequested() {
70
+ return this._killRequested;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * ProcessManager - Central process table and lifecycle management
76
+ */
77
+ export class ProcessManager extends EventEmitter {
78
+ constructor() {
79
+ super();
80
+ this.processes = new Map(); // processId -> StoneProcess
81
+ this.processCounter = 0;
82
+
83
+ // Owner tracking: clientId -> Set of processIds
84
+ this.ownerProcesses = new Map();
85
+ }
86
+
87
+ /**
88
+ * Create a new process entry
89
+ * @param {string} script - Script path or source
90
+ * @param {string} owner - Client ID that owns this process
91
+ * @param {Object} options - Execution options
92
+ * @returns {StoneProcess} The created process
93
+ */
94
+ createProcess(script, owner, options = {}) {
95
+ const id = `p${++this.processCounter}`;
96
+ const process = new StoneProcess(id, script, owner, options);
97
+ this.processes.set(id, process);
98
+
99
+ // Track owner
100
+ if (!this.ownerProcesses.has(owner)) {
101
+ this.ownerProcesses.set(owner, new Set());
102
+ }
103
+ this.ownerProcesses.get(owner).add(id);
104
+
105
+ this.emit('process:created', process);
106
+ return process;
107
+ }
108
+
109
+ /**
110
+ * Start a process
111
+ * @param {string} processId - Process ID
112
+ */
113
+ startProcess(processId) {
114
+ const process = this.processes.get(processId);
115
+ if (!process) {
116
+ throw new Error(`Process not found: ${processId}`);
117
+ }
118
+ if (process.status !== 'pending') {
119
+ throw new Error(`Process ${processId} is not in pending state`);
120
+ }
121
+
122
+ process.status = 'running';
123
+ process.startTime = Date.now();
124
+ this.emit('process:started', process);
125
+ }
126
+
127
+ /**
128
+ * Complete a process successfully
129
+ * @param {string} processId - Process ID
130
+ * @param {any} result - Execution result
131
+ * @param {Object} terminalsData - Terminal data
132
+ */
133
+ completeProcess(processId, result, terminalsData = {}) {
134
+ const process = this.processes.get(processId);
135
+ if (!process) return;
136
+
137
+ process.status = 'completed';
138
+ process.endTime = Date.now();
139
+ process.result = result;
140
+ process.terminalsData = terminalsData;
141
+
142
+ this.emit('process:completed', process);
143
+ }
144
+
145
+ /**
146
+ * Fail a process
147
+ * @param {string} processId - Process ID
148
+ * @param {Error|string} error - Error that caused failure
149
+ */
150
+ failProcess(processId, error) {
151
+ const process = this.processes.get(processId);
152
+ if (!process) return;
153
+
154
+ process.status = 'failed';
155
+ process.endTime = Date.now();
156
+ process.error = error instanceof Error ? error.message : error;
157
+
158
+ this.emit('process:failed', process);
159
+ }
160
+
161
+ /**
162
+ * Kill a process
163
+ * @param {string} processId - Process ID
164
+ * @param {string} requestedBy - Client ID requesting the kill (for auth)
165
+ * @returns {boolean} Whether the kill was successful
166
+ */
167
+ killProcess(processId, requestedBy = null) {
168
+ const process = this.processes.get(processId);
169
+ if (!process) {
170
+ return false;
171
+ }
172
+
173
+ // Check authorization (only owner can kill, unless admin)
174
+ if (requestedBy && process.owner !== requestedBy) {
175
+ throw new Error(`Not authorized to kill process ${processId}`);
176
+ }
177
+
178
+ // Only kill running processes
179
+ if (process.status !== 'running') {
180
+ return false;
181
+ }
182
+
183
+ process.requestKill();
184
+ process.status = 'killed';
185
+ process.endTime = Date.now();
186
+
187
+ this.emit('process:killed', process);
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * Get a process by ID
193
+ * @param {string} processId - Process ID
194
+ * @returns {StoneProcess|null}
195
+ */
196
+ getProcess(processId) {
197
+ return this.processes.get(processId) || null;
198
+ }
199
+
200
+ /**
201
+ * List all processes
202
+ * @param {Object} filter - Optional filter { status, owner }
203
+ * @returns {StoneProcess[]}
204
+ */
205
+ listProcesses(filter = {}) {
206
+ let result = Array.from(this.processes.values());
207
+
208
+ if (filter.status) {
209
+ result = result.filter(p => p.status === filter.status);
210
+ }
211
+
212
+ if (filter.owner) {
213
+ result = result.filter(p => p.owner === filter.owner);
214
+ }
215
+
216
+ return result;
217
+ }
218
+
219
+ /**
220
+ * Get processes owned by a client
221
+ * @param {string} owner - Client ID
222
+ * @returns {StoneProcess[]}
223
+ */
224
+ getProcessesByOwner(owner) {
225
+ const processIds = this.ownerProcesses.get(owner) || new Set();
226
+ return Array.from(processIds).map(id => this.processes.get(id)).filter(Boolean);
227
+ }
228
+
229
+ /**
230
+ * Kill all processes owned by a client
231
+ * Called when a client disconnects
232
+ * @param {string} owner - Client ID
233
+ * @returns {string[]} IDs of killed processes
234
+ */
235
+ killProcessesByOwner(owner) {
236
+ const killed = [];
237
+ const processIds = this.ownerProcesses.get(owner) || new Set();
238
+
239
+ for (const processId of processIds) {
240
+ const process = this.processes.get(processId);
241
+ if (process && process.status === 'running') {
242
+ this.killProcess(processId);
243
+ killed.push(processId);
244
+ }
245
+ }
246
+
247
+ return killed;
248
+ }
249
+
250
+ /**
251
+ * Clean up a process entry
252
+ * Called after process completes/fails and client has retrieved result
253
+ * @param {string} processId - Process ID
254
+ */
255
+ removeProcess(processId) {
256
+ const process = this.processes.get(processId);
257
+ if (!process) return;
258
+
259
+ // Remove from owner tracking
260
+ const ownerSet = this.ownerProcesses.get(process.owner);
261
+ if (ownerSet) {
262
+ ownerSet.delete(processId);
263
+ if (ownerSet.size === 0) {
264
+ this.ownerProcesses.delete(process.owner);
265
+ }
266
+ }
267
+
268
+ this.processes.delete(processId);
269
+ this.emit('process:removed', process);
270
+ }
271
+
272
+ /**
273
+ * Clean up old completed/failed processes
274
+ * @param {number} maxAge - Max age in ms (default 1 hour)
275
+ */
276
+ cleanupOldProcesses(maxAge = 3600000) {
277
+ const now = Date.now();
278
+ const toRemove = [];
279
+
280
+ for (const [id, process] of this.processes.entries()) {
281
+ if (process.status === 'completed' ||
282
+ process.status === 'failed' ||
283
+ process.status === 'killed') {
284
+ if (process.endTime && (now - process.endTime) > maxAge) {
285
+ toRemove.push(id);
286
+ }
287
+ }
288
+ }
289
+
290
+ for (const id of toRemove) {
291
+ this.removeProcess(id);
292
+ }
293
+
294
+ return toRemove.length;
295
+ }
296
+
297
+ /**
298
+ * Get process count by status
299
+ * @returns {Object} { pending, running, completed, failed, killed }
300
+ */
301
+ getProcessStats() {
302
+ const stats = { pending: 0, running: 0, completed: 0, failed: 0, killed: 0, total: 0 };
303
+
304
+ for (const process of this.processes.values()) {
305
+ stats[process.status] = (stats[process.status] || 0) + 1;
306
+ stats.total++;
307
+ }
308
+
309
+ return stats;
310
+ }
311
+
312
+ /**
313
+ * Check if there are any running processes
314
+ * @returns {boolean}
315
+ */
316
+ hasRunningProcesses() {
317
+ for (const process of this.processes.values()) {
318
+ if (process.status === 'running') {
319
+ return true;
320
+ }
321
+ }
322
+ return false;
323
+ }
324
+ }
325
+
326
+ export { StoneProcess };
327
+ export default ProcessManager;
@@ -0,0 +1,307 @@
1
+ /**
2
+ * ProcessRunner - Executes Stone scripts and streams events
3
+ *
4
+ * Uses the StoneEngine with a streaming OutputAdapter to send
5
+ * real-time updates to connected clients via the IPC server.
6
+ *
7
+ * Events emitted:
8
+ * - terminal:create - New terminal created
9
+ * - terminal:add - Data added to terminal
10
+ * - terminal:set - Terminal data replaced
11
+ * - terminal:clear - Terminal cleared
12
+ * - print - Print output
13
+ * - error - Execution error
14
+ * - complete - Execution completed
15
+ */
16
+
17
+ import { EventEmitter } from 'events';
18
+ import { OutputAdapter } from '../adapters/OutputAdapter.js';
19
+ import { StoneEngine } from '../StoneEngine.js';
20
+ import { createNodePackageContext } from '../packages/packageResolver.js';
21
+ import fs from 'fs';
22
+ import path from 'path';
23
+
24
+ /**
25
+ * StreamingOutputAdapter - Streams output events to the process runner
26
+ */
27
+ class StreamingOutputAdapter extends OutputAdapter {
28
+ constructor(processId, eventEmitter) {
29
+ super();
30
+ this.processId = processId;
31
+ this.emitter = eventEmitter;
32
+ this.terminalsData = {}; // Local copy for backward compat
33
+ }
34
+
35
+ print(text) {
36
+ this.emitter.emit('event', {
37
+ event: 'print',
38
+ processId: this.processId,
39
+ value: text,
40
+ });
41
+ }
42
+
43
+ createTerminal(id, type, config) {
44
+ this.terminalsData[id] = {
45
+ type,
46
+ config,
47
+ plots: type === 'graph2d' ? [] : undefined,
48
+ objects: type === 'graph3d' ? [] : undefined,
49
+ lines: type === 'console' ? [] : undefined,
50
+ };
51
+
52
+ this.emitter.emit('event', {
53
+ event: 'terminal:create',
54
+ processId: this.processId,
55
+ id,
56
+ type,
57
+ terminalType: type, // Alias for renderer compatibility
58
+ config,
59
+ });
60
+ }
61
+
62
+ addToTerminal(id, data) {
63
+ const terminal = this.terminalsData[id];
64
+ if (terminal) {
65
+ const items = Array.isArray(data) ? data : [data];
66
+ switch (terminal.type) {
67
+ case 'graph2d':
68
+ terminal.plots.push(...items);
69
+ break;
70
+ case 'graph3d':
71
+ terminal.objects.push(...items);
72
+ break;
73
+ case 'console':
74
+ terminal.lines.push(...items);
75
+ break;
76
+ }
77
+ }
78
+
79
+ this.emitter.emit('event', {
80
+ event: 'terminal:add',
81
+ processId: this.processId,
82
+ id,
83
+ data,
84
+ });
85
+ }
86
+
87
+ setTerminal(id, data) {
88
+ const terminal = this.terminalsData[id];
89
+ if (terminal) {
90
+ const items = Array.isArray(data) ? data : [data];
91
+ switch (terminal.type) {
92
+ case 'graph2d':
93
+ terminal.plots = items;
94
+ break;
95
+ case 'graph3d':
96
+ terminal.objects = items;
97
+ break;
98
+ case 'console':
99
+ terminal.lines = items;
100
+ break;
101
+ }
102
+ }
103
+
104
+ this.emitter.emit('event', {
105
+ event: 'terminal:set',
106
+ processId: this.processId,
107
+ id,
108
+ data,
109
+ });
110
+ }
111
+
112
+ clearTerminal(id) {
113
+ const terminal = this.terminalsData[id];
114
+ if (terminal) {
115
+ switch (terminal.type) {
116
+ case 'graph2d':
117
+ terminal.plots = [];
118
+ break;
119
+ case 'graph3d':
120
+ terminal.objects = [];
121
+ break;
122
+ case 'console':
123
+ terminal.lines = [];
124
+ break;
125
+ }
126
+ }
127
+
128
+ this.emitter.emit('event', {
129
+ event: 'terminal:clear',
130
+ processId: this.processId,
131
+ id,
132
+ });
133
+ }
134
+
135
+ error(message, location) {
136
+ this.emitter.emit('event', {
137
+ event: 'error',
138
+ processId: this.processId,
139
+ message,
140
+ location,
141
+ });
142
+ }
143
+
144
+ finish(success, result) {
145
+ // Don't emit here - ProcessRunner handles completion
146
+ }
147
+
148
+ getTerminalsData() {
149
+ return this.terminalsData;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * ProcessRunner - Runs a Stone script and streams output
155
+ */
156
+ export class ProcessRunner extends EventEmitter {
157
+ constructor(stoneProcess, options = {}) {
158
+ super();
159
+ this.process = stoneProcess;
160
+ this.engine = null;
161
+ this.adapter = null;
162
+ this._killed = false;
163
+
164
+ // Options
165
+ this.scriptPath = options.scriptPath || null;
166
+ this.source = options.source || null;
167
+ this.cwd = options.cwd || process.cwd();
168
+ }
169
+
170
+ /**
171
+ * Run the script
172
+ * @returns {Promise<Object>} Execution result
173
+ */
174
+ async run() {
175
+ if (this._killed) {
176
+ return { success: false, error: 'Process was killed before starting' };
177
+ }
178
+
179
+ try {
180
+ // Create streaming adapter
181
+ this.adapter = new StreamingOutputAdapter(this.process.id, this);
182
+
183
+ // Create engine with adapter
184
+ this.engine = new StoneEngine({
185
+ outputAdapter: this.adapter,
186
+ debug: false,
187
+ });
188
+
189
+ // Set up package context for stone_modules resolution
190
+ try {
191
+ const packageContext = await createNodePackageContext();
192
+ this.engine.setPackageContext(packageContext);
193
+ } catch (e) {
194
+ // Package context not available, continue without it
195
+ }
196
+
197
+ // Load source code
198
+ let source = this.source;
199
+ let filename = '<stdin>';
200
+
201
+ if (!source && this.scriptPath) {
202
+ // Read from file
203
+ const fullPath = path.isAbsolute(this.scriptPath)
204
+ ? this.scriptPath
205
+ : path.join(this.cwd, this.scriptPath);
206
+
207
+ if (!fs.existsSync(fullPath)) {
208
+ throw new Error(`Script not found: ${fullPath}`);
209
+ }
210
+
211
+ source = fs.readFileSync(fullPath, 'utf8');
212
+ filename = this.scriptPath;
213
+ }
214
+
215
+ if (!source) {
216
+ throw new Error('No script source provided');
217
+ }
218
+
219
+ // Emit start event
220
+ this.emit('event', {
221
+ event: 'process:start',
222
+ processId: this.process.id,
223
+ script: filename,
224
+ });
225
+
226
+ // Check for kill before expensive operations
227
+ if (this._killed) {
228
+ return { success: false, error: 'Process was killed' };
229
+ }
230
+
231
+ // Execute
232
+ const result = await this.engine.execute(source, {
233
+ filename,
234
+ currentPath: this.scriptPath ? path.dirname(path.resolve(this.cwd, this.scriptPath)) : this.cwd,
235
+ variables: this.process.variables || {},
236
+ maxIterations: this.process.maxIterations || 100000,
237
+ });
238
+
239
+ // Emit completion
240
+ this.emit('event', {
241
+ event: 'process:end',
242
+ processId: this.process.id,
243
+ success: result.success,
244
+ result: result.result,
245
+ error: result.error || null,
246
+ duration: result.duration,
247
+ });
248
+
249
+ return {
250
+ success: result.success,
251
+ result: result.result,
252
+ error: result.error,
253
+ terminalsData: this.adapter.getTerminalsData(),
254
+ duration: result.duration,
255
+ };
256
+ } catch (error) {
257
+ // Emit error event
258
+ this.emit('event', {
259
+ event: 'process:end',
260
+ processId: this.process.id,
261
+ success: false,
262
+ error: error.message,
263
+ });
264
+
265
+ return {
266
+ success: false,
267
+ error: error.message,
268
+ terminalsData: this.adapter ? this.adapter.getTerminalsData() : {},
269
+ };
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Kill the running script
275
+ */
276
+ kill() {
277
+ this._killed = true;
278
+ // The VM/executor will check for kill flag
279
+ // In the future, could use AbortController pattern
280
+ }
281
+
282
+ /**
283
+ * Check if process was killed
284
+ * @returns {boolean}
285
+ */
286
+ isKilled() {
287
+ return this._killed;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Create and run a process
293
+ * @param {StoneProcess} stoneProcess - Process entry from ProcessManager
294
+ * @param {Object} options - Runner options
295
+ * @returns {Promise<Object>} Execution result
296
+ */
297
+ export async function runProcess(stoneProcess, options = {}) {
298
+ const runner = new ProcessRunner(stoneProcess, options);
299
+
300
+ // Link runner to process for kill support
301
+ stoneProcess.runner = runner;
302
+
303
+ return runner.run();
304
+ }
305
+
306
+ export { StreamingOutputAdapter };
307
+ export default ProcessRunner;