@shapeshift-labs/frontier-lang-compiler 0.2.55 → 0.2.56

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 CHANGED
@@ -26,6 +26,18 @@ npm run build
26
26
  node examples/native-js-to-rust-demo.mjs
27
27
  ```
28
28
 
29
+ Run the interactive Frontier-style workbench with editable TypeScript and Rust panes:
30
+
31
+ ```sh
32
+ npm run demo:ts-rust -- --port 4177
33
+ ```
34
+
35
+ The workbench converts only when `Run` is pressed. TypeScript edits project through
36
+ the Frontier semantic graph into Rust scaffolding; Rust edits project back through
37
+ the graph into TypeScript scaffolding. The middle pane shows symbols, relations,
38
+ source maps, readiness, losses, patch hints, and the explicit supported/review-only/
39
+ unsupported bounds for the current direction.
40
+
29
41
  The demo prints JavaScript source, the Frontier universal AST/semantic-index summary,
30
42
  Rust declaration stubs, a host-adapter Rust projection, and a direct Frontier-source
31
43
  to Rust projection. Native JavaScript projection remains loss-aware: without a
@@ -0,0 +1,24 @@
1
+ export function conversionBounds(sourceLanguage, targetLanguage) {
2
+ return {
3
+ sourceLanguage,
4
+ targetLanguage,
5
+ supportedNow: [
6
+ 'Top-level imports, functions, classes, types, constants, and simple ownership regions.',
7
+ 'Frontier universal AST envelopes, semantic indexes, source maps, losses, and sidecar patch hints.',
8
+ 'Deterministic scaffold projection through explicit demo target adapters.',
9
+ 'Manual run submission so every conversion has a source hash and evidence record.'
10
+ ],
11
+ reviewRequired: [
12
+ 'Function bodies are preserved as source evidence and emitted as TODO scaffolds.',
13
+ 'Types are declaration evidence, not a full Rust trait/lifetime/ownership proof.',
14
+ 'Runtime APIs such as fetch, timers, storage, DOM, Node, and npm crates need adapters.',
15
+ 'Round trips are useful for review, but not automatic semantic equivalence proofs.'
16
+ ],
17
+ unsupportedToday: [
18
+ 'Behavior-preserving transpilation of arbitrary TypeScript or Rust.',
19
+ 'Full generic constraints, overload resolution, conditional types, macros, and borrow checking.',
20
+ 'Async control-flow lowering, exception/error model conversion, and memory ownership synthesis.',
21
+ 'Perfect comments/trivia formatting in projected target code without richer parser evidence.'
22
+ ]
23
+ };
24
+ }
@@ -0,0 +1,243 @@
1
+ export function workbenchClientScript() {
2
+ return `
3
+ const initial = JSON.parse(document.getElementById('initial-state').textContent);
4
+ const state = {
5
+ sources: { ...initial.sources },
6
+ sourceLanguage: initial.sourceLanguage || 'typescript',
7
+ result: initial.result,
8
+ activeTab: 'graph',
9
+ pending: false
10
+ };
11
+
12
+ window.frontierLangWorkbench = {
13
+ kind: 'frontier.framework.demo.langWorkbench',
14
+ version: 1,
15
+ state,
16
+ feature: {
17
+ id: 'frontier-lang-ts-rust-workbench',
18
+ package: '@shapeshift-labs/frontier-lang-compiler',
19
+ route: '/examples/js-frontier-rust-workbench',
20
+ panels: ['source', 'frontier', 'rust']
21
+ },
22
+ evidence: []
23
+ };
24
+
25
+ const typescriptInput = document.getElementById('typescriptInput');
26
+ const rustInput = document.getElementById('rustInput');
27
+ const frontierJson = document.getElementById('frontierJson');
28
+ const graphView = document.getElementById('graphView');
29
+ const readinessValue = document.getElementById('readinessValue');
30
+ const symbolValue = document.getElementById('symbolValue');
31
+ const mappingValue = document.getElementById('mappingValue');
32
+ const modeValue = document.getElementById('modeValue');
33
+ const typescriptStatus = document.getElementById('typescriptStatus');
34
+ const rustStatus = document.getElementById('rustStatus');
35
+
36
+ let updatingEditors = false;
37
+ typescriptInput.value = state.sources.typescript;
38
+ rustInput.value = state.sources.rust;
39
+ render();
40
+
41
+ typescriptInput.addEventListener('input', () => {
42
+ if (updatingEditors) return;
43
+ state.sources.typescript = typescriptInput.value;
44
+ state.sourceLanguage = 'typescript';
45
+ renderEditorModes();
46
+ });
47
+
48
+ rustInput.addEventListener('input', () => {
49
+ if (updatingEditors) return;
50
+ state.sources.rust = rustInput.value;
51
+ state.sourceLanguage = 'rust';
52
+ renderEditorModes();
53
+ });
54
+
55
+ document.getElementById('runButton').addEventListener('click', async () => {
56
+ await convertFrom(state.sourceLanguage);
57
+ });
58
+
59
+ document.getElementById('resetButton').addEventListener('click', async () => {
60
+ state.sources = { ...initial.sources };
61
+ state.sourceLanguage = 'typescript';
62
+ state.result = initial.result;
63
+ render();
64
+ });
65
+
66
+ document.getElementById('copyButton').addEventListener('click', async () => {
67
+ await navigator.clipboard.writeText(projectedSourceText());
68
+ });
69
+
70
+ for (const button of document.querySelectorAll('[data-source-mode]')) {
71
+ button.addEventListener('click', () => {
72
+ state.sourceLanguage = button.dataset.sourceMode;
73
+ renderEditorModes();
74
+ });
75
+ }
76
+
77
+ for (const button of document.querySelectorAll('[data-frontier-tab]')) {
78
+ button.addEventListener('click', () => {
79
+ state.activeTab = button.dataset.frontierTab;
80
+ for (const peer of document.querySelectorAll('[data-frontier-tab]')) {
81
+ peer.setAttribute('aria-pressed', String(peer === button));
82
+ }
83
+ render();
84
+ });
85
+ }
86
+
87
+ async function convertFrom(sourceLanguage) {
88
+ state.sourceLanguage = sourceLanguage;
89
+ state.sources[sourceLanguage] = sourceLanguage === 'rust' ? rustInput.value : typescriptInput.value;
90
+ setPending(true);
91
+ try {
92
+ const response = await fetch('/api/convert', {
93
+ method: 'POST',
94
+ headers: { 'content-type': 'application/json' },
95
+ body: JSON.stringify({ source: state.sources[sourceLanguage], sourceLanguage })
96
+ });
97
+ const payload = await response.json();
98
+ if (!response.ok || !payload.ok) throw new Error(payload.error || 'conversion failed');
99
+ state.result = payload.result;
100
+ state.sources[payload.result.projection.targetLanguage] = payload.result.projection.output;
101
+ window.frontierLangWorkbench.evidence.push({
102
+ at: Date.now(),
103
+ kind: 'conversion',
104
+ sourceHash: payload.result.sourceHash,
105
+ symbolCount: payload.result.summary.symbols,
106
+ readiness: payload.result.summary.readiness
107
+ });
108
+ } catch (error) {
109
+ state.result = errorResult(error);
110
+ } finally {
111
+ setPending(false);
112
+ render();
113
+ }
114
+ }
115
+
116
+ function render() {
117
+ const result = state.result || {};
118
+ const summary = result.summary || {};
119
+ readinessValue.textContent = summary.readiness || 'error';
120
+ readinessValue.style.color = readinessColor(summary.readiness);
121
+ symbolValue.textContent = String(summary.symbols ?? 0);
122
+ mappingValue.textContent = String(summary.sourceMapMappings ?? 0);
123
+ modeValue.textContent = result.projection?.mode || '-';
124
+ typescriptStatus.textContent = languageStatus('typescript', result);
125
+ rustStatus.textContent = languageStatus('rust', result);
126
+ renderEditorModes();
127
+ frontierJson.textContent = JSON.stringify(result.frontier, null, 2);
128
+ frontierJson.hidden = state.activeTab !== 'json';
129
+ graphView.hidden = state.activeTab !== 'graph';
130
+ if (!graphView.hidden) graphView.innerHTML = graphHtml(result);
131
+ }
132
+
133
+ function renderEditorModes() {
134
+ updatingEditors = true;
135
+ typescriptInput.value = state.sources.typescript || '';
136
+ rustInput.value = state.sources.rust || '';
137
+ updatingEditors = false;
138
+ document.querySelector('[data-view-panel="source"]').classList.toggle('isSource', state.sourceLanguage === 'typescript');
139
+ document.querySelector('[data-view-panel="rust"]').classList.toggle('isSource', state.sourceLanguage === 'rust');
140
+ for (const button of document.querySelectorAll('[data-source-mode]')) {
141
+ button.setAttribute('aria-pressed', String(button.dataset.sourceMode === state.sourceLanguage));
142
+ }
143
+ }
144
+
145
+ function languageStatus(language, result) {
146
+ if (state.sourceLanguage === language) return result.sourceHash || 'source';
147
+ return (result.projection?.targetLanguage === language ? result.projection.readiness : 'projection') || 'projection';
148
+ }
149
+
150
+ function graphHtml(result) {
151
+ const summary = result.summary || {};
152
+ const frontier = result.frontier || {};
153
+ const symbols = frontier.symbols || [];
154
+ const relations = frontier.relations || [];
155
+ return [
156
+ '<div class="chipRow">',
157
+ chip(summary.readiness, readinessClass(summary.readiness)),
158
+ chip(String(summary.losses || 0) + ' losses', 'review'),
159
+ chip(String(summary.patchHints || 0) + ' patch hints', 'ready'),
160
+ chip((result.sourceLanguage || '-') + ' -> ' + (result.projection?.targetLanguage || '-'), 'review'),
161
+ '</div>',
162
+ '<div class="sectionTitle">Symbols</div>',
163
+ '<div class="graphGrid">',
164
+ ...symbols.map((symbol) => nodeCard(symbol)),
165
+ '</div>',
166
+ '<div class="sectionTitle">Relations</div>',
167
+ '<ul class="edgeList">',
168
+ ...relations.slice(0, 18).map((edge) => '<li>' + escapeHtml(edge.label) + '</li>'),
169
+ relations.length > 18 ? '<li>' + (relations.length - 18) + ' more</li>' : '',
170
+ '</ul>',
171
+ boundsHtml(result.bounds || {})
172
+ ].join('');
173
+ }
174
+
175
+ function nodeCard(symbol) {
176
+ return '<article class="nodeCard" data-graph-node="' + escapeHtml(symbol.id || symbol.name) + '">' +
177
+ '<strong>' + escapeHtml(symbol.name) + '</strong>' +
178
+ '<span>' + escapeHtml(symbol.kind || 'symbol') + ' · ' + escapeHtml(symbol.region || 'region') + '</span>' +
179
+ '</article>';
180
+ }
181
+
182
+ function chip(text, className) {
183
+ return '<span class="chip ' + className + '">' + escapeHtml(text || '-') + '</span>';
184
+ }
185
+
186
+ function readinessClass(readiness) {
187
+ if (readiness === 'ready' || readiness === 'ready-with-losses') return 'ready';
188
+ if (readiness === 'blocked') return 'blocked';
189
+ return 'review';
190
+ }
191
+
192
+ function readinessColor(readiness) {
193
+ if (readiness === 'ready' || readiness === 'ready-with-losses') return 'var(--green)';
194
+ if (readiness === 'blocked') return 'var(--red)';
195
+ return 'var(--amber)';
196
+ }
197
+
198
+ function errorResult(error) {
199
+ return {
200
+ summary: { readiness: 'blocked', symbols: 0, sourceMapMappings: 0, losses: 1, patchHints: 0 },
201
+ frontier: { symbols: [], relations: [], error: String(error.message || error) },
202
+ projection: { output: String(error.stack || error), readiness: 'blocked', mode: 'error' }
203
+ };
204
+ }
205
+
206
+ function projectedSourceText() {
207
+ const target = state.result?.projection?.targetLanguage;
208
+ return target ? state.sources[target] || '' : '';
209
+ }
210
+
211
+ function boundsHtml(bounds) {
212
+ return [
213
+ '<div class="sectionTitle">Bounds</div>',
214
+ '<div class="boundsGrid">',
215
+ boundCard('Supported now', bounds.supportedNow || []),
216
+ boundCard('Review required', bounds.reviewRequired || []),
217
+ boundCard('Unsupported today', bounds.unsupportedToday || []),
218
+ '</div>'
219
+ ].join('');
220
+ }
221
+
222
+ function boundCard(title, rows) {
223
+ return '<section class="boundCard"><strong>' + escapeHtml(title) + '</strong><ul>' +
224
+ rows.map((row) => '<li>' + escapeHtml(row) + '</li>').join('') +
225
+ '</ul></section>';
226
+ }
227
+
228
+ function setPending(pending) {
229
+ state.pending = pending;
230
+ document.body.toggleAttribute('data-pending', pending);
231
+ }
232
+
233
+ function escapeHtml(value) {
234
+ return String(value ?? '').replace(/[&<>"']/g, (char) => ({
235
+ '&': '&amp;',
236
+ '<': '&lt;',
237
+ '>': '&gt;',
238
+ '"': '&quot;',
239
+ "'": '&#39;'
240
+ })[char]);
241
+ }
242
+ `;
243
+ }
@@ -0,0 +1,76 @@
1
+ export function renderWorkbenchHtml(initialState) {
2
+ return `<!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>Frontier Lang TypeScript to Rust Workbench</title>
8
+ <link rel="stylesheet" href="/styles.css">
9
+ </head>
10
+ <body>
11
+ <main class="appShell" data-frontier-app="lang-workbench">
12
+ <header class="topbar">
13
+ <div class="brand">
14
+ <span class="brandMark">F</span>
15
+ <div>
16
+ <h1>Frontier Lang Workbench</h1>
17
+ <p>TypeScript -> Frontier graph -> Rust</p>
18
+ </div>
19
+ </div>
20
+ <div class="topActions">
21
+ <div class="modeGroup" role="group" aria-label="Source language">
22
+ <button data-source-mode="typescript" aria-pressed="true">TS source</button>
23
+ <button data-source-mode="rust" aria-pressed="false">Rust source</button>
24
+ </div>
25
+ <button class="runButton" id="runButton">Run</button>
26
+ <button class="iconButton" id="resetButton" title="Reset source" aria-label="Reset source">↺</button>
27
+ <button class="iconButton" id="copyButton" title="Copy projection" aria-label="Copy projection">⧉</button>
28
+ </div>
29
+ </header>
30
+
31
+ <section class="statusStrip" data-view-panel="status">
32
+ <div><span class="label">Readiness</span><strong id="readinessValue">-</strong></div>
33
+ <div><span class="label">Symbols</span><strong id="symbolValue">-</strong></div>
34
+ <div><span class="label">Mappings</span><strong id="mappingValue">-</strong></div>
35
+ <div><span class="label">Mode</span><strong id="modeValue">-</strong></div>
36
+ </section>
37
+
38
+ <section class="workspace">
39
+ <section class="pane sourcePane" data-view-panel="source">
40
+ <div class="paneHeader">
41
+ <h2>TypeScript</h2>
42
+ <span id="typescriptStatus">source</span>
43
+ </div>
44
+ <textarea id="typescriptInput" spellcheck="false" aria-label="TypeScript source"></textarea>
45
+ </section>
46
+
47
+ <section class="pane graphPane" data-view-panel="frontier">
48
+ <div class="paneHeader">
49
+ <h2>Frontier</h2>
50
+ <div class="segmented" role="tablist" aria-label="Frontier view">
51
+ <button data-frontier-tab="graph" aria-pressed="true">Graph</button>
52
+ <button data-frontier-tab="json" aria-pressed="false">JSON</button>
53
+ </div>
54
+ </div>
55
+ <div id="graphView" class="graphView"></div>
56
+ <pre id="frontierJson" class="codeBlock" hidden></pre>
57
+ </section>
58
+
59
+ <section class="pane outputPane" data-view-panel="rust">
60
+ <div class="paneHeader">
61
+ <h2>Rust</h2>
62
+ <span id="rustStatus">target</span>
63
+ </div>
64
+ <textarea id="rustInput" spellcheck="false" aria-label="Rust source"></textarea>
65
+ </section>
66
+ </section>
67
+ </main>
68
+ <script type="application/json" id="initial-state">${escapeScriptJson(initialState)}</script>
69
+ <script src="/client.js" type="module"></script>
70
+ </body>
71
+ </html>`;
72
+ }
73
+
74
+ function escapeScriptJson(value) {
75
+ return JSON.stringify(value).replace(/</g, '\\u003c');
76
+ }
@@ -0,0 +1,243 @@
1
+ export function workbenchStyles() {
2
+ return `
3
+ :root {
4
+ color-scheme: dark;
5
+ --bg: #08090a;
6
+ --panel: #111417;
7
+ --panel-2: #171b20;
8
+ --panel-3: #20262c;
9
+ --line: #303941;
10
+ --text: #f4f7f2;
11
+ --muted: #aab3aa;
12
+ --faint: #758075;
13
+ --green: #61d394;
14
+ --amber: #e7b75f;
15
+ --red: #e46c69;
16
+ --blue: #6db4e8;
17
+ --violet: #a78bfa;
18
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
19
+ --ui: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
20
+ }
21
+
22
+ * { box-sizing: border-box; }
23
+ html, body { height: 100%; margin: 0; }
24
+ body {
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ font-family: var(--ui);
28
+ overflow: hidden;
29
+ }
30
+
31
+ .appShell {
32
+ min-height: 100vh;
33
+ display: grid;
34
+ grid-template-rows: 58px 46px minmax(0, 1fr);
35
+ }
36
+
37
+ .topbar, .statusStrip, .paneHeader {
38
+ display: flex;
39
+ align-items: center;
40
+ border-bottom: 1px solid var(--line);
41
+ }
42
+
43
+ .topbar {
44
+ justify-content: space-between;
45
+ padding: 0 14px;
46
+ background: #0c0e10;
47
+ }
48
+
49
+ .brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
50
+ .brandMark {
51
+ width: 34px;
52
+ height: 34px;
53
+ display: grid;
54
+ place-items: center;
55
+ border: 1px solid var(--line);
56
+ border-radius: 8px;
57
+ background: #151b17;
58
+ color: var(--green);
59
+ font-weight: 800;
60
+ }
61
+ .brand h1 { margin: 0; font-size: 15px; line-height: 1.1; letter-spacing: 0; }
62
+ .brand p { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
63
+
64
+ .topActions { display: flex; align-items: center; gap: 8px; }
65
+ .iconButton, .segmented button, .modeGroup button, .runButton {
66
+ height: 32px;
67
+ border: 1px solid var(--line);
68
+ background: var(--panel-2);
69
+ color: var(--text);
70
+ border-radius: 8px;
71
+ }
72
+ .iconButton { width: 34px; font-size: 16px; cursor: pointer; }
73
+ .runButton { min-width: 64px; padding: 0 14px; cursor: pointer; color: var(--green); }
74
+ .modeGroup { display: flex; }
75
+ .modeGroup button {
76
+ width: 84px;
77
+ border-radius: 0;
78
+ font-size: 12px;
79
+ cursor: pointer;
80
+ }
81
+ .modeGroup button:first-child { border-radius: 7px 0 0 7px; }
82
+ .modeGroup button:last-child { border-radius: 0 7px 7px 0; border-left: 0; }
83
+ .modeGroup button[aria-pressed="true"] { background: #213127; color: var(--green); }
84
+ .iconButton:hover, .segmented button:hover, .modeGroup button:hover, .runButton:hover { background: var(--panel-3); }
85
+
86
+ .statusStrip {
87
+ display: grid;
88
+ grid-template-columns: repeat(4, minmax(0, 1fr));
89
+ background: #0f1214;
90
+ }
91
+ .statusStrip div {
92
+ min-width: 0;
93
+ padding: 8px 14px;
94
+ border-right: 1px solid var(--line);
95
+ }
96
+ .label {
97
+ display: block;
98
+ color: var(--faint);
99
+ font-size: 10px;
100
+ text-transform: uppercase;
101
+ }
102
+ .statusStrip strong { font-size: 13px; font-weight: 650; }
103
+
104
+ .workspace {
105
+ min-height: 0;
106
+ display: grid;
107
+ grid-template-columns: minmax(270px, 0.95fr) minmax(330px, 1.1fr) minmax(300px, 1fr);
108
+ }
109
+ .pane {
110
+ min-width: 0;
111
+ min-height: 0;
112
+ display: grid;
113
+ grid-template-rows: 44px minmax(0, 1fr);
114
+ border-right: 1px solid var(--line);
115
+ background: var(--panel);
116
+ }
117
+ .pane:last-child { border-right: 0; }
118
+ .pane.isSource .paneHeader { box-shadow: inset 0 -2px 0 var(--green); }
119
+ .paneHeader {
120
+ justify-content: space-between;
121
+ padding: 0 12px;
122
+ background: var(--panel-2);
123
+ }
124
+ .paneHeader h2 { margin: 0; font-size: 13px; letter-spacing: 0; }
125
+ .paneHeader span { color: var(--muted); font-size: 12px; font-family: var(--mono); }
126
+
127
+ textarea, .codeBlock {
128
+ width: 100%;
129
+ height: 100%;
130
+ min-height: 0;
131
+ margin: 0;
132
+ border: 0;
133
+ resize: none;
134
+ outline: none;
135
+ background: #0d1012;
136
+ color: #edf2ea;
137
+ font-family: var(--mono);
138
+ font-size: 13px;
139
+ line-height: 1.48;
140
+ padding: 14px;
141
+ overflow: auto;
142
+ white-space: pre;
143
+ }
144
+
145
+ .graphView {
146
+ min-height: 0;
147
+ overflow: auto;
148
+ padding: 12px;
149
+ }
150
+ .graphGrid {
151
+ display: grid;
152
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
153
+ gap: 8px;
154
+ }
155
+ .nodeCard {
156
+ border: 1px solid var(--line);
157
+ border-radius: 8px;
158
+ background: #14191d;
159
+ padding: 10px;
160
+ }
161
+ .nodeCard strong {
162
+ display: block;
163
+ overflow-wrap: anywhere;
164
+ font-size: 13px;
165
+ }
166
+ .nodeCard span { color: var(--muted); font-size: 12px; }
167
+ .chipRow { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
168
+ .chip {
169
+ border: 1px solid var(--line);
170
+ border-radius: 999px;
171
+ padding: 4px 8px;
172
+ color: var(--muted);
173
+ font-size: 11px;
174
+ }
175
+ .chip.ready { color: var(--green); border-color: rgba(97, 211, 148, .45); }
176
+ .chip.review { color: var(--amber); border-color: rgba(231, 183, 95, .45); }
177
+ .chip.blocked { color: var(--red); border-color: rgba(228, 108, 105, .45); }
178
+ .sectionTitle {
179
+ margin: 12px 0 8px;
180
+ color: var(--muted);
181
+ font-size: 11px;
182
+ text-transform: uppercase;
183
+ }
184
+ .edgeList {
185
+ display: grid;
186
+ gap: 6px;
187
+ margin: 0;
188
+ padding: 0;
189
+ list-style: none;
190
+ }
191
+ .edgeList li {
192
+ border-left: 2px solid var(--blue);
193
+ background: rgba(109, 180, 232, .08);
194
+ padding: 6px 8px;
195
+ color: var(--muted);
196
+ font-size: 12px;
197
+ overflow-wrap: anywhere;
198
+ }
199
+ .boundsGrid {
200
+ display: grid;
201
+ grid-template-columns: repeat(3, minmax(0, 1fr));
202
+ gap: 8px;
203
+ }
204
+ .boundCard {
205
+ border: 1px solid var(--line);
206
+ border-radius: 8px;
207
+ background: #12171a;
208
+ padding: 10px;
209
+ }
210
+ .boundCard strong {
211
+ display: block;
212
+ margin-bottom: 8px;
213
+ font-size: 12px;
214
+ }
215
+ .boundCard ul {
216
+ margin: 0;
217
+ padding-left: 16px;
218
+ color: var(--muted);
219
+ font-size: 11px;
220
+ line-height: 1.45;
221
+ }
222
+ .segmented { display: flex; gap: 0; }
223
+ .segmented button {
224
+ width: 58px;
225
+ border-radius: 0;
226
+ font-size: 12px;
227
+ }
228
+ .segmented button:first-child { border-radius: 7px 0 0 7px; }
229
+ .segmented button:last-child { border-radius: 0 7px 7px 0; border-left: 0; }
230
+ .segmented button[aria-pressed="true"] { background: #213127; color: var(--green); }
231
+
232
+ @media (max-width: 980px) {
233
+ body { overflow: auto; }
234
+ .appShell { min-height: 100vh; grid-template-rows: auto auto auto; }
235
+ .workspace { grid-template-columns: 1fr; }
236
+ .pane { min-height: 360px; border-right: 0; border-bottom: 1px solid var(--line); }
237
+ .statusStrip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
238
+ .topbar { align-items: flex-start; gap: 10px; padding: 10px 14px; }
239
+ .topActions { flex-wrap: wrap; justify-content: flex-end; }
240
+ .boundsGrid { grid-template-columns: 1fr; }
241
+ }
242
+ `;
243
+ }
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ import http from 'node:http';
3
+ import { compileNativeSource, createSemanticImportSidecar, importNativeSource, writeUniversalAstJson } from '../dist/index.js';
4
+ import { conversionBounds } from './js-frontier-rust-workbench-bounds.mjs';
5
+ import { renderWorkbenchHtml } from './js-frontier-rust-workbench-html.mjs';
6
+ import { workbenchClientScript } from './js-frontier-rust-workbench-client.mjs';
7
+ import { workbenchStyles } from './js-frontier-rust-workbench-styles.mjs';
8
+
9
+ const args = readArgs(process.argv.slice(2));
10
+ const port = Number(args.port ?? 4177);
11
+ const sampleSource = `import { nanoid } from "nanoid";
12
+
13
+ export type TodoInput = {
14
+ title: string;
15
+ };
16
+
17
+ export function addTodo(input: TodoInput) {
18
+ return { id: nanoid(), title: input.title };
19
+ }
20
+
21
+ export class TodoStore {
22
+ save(title: string) {
23
+ return addTodo({ title });
24
+ }
25
+ }
26
+ `;
27
+ const sampleRustSource = `// Generated from Frontier semantic graph evidence.
28
+ pub fn add_todo() {
29
+ // port body from preserved TypeScript source
30
+ }
31
+
32
+ pub struct TodoStore;
33
+ `;
34
+
35
+ if (args.smoke) {
36
+ const result = convertSource(sampleSource, { sourceLanguage: 'typescript' });
37
+ assert(result.summary.symbols >= 2, 'expected semantic symbols');
38
+ assert(result.projection.output.includes('pub'), 'expected Rust projection');
39
+ const reverse = convertSource(result.projection.output, { sourceLanguage: 'rust' });
40
+ assert(reverse.projection.output.includes('export'), 'expected TypeScript projection');
41
+ console.log(JSON.stringify({ ok: true, summary: result.summary }, null, 2));
42
+ } else {
43
+ const server = http.createServer(routeRequest);
44
+ server.listen(port, '127.0.0.1', () => {
45
+ console.log(`Frontier Lang workbench: http://127.0.0.1:${port}/`);
46
+ });
47
+ }
48
+
49
+ async function routeRequest(request, response) {
50
+ try {
51
+ if (request.method === 'GET' && request.url === '/') {
52
+ return send(response, 200, renderWorkbenchHtml({
53
+ sourceLanguage: 'typescript',
54
+ sources: { typescript: sampleSource, rust: sampleRustSource },
55
+ result: convertSource(sampleSource, { sourceLanguage: 'typescript' })
56
+ }), 'text/html; charset=utf-8');
57
+ }
58
+ if (request.method === 'GET' && request.url === '/styles.css') {
59
+ return send(response, 200, workbenchStyles(), 'text/css; charset=utf-8');
60
+ }
61
+ if (request.method === 'GET' && request.url === '/client.js') {
62
+ return send(response, 200, workbenchClientScript(), 'text/javascript; charset=utf-8');
63
+ }
64
+ if (request.method === 'POST' && request.url === '/api/convert') {
65
+ const body = await readJsonBody(request);
66
+ return sendJson(response, 200, {
67
+ ok: true,
68
+ result: convertSource(String(body.source ?? ''), {
69
+ sourceLanguage: body.sourceLanguage ?? 'typescript'
70
+ })
71
+ });
72
+ }
73
+ return send(response, 404, 'not found', 'text/plain; charset=utf-8');
74
+ } catch (error) {
75
+ return sendJson(response, 500, { ok: false, error: String(error.message || error) });
76
+ }
77
+ }
78
+
79
+ function convertSource(source, options = {}) {
80
+ const sourceLanguage = normalizeSourceLanguage(options.sourceLanguage);
81
+ const target = sourceLanguage === 'rust' ? 'typescript' : 'rust';
82
+ const imported = importNativeSource({
83
+ language: sourceLanguage,
84
+ sourcePath: `workbench/input.${sourceExtension(sourceLanguage)}`,
85
+ sourceText: source
86
+ });
87
+ const projection = compileNativeSource(imported, {
88
+ target,
89
+ targetPath: `workbench/output.${targetExtension(target)}`,
90
+ targetAdapters: [createTsToRustWorkbenchAdapter(), createRustToTsWorkbenchAdapter()],
91
+ emitOnBlocked: true
92
+ });
93
+ const sidecar = createSemanticImportSidecar(imported, { generatedAt: 0, targetPath: projection.sourceMap?.targetPath });
94
+ return {
95
+ sourceHash: imported.nativeSource.sourceHash,
96
+ sourceLanguage,
97
+ summary: {
98
+ readiness: sidecar.summary.readiness,
99
+ symbols: imported.semanticIndex.symbols.length,
100
+ sourceMapMappings: imported.sourceMaps[0]?.mappings.length ?? 0,
101
+ losses: imported.losses.length,
102
+ patchHints: sidecar.patchHints.length
103
+ },
104
+ frontier: {
105
+ universalAst: universalAstSummary(imported),
106
+ semanticIndexId: imported.semanticIndex.id,
107
+ sidecarId: sidecar.id,
108
+ symbols: imported.semanticIndex.symbols.map(symbolSummary),
109
+ relations: relationSummaries(imported),
110
+ losses: imported.losses.map(lossSummary),
111
+ patchHints: sidecar.patchHints.map((hint) => ({
112
+ id: hint.id,
113
+ readiness: hint.readiness,
114
+ operations: hint.supportedOperations,
115
+ ownershipKey: hint.ownershipKey
116
+ }))
117
+ },
118
+ bounds: conversionBounds(sourceLanguage, target),
119
+ projection: {
120
+ target,
121
+ targetLanguage: target,
122
+ sourceLanguage,
123
+ mode: projection.outputMode,
124
+ readiness: projection.readiness.readiness,
125
+ ok: projection.ok,
126
+ output: projection.output,
127
+ sourceMapMappings: projection.sourceMap?.mappings.length ?? 0
128
+ }
129
+ };
130
+ }
131
+
132
+ function createTsToRustWorkbenchAdapter() {
133
+ return {
134
+ id: 'frontier-lang-workbench-ts-to-rust',
135
+ sourceLanguage: 'typescript',
136
+ target: 'rust',
137
+ version: '0.0.0-demo',
138
+ capabilities: ['semantic-symbol-scaffold'],
139
+ coverage: {
140
+ readiness: 'ready',
141
+ handledLossKinds: ['opaqueNative', 'declarationOnlyCoverage', 'partialSemanticIndex', 'sourceMapApproximation', 'sourcePreservation'],
142
+ notes: ['Workbench adapter lowers TypeScript semantic symbols to Rust scaffolding and leaves bodies explicit.']
143
+ },
144
+ project(input) {
145
+ return {
146
+ output: rustFromSymbols(input.importResult.semanticIndex?.symbols ?? []),
147
+ readiness: 'ready',
148
+ evidence: [{
149
+ id: 'evidence_workbench_js_to_rust',
150
+ kind: 'projection',
151
+ status: 'passed',
152
+ summary: 'Workbench adapter projected TypeScript semantic symbols to Rust scaffolding.'
153
+ }]
154
+ };
155
+ }
156
+ };
157
+ }
158
+
159
+ function createRustToTsWorkbenchAdapter() {
160
+ return {
161
+ id: 'frontier-lang-workbench-rust-to-ts',
162
+ sourceLanguage: 'rust',
163
+ target: 'typescript',
164
+ version: '0.0.0-demo',
165
+ capabilities: ['semantic-symbol-scaffold'],
166
+ coverage: {
167
+ readiness: 'ready',
168
+ handledLossKinds: ['opaqueNative', 'macroExpansion', 'declarationOnlyCoverage', 'partialSemanticIndex', 'sourceMapApproximation', 'sourcePreservation'],
169
+ notes: ['Workbench adapter lowers Rust semantic symbols to TypeScript scaffolding and leaves bodies explicit.']
170
+ },
171
+ project(input) {
172
+ return {
173
+ output: tsFromSymbols(input.importResult.semanticIndex?.symbols ?? []),
174
+ readiness: 'ready',
175
+ evidence: [{
176
+ id: 'evidence_workbench_rust_to_ts',
177
+ kind: 'projection',
178
+ status: 'passed',
179
+ summary: 'Workbench adapter projected Rust semantic symbols to TypeScript scaffolding.'
180
+ }]
181
+ };
182
+ }
183
+ };
184
+ }
185
+
186
+ function rustFromSymbols(symbols) {
187
+ const lines = ['// Generated from Frontier semantic graph evidence.'];
188
+ for (const symbol of symbols) {
189
+ if (symbol.kind === 'class') lines.push('', `pub struct ${safeTypeName(symbol.name)};`);
190
+ if (symbol.kind === 'function' || symbol.kind === 'method') {
191
+ lines.push('', `pub fn ${safeRustName(symbol.name)}() {`, ' // port body from preserved TypeScript source', '}');
192
+ }
193
+ }
194
+ return `${lines.join('\n').trim()}\n`;
195
+ }
196
+
197
+ function tsFromSymbols(symbols) {
198
+ const lines = ['// Generated from Frontier semantic graph evidence.'];
199
+ for (const symbol of symbols) {
200
+ if (symbol.kind === 'type' || symbol.kind === 'class') lines.push('', `export class ${safeTypeName(symbol.name)} {}`);
201
+ if (symbol.kind === 'function' || symbol.kind === 'method') {
202
+ lines.push('', `export function ${safeJsName(symbol.name)}(...args: unknown[]) {`, " throw new Error('port body from preserved Rust source');", '}');
203
+ }
204
+ }
205
+ return `${lines.join('\n').trim()}\n`;
206
+ }
207
+
208
+ function universalAstSummary(imported) {
209
+ const summary = { valid: true, validationError: undefined };
210
+ let parsed = imported.universalAst;
211
+ try {
212
+ parsed = JSON.parse(writeUniversalAstJson(imported.universalAst));
213
+ } catch (error) {
214
+ summary.valid = false;
215
+ summary.validationError = String(error.message || error);
216
+ }
217
+ return {
218
+ ...summary,
219
+ kind: parsed.kind,
220
+ id: parsed.id,
221
+ nativeSources: parsed.nativeSources?.length ?? 0,
222
+ semanticSymbols: parsed.semanticIndex?.symbols?.length ?? 0,
223
+ sourceMaps: parsed.sourceMaps?.length ?? 0,
224
+ layers: Object.keys(parsed.layers ?? {})
225
+ };
226
+ }
227
+
228
+ function symbolSummary(symbol) {
229
+ return {
230
+ id: symbol.id,
231
+ name: symbol.name,
232
+ kind: symbol.kind,
233
+ language: symbol.language,
234
+ region: symbol.metadata?.ownershipRegionKind,
235
+ ownershipKey: symbol.metadata?.ownershipRegionKey
236
+ };
237
+ }
238
+
239
+ function relationSummaries(imported) {
240
+ const names = new Map(imported.semanticIndex.symbols.map((symbol) => [symbol.id, symbol.name]));
241
+ return imported.semanticIndex.relations.map((relation) => ({
242
+ id: relation.id,
243
+ predicate: relation.predicate,
244
+ label: `${names.get(relation.sourceId) ?? relation.sourceId} ${relation.predicate} ${names.get(relation.targetId) ?? relation.targetId}`
245
+ }));
246
+ }
247
+
248
+ function lossSummary(loss) {
249
+ return { id: loss.id, kind: loss.kind, severity: loss.severity, message: loss.message };
250
+ }
251
+
252
+ function normalizeSourceLanguage(language) {
253
+ return String(language || 'typescript').toLowerCase() === 'rust' ? 'rust' : 'typescript';
254
+ }
255
+
256
+ function targetExtension(target) {
257
+ return target === 'rust' ? 'rs' : 'ts';
258
+ }
259
+
260
+ function sourceExtension(language) {
261
+ return language === 'rust' ? 'rs' : 'ts';
262
+ }
263
+
264
+ function safeTypeName(name) {
265
+ const clean = String(name).replace(/[^A-Za-z0-9_]/g, '');
266
+ return clean && /^[A-Z]/.test(clean) ? clean : `Frontier${clean || 'Type'}`;
267
+ }
268
+
269
+ function safeRustName(name) {
270
+ return String(name).replace(/\./g, '_').replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^A-Za-z0-9_]/g, '_').toLowerCase();
271
+ }
272
+
273
+ function safeJsName(name) {
274
+ const clean = String(name).replace(/[^A-Za-z0-9_$]/g, '_');
275
+ return /^[A-Za-z_$]/.test(clean) ? clean : `frontier_${clean}`;
276
+ }
277
+
278
+ function sendJson(response, status, value) {
279
+ return send(response, status, JSON.stringify(value), 'application/json; charset=utf-8');
280
+ }
281
+
282
+ function send(response, status, body, type) {
283
+ response.writeHead(status, { 'content-type': type, 'cache-control': 'no-store' });
284
+ response.end(body);
285
+ }
286
+
287
+ function readJsonBody(request) {
288
+ return new Promise((resolve, reject) => {
289
+ let text = '';
290
+ request.setEncoding('utf8');
291
+ request.on('data', (chunk) => {
292
+ text += chunk;
293
+ if (text.length > 200000) reject(new Error('request body too large'));
294
+ });
295
+ request.on('end', () => resolve(text ? JSON.parse(text) : {}));
296
+ request.on('error', reject);
297
+ });
298
+ }
299
+
300
+ function readArgs(entries) {
301
+ const parsed = {};
302
+ for (let index = 0; index < entries.length; index += 1) {
303
+ const entry = entries[index];
304
+ if (entry === '--serve') parsed.serve = true;
305
+ else if (entry === '--smoke') parsed.smoke = true;
306
+ else if (entry === '--port') parsed.port = entries[++index];
307
+ else if (entry.startsWith('--port=')) parsed.port = entry.slice('--port='.length);
308
+ }
309
+ return parsed;
310
+ }
311
+
312
+ function assert(condition, message) {
313
+ if (!condition) throw new Error(message);
314
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.55",
3
+ "version": "0.2.56",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,6 +26,10 @@
26
26
  "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p test/tsconfig.json",
27
27
  "fuzz": "npm run build && node fuzz/smoke.mjs",
28
28
  "bench": "npm run build && node bench/smoke.mjs",
29
+ "demo:ts-rust": "npm run build && node examples/js-frontier-rust-workbench.mjs --serve",
30
+ "demo:ts-rust:smoke": "npm run build && node examples/js-frontier-rust-workbench.mjs --smoke",
31
+ "demo:js-rust": "npm run demo:ts-rust",
32
+ "demo:js-rust:smoke": "npm run demo:ts-rust:smoke",
29
33
  "prepare": "npm run build",
30
34
  "prepack": "npm run lint && npm test",
31
35
  "pack:dry": "npm pack --dry-run",