|
170 | 170 | })(); |
171 | 171 |
|
172 | 172 |
|
| 173 | + /* ================================================================ |
| 174 | + PYODIDE REPL |
| 175 | + Lazily loads Pyodide the first time the user opens the REPL panel, |
| 176 | + then uses ProgramExecutor(language=lang) to compile and execute |
| 177 | + arbitrary multilingual source code client-side. |
| 178 | + ================================================================ */ |
| 179 | + |
| 180 | + let _pyodide = null; |
| 181 | + let _pyodideReady = false; |
| 182 | + let _pyodideInit = null; // singleton Promise |
| 183 | + |
| 184 | + function _loadPyodideScript() { |
| 185 | + return new Promise((resolve, reject) => { |
| 186 | + if (typeof loadPyodide === 'function') { resolve(); return; } |
| 187 | + const s = document.createElement('script'); |
| 188 | + s.src = 'https://cdn.jsdelivr.net/pyodide/v0.26.0/full/pyodide.js'; |
| 189 | + s.onload = resolve; |
| 190 | + s.onerror = () => reject(new Error('Failed to load Pyodide script')); |
| 191 | + document.head.appendChild(s); |
| 192 | + }); |
| 193 | + } |
| 194 | + |
| 195 | + async function _initReplPyodide() { |
| 196 | + if (_pyodideReady) return; |
| 197 | + setReplStatus('loading', 'loading…'); |
| 198 | + await _loadPyodideScript(); |
| 199 | + |
| 200 | + _pyodide = await loadPyodide({ // eslint-disable-line no-undef |
| 201 | + indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.26.0/full/', |
| 202 | + }); |
| 203 | + setReplStatus('loading', 'installing…'); |
| 204 | + await _pyodide.loadPackage('micropip'); |
| 205 | + const micropip = _pyodide.pyimport('micropip'); |
| 206 | + await micropip.install('roman'); |
| 207 | + await micropip.install('python-dateutil'); |
| 208 | + |
| 209 | + // Try a locally-built wheel first, fall back to PyPI |
| 210 | + let installed = false; |
| 211 | + try { |
| 212 | + const resp = await fetch(baseUrl + '/assets/wheel_info.json'); |
| 213 | + if (resp.ok) { |
| 214 | + const info = await resp.json(); |
| 215 | + const url = new URL(baseUrl + '/assets/' + info.wheel, window.location.href).href; |
| 216 | + await micropip.install(url); |
| 217 | + installed = true; |
| 218 | + } |
| 219 | + } catch (_) { /* wheel_info.json absent — fall back to PyPI */ } |
| 220 | + |
| 221 | + if (!installed) { |
| 222 | + await micropip.install('multilingualprogramming'); |
| 223 | + } |
| 224 | + |
| 225 | + // Warm-up import so the first Run is fast |
| 226 | + await _pyodide.runPythonAsync( |
| 227 | + 'from multilingualprogramming.codegen.executor import ProgramExecutor' |
| 228 | + ); |
| 229 | + |
| 230 | + _pyodideReady = true; |
| 231 | + setReplStatus('ready', 'ready'); |
| 232 | + } |
| 233 | + |
| 234 | + function ensureReplPyodide() { |
| 235 | + if (!_pyodideInit) _pyodideInit = _initReplPyodide().catch(err => { |
| 236 | + setReplStatus('error', 'error'); |
| 237 | + console.error('Pyodide init failed:', err); |
| 238 | + _pyodideInit = null; // allow retry |
| 239 | + }); |
| 240 | + return _pyodideInit; |
| 241 | + } |
| 242 | + |
173 | 243 | /* ================================================================ |
174 | 244 | REPL PANEL |
175 | 245 | ================================================================ */ |
|
224 | 294 | if (badge) badge.textContent = label; |
225 | 295 | } |
226 | 296 |
|
227 | | - /* Warm up the WASM module and update status indicators. */ |
228 | | - function initWasm() { |
229 | | - MLWasm.ready |
230 | | - .then(() => setReplStatus('ready', 'ready')) |
231 | | - .catch(() => setReplStatus('error', 'unavailable')); |
232 | | - } |
233 | | - |
234 | 297 | /* Wire up the REPL panel controls. */ |
235 | 298 | const replPanel = document.getElementById('repl-panel'); |
236 | 299 | const replToggle = document.getElementById('repl-toggle'); |
|
242 | 305 | replPanel.hidden = open; |
243 | 306 | replToggle.setAttribute('aria-expanded', String(!open)); |
244 | 307 | if (!open) { |
245 | | - /* Start loading WASM when the user first opens the REPL. */ |
246 | | - initWasm(); |
| 308 | + /* Start loading Pyodide the first time the user opens the REPL. */ |
| 309 | + ensureReplPyodide(); |
247 | 310 | document.getElementById('repl-input').focus(); |
248 | 311 | } |
249 | 312 | }); |
|
261 | 324 | document.getElementById('repl-input').value = ''; |
262 | 325 | }); |
263 | 326 |
|
264 | | - /* Run button. */ |
265 | | - document.getElementById('repl-run-btn').addEventListener('click', () => { |
| 327 | + /* Run button — compile and execute via Pyodide ProgramExecutor. */ |
| 328 | + document.getElementById('repl-run-btn').addEventListener('click', async () => { |
266 | 329 | const src = document.getElementById('repl-input').value.trim(); |
| 330 | + const lang = document.getElementById('repl-lang').value; |
267 | 331 | const output = document.getElementById('repl-output'); |
268 | 332 | if (!src) return; |
269 | 333 |
|
| 334 | + if (!_pyodideReady) { |
| 335 | + output.innerHTML = '<span class="repl-output-placeholder">Still loading — please wait…</span>'; |
| 336 | + await ensureReplPyodide(); |
| 337 | + } |
| 338 | + |
270 | 339 | setReplStatus('running', 'running…'); |
271 | 340 | output.innerHTML = '<span class="repl-output-placeholder">Running…</span>'; |
272 | 341 |
|
273 | | - MLWasm.execute(src) |
274 | | - .then(result => { |
275 | | - renderOutput(output, result); |
276 | | - setReplStatus('ready', 'ready'); |
277 | | - }) |
278 | | - .catch(err => { |
279 | | - output.innerHTML = `<pre class="output-stderr">${err}</pre>`; |
280 | | - setReplStatus('error', 'error'); |
| 342 | + try { |
| 343 | + _pyodide.globals.set('_repl_code', src); |
| 344 | + _pyodide.globals.set('_repl_lang', lang); |
| 345 | + await _pyodide.runPythonAsync(` |
| 346 | +from multilingualprogramming.codegen.executor import ProgramExecutor |
| 347 | +_r = ProgramExecutor(language=_repl_lang).execute(_repl_code) |
| 348 | +_repl_out = _r.output or '' |
| 349 | +_repl_errs = '\\n'.join(_r.errors) if _r.errors else '' |
| 350 | +`); |
| 351 | + renderOutput(output, { |
| 352 | + stdout: _pyodide.globals.get('_repl_out'), |
| 353 | + stderr: _pyodide.globals.get('_repl_errs'), |
281 | 354 | }); |
| 355 | + setReplStatus('ready', 'ready'); |
| 356 | + } catch (err) { |
| 357 | + output.innerHTML = `<pre class="output-stderr">${err}</pre>`; |
| 358 | + setReplStatus('error', 'error'); |
| 359 | + } |
282 | 360 | }); |
283 | 361 |
|
284 | | - /* WAT button — show compiled WebAssembly text in the output pane. */ |
285 | | - document.getElementById('repl-wat-btn').addEventListener('click', () => { |
| 362 | + /* WAT button — generate WAT for the current user code via Pyodide. */ |
| 363 | + document.getElementById('repl-wat-btn').addEventListener('click', async () => { |
286 | 364 | const output = document.getElementById('repl-output'); |
287 | | - /* Toggle off if already showing WAT. */ |
288 | 365 | if (output.dataset.mode === 'wat') { |
289 | 366 | output.innerHTML = '<span class="repl-output-placeholder">Output will appear here</span>'; |
290 | 367 | delete output.dataset.mode; |
291 | 368 | return; |
292 | 369 | } |
293 | | - output.innerHTML = '<span class="repl-output-placeholder">Loading WAT…</span>'; |
| 370 | + |
| 371 | + const src = document.getElementById('repl-input').value.trim(); |
| 372 | + const lang = document.getElementById('repl-lang').value; |
| 373 | + |
| 374 | + output.innerHTML = '<span class="repl-output-placeholder">Generating WAT…</span>'; |
294 | 375 | output.dataset.mode = 'wat'; |
295 | | - MLWasm.wat |
296 | | - .then(text => { |
| 376 | + |
| 377 | + if (!_pyodideReady) { await ensureReplPyodide(); } |
| 378 | + |
| 379 | + try { |
| 380 | + _pyodide.globals.set('_repl_code', src || ''); |
| 381 | + _pyodide.globals.set('_repl_lang', lang); |
| 382 | + await _pyodide.runPythonAsync(` |
| 383 | +from multilingualprogramming.lexer.lexer import Lexer |
| 384 | +from multilingualprogramming.parser.parser import Parser |
| 385 | +from multilingualprogramming.codegen.wat_generator import WATCodeGenerator |
| 386 | +try: |
| 387 | + _toks = Lexer(_repl_code, language=_repl_lang).tokenize() |
| 388 | + _prog = Parser(_toks, source_language=_repl_lang).parse() |
| 389 | + _wat_out = WATCodeGenerator().generate(_prog) |
| 390 | + _wat_err = '' |
| 391 | +except Exception as _e: |
| 392 | + _wat_out = '' |
| 393 | + _wat_err = str(_e) |
| 394 | +`); |
| 395 | + const watSrc = _pyodide.globals.get('_wat_out'); |
| 396 | + const watErr = _pyodide.globals.get('_wat_err'); |
| 397 | + if (watErr) { |
| 398 | + output.innerHTML = `<pre class="output-stderr">${watErr}</pre>`; |
| 399 | + delete output.dataset.mode; |
| 400 | + } else { |
297 | 401 | output.innerHTML = ''; |
298 | 402 | const pre = document.createElement('pre'); |
299 | 403 | pre.className = 'output-wat'; |
300 | | - pre.textContent = text; |
| 404 | + pre.textContent = watSrc; |
301 | 405 | output.appendChild(pre); |
302 | | - }) |
303 | | - .catch(() => { |
304 | | - output.innerHTML = |
305 | | - '<span class="output-stderr">WAT not available — build the WASM module first.</span>'; |
306 | | - delete output.dataset.mode; |
307 | | - }); |
| 406 | + } |
| 407 | + } catch (err) { |
| 408 | + output.innerHTML = `<pre class="output-stderr">${err}</pre>`; |
| 409 | + delete output.dataset.mode; |
| 410 | + } |
308 | 411 | }); |
309 | 412 |
|
310 | 413 | /* Ctrl/Cmd+Enter to run. */ |
|
0 commit comments