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,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the Stone compiler with all backends.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
compile,
|
|
9
|
+
compileToWasm,
|
|
10
|
+
compileToC,
|
|
11
|
+
compileToHIR,
|
|
12
|
+
CompilationTarget,
|
|
13
|
+
StoneCompilerOptions,
|
|
14
|
+
} from '../compiler.js';
|
|
15
|
+
import { OptimizationLevel } from '../optimization/optimization-manager.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// TEST UTILITIES
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
function test(name, fn) {
|
|
22
|
+
try {
|
|
23
|
+
fn();
|
|
24
|
+
console.log(`✓ ${name}`);
|
|
25
|
+
return true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`✗ ${name}`);
|
|
28
|
+
console.error(` ${error.message}`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assert(condition, message) {
|
|
34
|
+
if (!condition) {
|
|
35
|
+
throw new Error(message || 'Assertion failed');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// TESTS
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
const testSource = `
|
|
44
|
+
fn add(a, b) = a + b
|
|
45
|
+
fn mul(x, y) = x * y
|
|
46
|
+
result = add(3, 4) + mul(2, 5)
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const tests = [
|
|
50
|
+
// Basic compilation tests
|
|
51
|
+
() => test('compile() returns success', () => {
|
|
52
|
+
const result = compile(testSource);
|
|
53
|
+
assert(result.success, 'Compilation should succeed');
|
|
54
|
+
assert(result.output, 'Should have output');
|
|
55
|
+
assert(result.metadata, 'Should have metadata');
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
() => test('compile() returns timings', () => {
|
|
59
|
+
const result = compile(testSource);
|
|
60
|
+
assert(result.metadata.timings.parse !== undefined, 'Should have parse timing');
|
|
61
|
+
assert(result.metadata.timings.hir !== undefined, 'Should have hir timing');
|
|
62
|
+
assert(result.metadata.totalTime >= 0, 'Should have total time');
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
// Target-specific tests
|
|
66
|
+
() => test('compileToWasm() produces WASM binary', () => {
|
|
67
|
+
const result = compileToWasm(testSource);
|
|
68
|
+
assert(result.success, 'WASM compilation should succeed');
|
|
69
|
+
assert(result.output.binary instanceof Uint8Array, 'Should produce binary');
|
|
70
|
+
// Check WASM magic number
|
|
71
|
+
assert(result.output.binary[0] === 0x00, 'Should have WASM magic');
|
|
72
|
+
assert(result.output.binary[1] === 0x61, 'Should have WASM magic');
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
() => test('compileToC() produces C source', () => {
|
|
76
|
+
const result = compileToC(testSource);
|
|
77
|
+
assert(result.success, 'C compilation should succeed');
|
|
78
|
+
assert(typeof result.output.source === 'string', 'Should produce source');
|
|
79
|
+
assert(result.output.source.includes('#include'), 'Should have includes');
|
|
80
|
+
assert(result.output.source.includes('int main'), 'Should have main');
|
|
81
|
+
assert(result.output.makefile, 'Should have Makefile');
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
() => test('compileToHIR() produces HIR text', () => {
|
|
85
|
+
const result = compileToHIR(testSource);
|
|
86
|
+
assert(result.success, 'HIR compilation should succeed');
|
|
87
|
+
assert(typeof result.output.text === 'string', 'Should produce text');
|
|
88
|
+
assert(result.output.hir, 'Should have HIR module');
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
// Optimization level tests
|
|
92
|
+
() => test('O0 optimization level works', () => {
|
|
93
|
+
const result = compile(testSource, {
|
|
94
|
+
optimizationLevel: OptimizationLevel.NONE,
|
|
95
|
+
});
|
|
96
|
+
assert(result.success, 'Should compile with O0');
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
() => test('O3 optimization level works', () => {
|
|
100
|
+
const result = compile(testSource, {
|
|
101
|
+
optimizationLevel: OptimizationLevel.AGGRESSIVE,
|
|
102
|
+
});
|
|
103
|
+
assert(result.success, 'Should compile with O3');
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
// Error handling tests
|
|
107
|
+
() => test('compile() handles syntax errors', () => {
|
|
108
|
+
const badSource = 'fn broken( = )';
|
|
109
|
+
const result = compile(badSource);
|
|
110
|
+
assert(!result.success, 'Should fail on syntax error');
|
|
111
|
+
assert(result.error, 'Should have error info');
|
|
112
|
+
assert(result.error.stage === 'parse', 'Should fail at parse stage');
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
// Complex source tests
|
|
116
|
+
() => test('compiles functions with blocks', () => {
|
|
117
|
+
const source = `
|
|
118
|
+
fn compute(x) {
|
|
119
|
+
y = x * 2
|
|
120
|
+
z = y + 1
|
|
121
|
+
return z
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
const result = compile(source);
|
|
125
|
+
assert(result.success, 'Should compile block functions');
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
() => test('compiles conditionals', () => {
|
|
129
|
+
const source = `
|
|
130
|
+
fn max(a, b) = if (a > b) { a } else { b }
|
|
131
|
+
`;
|
|
132
|
+
const result = compile(source);
|
|
133
|
+
assert(result.success, 'Should compile conditionals');
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
// Options tests
|
|
137
|
+
() => test('printHIR option works', () => {
|
|
138
|
+
const result = compile(testSource, { printHIR: true });
|
|
139
|
+
assert(result.success, 'Should compile with printHIR');
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
() => test('different targets produce different outputs', () => {
|
|
143
|
+
const wasmResult = compileToWasm(testSource);
|
|
144
|
+
const cResult = compileToC(testSource);
|
|
145
|
+
const hirResult = compileToHIR(testSource);
|
|
146
|
+
|
|
147
|
+
assert(wasmResult.success, 'WASM should succeed');
|
|
148
|
+
assert(cResult.success, 'C should succeed');
|
|
149
|
+
assert(hirResult.success, 'HIR should succeed');
|
|
150
|
+
|
|
151
|
+
// Verify they're different
|
|
152
|
+
assert(wasmResult.output.binary, 'WASM has binary');
|
|
153
|
+
assert(cResult.output.source, 'C has source');
|
|
154
|
+
assert(hirResult.output.text, 'HIR has text');
|
|
155
|
+
}),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// RUN TESTS
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
console.log('Stone Compiler Integration Tests');
|
|
163
|
+
console.log('='.repeat(50));
|
|
164
|
+
console.log('');
|
|
165
|
+
|
|
166
|
+
let passed = 0;
|
|
167
|
+
let failed = 0;
|
|
168
|
+
|
|
169
|
+
for (const testFn of tests) {
|
|
170
|
+
if (testFn()) {
|
|
171
|
+
passed++;
|
|
172
|
+
} else {
|
|
173
|
+
failed++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log('='.repeat(50));
|
|
179
|
+
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
180
|
+
|
|
181
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Stone Viewer</title>
|
|
7
|
+
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
+
body {
|
|
11
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
12
|
+
background: #0d1117;
|
|
13
|
+
color: #e6edf3;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
}
|
|
16
|
+
header {
|
|
17
|
+
background: #161b22;
|
|
18
|
+
border-bottom: 1px solid #30363d;
|
|
19
|
+
padding: 12px 20px;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 12px;
|
|
23
|
+
}
|
|
24
|
+
header h1 {
|
|
25
|
+
font-size: 16px;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
color: #e6edf3;
|
|
28
|
+
}
|
|
29
|
+
header .session-id {
|
|
30
|
+
font-family: monospace;
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
color: #7d8590;
|
|
33
|
+
background: #21262d;
|
|
34
|
+
padding: 4px 8px;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
}
|
|
37
|
+
.container {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
|
40
|
+
gap: 20px;
|
|
41
|
+
padding: 20px;
|
|
42
|
+
}
|
|
43
|
+
.terminal {
|
|
44
|
+
background: #161b22;
|
|
45
|
+
border: 1px solid #30363d;
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
.terminal-header {
|
|
50
|
+
background: #21262d;
|
|
51
|
+
padding: 10px 14px;
|
|
52
|
+
border-bottom: 1px solid #30363d;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 8px;
|
|
56
|
+
}
|
|
57
|
+
.terminal-type {
|
|
58
|
+
font-size: 10px;
|
|
59
|
+
text-transform: uppercase;
|
|
60
|
+
letter-spacing: 0.5px;
|
|
61
|
+
color: #7d8590;
|
|
62
|
+
background: #30363d;
|
|
63
|
+
padding: 2px 6px;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
}
|
|
66
|
+
.terminal-title {
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
color: #e6edf3;
|
|
70
|
+
}
|
|
71
|
+
.terminal-body {
|
|
72
|
+
padding: 14px;
|
|
73
|
+
}
|
|
74
|
+
.console-lines {
|
|
75
|
+
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
|
76
|
+
font-size: 13px;
|
|
77
|
+
max-height: 400px;
|
|
78
|
+
overflow-y: auto;
|
|
79
|
+
line-height: 1.5;
|
|
80
|
+
}
|
|
81
|
+
.console-line {
|
|
82
|
+
padding: 1px 0;
|
|
83
|
+
color: #e6edf3;
|
|
84
|
+
}
|
|
85
|
+
.graph {
|
|
86
|
+
min-height: 350px;
|
|
87
|
+
}
|
|
88
|
+
.status {
|
|
89
|
+
position: fixed;
|
|
90
|
+
bottom: 16px;
|
|
91
|
+
right: 16px;
|
|
92
|
+
padding: 8px 14px;
|
|
93
|
+
background: #21262d;
|
|
94
|
+
border: 1px solid #30363d;
|
|
95
|
+
border-radius: 6px;
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
}
|
|
101
|
+
.status-dot {
|
|
102
|
+
width: 8px;
|
|
103
|
+
height: 8px;
|
|
104
|
+
border-radius: 50%;
|
|
105
|
+
background: #7d8590;
|
|
106
|
+
}
|
|
107
|
+
.status.connected .status-dot { background: #3fb950; }
|
|
108
|
+
.status.disconnected .status-dot { background: #f85149; }
|
|
109
|
+
.status.connecting .status-dot { background: #d29922; animation: pulse 1s infinite; }
|
|
110
|
+
@keyframes pulse {
|
|
111
|
+
0%, 100% { opacity: 1; }
|
|
112
|
+
50% { opacity: 0.5; }
|
|
113
|
+
}
|
|
114
|
+
.empty-state {
|
|
115
|
+
text-align: center;
|
|
116
|
+
padding: 60px 20px;
|
|
117
|
+
color: #7d8590;
|
|
118
|
+
}
|
|
119
|
+
.empty-state h2 {
|
|
120
|
+
font-size: 18px;
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
margin-bottom: 8px;
|
|
123
|
+
color: #e6edf3;
|
|
124
|
+
}
|
|
125
|
+
.empty-state p {
|
|
126
|
+
font-size: 14px;
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
<header>
|
|
132
|
+
<h1>Stone Viewer</h1>
|
|
133
|
+
<span class="session-id" id="session-id"></span>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
<div class="container" id="terminals">
|
|
137
|
+
<div class="empty-state" id="empty-state">
|
|
138
|
+
<h2>Waiting for output...</h2>
|
|
139
|
+
<p>Terminals will appear here as they are created by your script.</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class="status connecting" id="status">
|
|
144
|
+
<div class="status-dot"></div>
|
|
145
|
+
<span id="status-text">Connecting...</span>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<script>
|
|
149
|
+
// Extract session ID from URL
|
|
150
|
+
const sessionId = window.location.pathname.split('/')[2];
|
|
151
|
+
document.getElementById('session-id').textContent = sessionId;
|
|
152
|
+
|
|
153
|
+
const container = document.getElementById('terminals');
|
|
154
|
+
const emptyState = document.getElementById('empty-state');
|
|
155
|
+
const statusEl = document.getElementById('status');
|
|
156
|
+
const statusText = document.getElementById('status-text');
|
|
157
|
+
let terminals = {};
|
|
158
|
+
let hasTerminals = false;
|
|
159
|
+
|
|
160
|
+
// WebSocket connection
|
|
161
|
+
const wsUrl = `ws://${location.host}/ws/${sessionId}`;
|
|
162
|
+
let ws;
|
|
163
|
+
let reconnectAttempts = 0;
|
|
164
|
+
const maxReconnectAttempts = 10;
|
|
165
|
+
|
|
166
|
+
function connect() {
|
|
167
|
+
ws = new WebSocket(wsUrl);
|
|
168
|
+
|
|
169
|
+
ws.onopen = () => {
|
|
170
|
+
reconnectAttempts = 0;
|
|
171
|
+
statusEl.className = 'status connected';
|
|
172
|
+
statusText.textContent = 'Connected';
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
ws.onclose = () => {
|
|
176
|
+
statusEl.className = 'status disconnected';
|
|
177
|
+
statusText.textContent = 'Disconnected';
|
|
178
|
+
|
|
179
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
180
|
+
reconnectAttempts++;
|
|
181
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), 10000);
|
|
182
|
+
statusText.textContent = `Reconnecting in ${delay/1000}s...`;
|
|
183
|
+
setTimeout(connect, delay);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
ws.onerror = () => {
|
|
188
|
+
statusEl.className = 'status disconnected';
|
|
189
|
+
statusText.textContent = 'Connection error';
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
ws.onmessage = (e) => {
|
|
193
|
+
const msg = JSON.parse(e.data);
|
|
194
|
+
handleMessage(msg);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleMessage(msg) {
|
|
199
|
+
switch (msg.type) {
|
|
200
|
+
case 'init':
|
|
201
|
+
for (const [id, data] of Object.entries(msg.terminals || {})) {
|
|
202
|
+
createTerminal(id, data.type, data.config);
|
|
203
|
+
if (data.data && data.data.length > 0) {
|
|
204
|
+
updateTerminal(id, data.type, data.data, false);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
case 'terminal:create':
|
|
210
|
+
createTerminal(msg.id, msg.terminalType, msg.config);
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'terminal:add':
|
|
214
|
+
updateTerminal(msg.id, terminals[msg.id]?.type, msg.data, true);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case 'terminal:set':
|
|
218
|
+
updateTerminal(msg.id, terminals[msg.id]?.type, msg.data, false);
|
|
219
|
+
break;
|
|
220
|
+
|
|
221
|
+
case 'terminal:clear':
|
|
222
|
+
clearTerminal(msg.id);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function createTerminal(id, type, config) {
|
|
228
|
+
if (terminals[id]) return;
|
|
229
|
+
|
|
230
|
+
// Hide empty state
|
|
231
|
+
if (!hasTerminals) {
|
|
232
|
+
hasTerminals = true;
|
|
233
|
+
emptyState.style.display = 'none';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const div = document.createElement('div');
|
|
237
|
+
div.className = 'terminal';
|
|
238
|
+
div.id = `terminal-${id}`;
|
|
239
|
+
|
|
240
|
+
const header = document.createElement('div');
|
|
241
|
+
header.className = 'terminal-header';
|
|
242
|
+
|
|
243
|
+
const typeLabel = document.createElement('span');
|
|
244
|
+
typeLabel.className = 'terminal-type';
|
|
245
|
+
typeLabel.textContent = type;
|
|
246
|
+
header.appendChild(typeLabel);
|
|
247
|
+
|
|
248
|
+
const title = document.createElement('span');
|
|
249
|
+
title.className = 'terminal-title';
|
|
250
|
+
title.textContent = config.title || id;
|
|
251
|
+
header.appendChild(title);
|
|
252
|
+
|
|
253
|
+
div.appendChild(header);
|
|
254
|
+
|
|
255
|
+
const body = document.createElement('div');
|
|
256
|
+
body.className = 'terminal-body';
|
|
257
|
+
|
|
258
|
+
if (type === 'console') {
|
|
259
|
+
const lines = document.createElement('div');
|
|
260
|
+
lines.className = 'console-lines';
|
|
261
|
+
lines.id = `lines-${id}`;
|
|
262
|
+
body.appendChild(lines);
|
|
263
|
+
} else {
|
|
264
|
+
const graph = document.createElement('div');
|
|
265
|
+
graph.className = 'graph';
|
|
266
|
+
graph.id = `graph-${id}`;
|
|
267
|
+
body.appendChild(graph);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
div.appendChild(body);
|
|
271
|
+
container.appendChild(div);
|
|
272
|
+
terminals[id] = { type, config, element: div, data: [] };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function updateTerminal(id, type, data, append) {
|
|
276
|
+
const terminal = terminals[id];
|
|
277
|
+
if (!terminal) return;
|
|
278
|
+
|
|
279
|
+
const items = Array.isArray(data) ? data : [data];
|
|
280
|
+
|
|
281
|
+
if (append) {
|
|
282
|
+
terminal.data.push(...items);
|
|
283
|
+
} else {
|
|
284
|
+
terminal.data = items;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (type === 'console') {
|
|
288
|
+
renderConsole(id, terminal.data, append);
|
|
289
|
+
} else if (type === 'graph2d') {
|
|
290
|
+
renderGraph2D(id, terminal.data, terminal.config);
|
|
291
|
+
} else if (type === 'graph3d') {
|
|
292
|
+
renderGraph3D(id, terminal.data, terminal.config);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function clearTerminal(id) {
|
|
297
|
+
const terminal = terminals[id];
|
|
298
|
+
if (!terminal) return;
|
|
299
|
+
|
|
300
|
+
terminal.data = [];
|
|
301
|
+
|
|
302
|
+
if (terminal.type === 'console') {
|
|
303
|
+
const linesEl = document.getElementById(`lines-${id}`);
|
|
304
|
+
if (linesEl) linesEl.innerHTML = '';
|
|
305
|
+
} else {
|
|
306
|
+
const graphEl = document.getElementById(`graph-${id}`);
|
|
307
|
+
if (graphEl) Plotly.purge(graphEl);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderConsole(id, lines, append) {
|
|
312
|
+
const linesEl = document.getElementById(`lines-${id}`);
|
|
313
|
+
if (!linesEl) return;
|
|
314
|
+
|
|
315
|
+
if (!append) linesEl.innerHTML = '';
|
|
316
|
+
|
|
317
|
+
const itemsToRender = append ? lines.slice(-lines.length) : lines;
|
|
318
|
+
for (const item of itemsToRender) {
|
|
319
|
+
const line = document.createElement('div');
|
|
320
|
+
line.className = 'console-line';
|
|
321
|
+
line.textContent = String(item);
|
|
322
|
+
linesEl.appendChild(line);
|
|
323
|
+
}
|
|
324
|
+
linesEl.scrollTop = linesEl.scrollHeight;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Convert StoneArray to regular JS array
|
|
328
|
+
function stoneArrayToJS(arr) {
|
|
329
|
+
if (!arr || arr._type !== 'StoneArray') return arr;
|
|
330
|
+
const data = arr.data;
|
|
331
|
+
// Handle both typed array (ArrayBuffer) and serialized object format
|
|
332
|
+
if (ArrayBuffer.isView(data)) {
|
|
333
|
+
return Array.from(data);
|
|
334
|
+
} else if (Array.isArray(data)) {
|
|
335
|
+
return data;
|
|
336
|
+
} else if (typeof data === 'object') {
|
|
337
|
+
// Serialized JSON format: {"0": val, "1": val, ...}
|
|
338
|
+
const size = arr.size || Object.keys(data).length;
|
|
339
|
+
const result = [];
|
|
340
|
+
for (let i = 0; i < size; i++) {
|
|
341
|
+
result.push(data[i]);
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Process Stone plot data to Plotly format
|
|
349
|
+
function processPlotData(plot) {
|
|
350
|
+
const type = plot.type || 'line';
|
|
351
|
+
|
|
352
|
+
// Extract x/y data - handle y-only case by auto-generating x
|
|
353
|
+
let x = plot.x;
|
|
354
|
+
let y = plot.y;
|
|
355
|
+
|
|
356
|
+
// Handle StoneArray format first
|
|
357
|
+
if (x && x._type === 'StoneArray') {
|
|
358
|
+
x = stoneArrayToJS(x);
|
|
359
|
+
}
|
|
360
|
+
if (y && y._type === 'StoneArray') {
|
|
361
|
+
y = stoneArrayToJS(y);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// If only y is provided, generate x as indices
|
|
365
|
+
if (y && !x) {
|
|
366
|
+
const yArr = Array.isArray(y) ? y : [];
|
|
367
|
+
x = yArr.map((_, i) => i);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Convert Stone types to Plotly types
|
|
371
|
+
const color = plot.style?.color || plot.color || null;
|
|
372
|
+
|
|
373
|
+
switch (type) {
|
|
374
|
+
case 'line':
|
|
375
|
+
return {
|
|
376
|
+
x: x || [],
|
|
377
|
+
y: y || [],
|
|
378
|
+
type: 'scatter',
|
|
379
|
+
mode: 'lines+markers',
|
|
380
|
+
name: plot.title || plot.name || plot.label || 'Line',
|
|
381
|
+
line: { color: color, width: plot.style?.lineWidth || 2 },
|
|
382
|
+
marker: { color: color, size: plot.style?.markerSize || 5 }
|
|
383
|
+
};
|
|
384
|
+
case 'scatter':
|
|
385
|
+
return {
|
|
386
|
+
x: x || [],
|
|
387
|
+
y: y || [],
|
|
388
|
+
type: 'scatter',
|
|
389
|
+
mode: 'markers',
|
|
390
|
+
name: plot.title || plot.name || plot.label || 'Scatter',
|
|
391
|
+
marker: { color: color, size: plot.style?.markerSize || 8 }
|
|
392
|
+
};
|
|
393
|
+
case 'bar':
|
|
394
|
+
return {
|
|
395
|
+
x: x || plot.labels || [],
|
|
396
|
+
y: y || plot.values || [],
|
|
397
|
+
type: 'bar',
|
|
398
|
+
name: plot.title || plot.name || plot.label || 'Bar',
|
|
399
|
+
marker: { color: color }
|
|
400
|
+
};
|
|
401
|
+
case 'area':
|
|
402
|
+
return {
|
|
403
|
+
x: x || [],
|
|
404
|
+
y: y || [],
|
|
405
|
+
type: 'scatter',
|
|
406
|
+
mode: 'lines',
|
|
407
|
+
fill: 'tozeroy',
|
|
408
|
+
name: plot.title || plot.name || plot.label || 'Area',
|
|
409
|
+
line: { color: color }
|
|
410
|
+
};
|
|
411
|
+
default:
|
|
412
|
+
// Pass through for already-Plotly-formatted data
|
|
413
|
+
return {
|
|
414
|
+
x: x || [],
|
|
415
|
+
y: y || [],
|
|
416
|
+
type: plot.type || 'scatter',
|
|
417
|
+
mode: plot.mode || 'lines+markers',
|
|
418
|
+
name: plot.title || plot.name || plot.label || 'Plot',
|
|
419
|
+
line: plot.line || { color: color },
|
|
420
|
+
marker: plot.marker || { color: color }
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function renderGraph2D(id, plots, config) {
|
|
426
|
+
const graphEl = document.getElementById(`graph-${id}`);
|
|
427
|
+
if (!graphEl) return;
|
|
428
|
+
|
|
429
|
+
const traces = plots.map((plot, i) => {
|
|
430
|
+
const processed = processPlotData(plot);
|
|
431
|
+
if (!processed.name || processed.name === 'Line' || processed.name === 'Plot') {
|
|
432
|
+
processed.name = `Series ${i + 1}`;
|
|
433
|
+
}
|
|
434
|
+
return processed;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const layout = {
|
|
438
|
+
title: { text: config.title || '', font: { color: '#e6edf3', size: 14 } },
|
|
439
|
+
xaxis: {
|
|
440
|
+
title: config.x_label || 'x',
|
|
441
|
+
color: '#7d8590',
|
|
442
|
+
gridcolor: '#30363d',
|
|
443
|
+
zerolinecolor: '#30363d',
|
|
444
|
+
},
|
|
445
|
+
yaxis: {
|
|
446
|
+
title: config.y_label || 'y',
|
|
447
|
+
color: '#7d8590',
|
|
448
|
+
gridcolor: '#30363d',
|
|
449
|
+
zerolinecolor: '#30363d',
|
|
450
|
+
},
|
|
451
|
+
paper_bgcolor: '#161b22',
|
|
452
|
+
plot_bgcolor: '#161b22',
|
|
453
|
+
font: { color: '#e6edf3' },
|
|
454
|
+
margin: { t: 50, r: 30, b: 50, l: 60 },
|
|
455
|
+
legend: { font: { color: '#e6edf3' } },
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
Plotly.react(graphEl, traces, layout, { responsive: true });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderGraph3D(id, objects, config) {
|
|
462
|
+
const graphEl = document.getElementById(`graph-${id}`);
|
|
463
|
+
if (!graphEl) return;
|
|
464
|
+
|
|
465
|
+
const traces = objects.map((obj, i) => ({
|
|
466
|
+
x: obj.x || [],
|
|
467
|
+
y: obj.y || [],
|
|
468
|
+
z: obj.z || [],
|
|
469
|
+
name: obj.label || `Object ${i + 1}`,
|
|
470
|
+
type: 'scatter3d',
|
|
471
|
+
mode: obj.mode || 'markers',
|
|
472
|
+
marker: { size: obj.size || 3, color: obj.color },
|
|
473
|
+
}));
|
|
474
|
+
|
|
475
|
+
const layout = {
|
|
476
|
+
title: { text: config.title || '', font: { color: '#e6edf3', size: 14 } },
|
|
477
|
+
paper_bgcolor: '#161b22',
|
|
478
|
+
scene: {
|
|
479
|
+
bgcolor: '#161b22',
|
|
480
|
+
xaxis: { title: 'x', gridcolor: '#30363d', color: '#7d8590' },
|
|
481
|
+
yaxis: { title: 'y', gridcolor: '#30363d', color: '#7d8590' },
|
|
482
|
+
zaxis: { title: 'z', gridcolor: '#30363d', color: '#7d8590' },
|
|
483
|
+
},
|
|
484
|
+
font: { color: '#e6edf3' },
|
|
485
|
+
margin: { t: 50, r: 30, b: 30, l: 30 },
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
Plotly.react(graphEl, traces, layout, { responsive: true });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Start connection
|
|
492
|
+
connect();
|
|
493
|
+
</script>
|
|
494
|
+
</body>
|
|
495
|
+
</html>
|