From e6b88c9dec881382f457d5576011ea54e3d31a09 Mon Sep 17 00:00:00 2001 From: Sidi Liang <1467329765@qq.com> Date: Tue, 5 Nov 2024 01:25:09 +0800 Subject: [PATCH] [web] Added Web REPL server and frontend demo --- nasal-web-app/public/repl.html | 240 +++++++++++++++++++++++++++++++ nasal-web-app/public/repl.js | 249 +++++++++++++++++++++++++++++++++ nasal-web-app/server_repl.js | 176 +++++++++++++++++++++++ 3 files changed, 665 insertions(+) create mode 100644 nasal-web-app/public/repl.html create mode 100644 nasal-web-app/public/repl.js create mode 100644 nasal-web-app/server_repl.js diff --git a/nasal-web-app/public/repl.html b/nasal-web-app/public/repl.html new file mode 100644 index 0000000..9481424 --- /dev/null +++ b/nasal-web-app/public/repl.html @@ -0,0 +1,240 @@ + + + + + + Nasal Interpreter Web REPL + + + + + +
+
+

Nasal Interpreter Web REPL

+

Interactive Read-Eval-Print Loop for Nasal

+

Powered by ValKmjolnir's Nasal Interpreter, Web App by LIANG Sidi

+
+ +
+
+
Welcome to Nasal Web REPL Demo!
+
+
+ >>> + +
+
+ +
+ +
+ +
+

Example Commands:

+ + + +
+
+ + + + \ No newline at end of file diff --git a/nasal-web-app/public/repl.js b/nasal-web-app/public/repl.js new file mode 100644 index 0000000..742120e --- /dev/null +++ b/nasal-web-app/public/repl.js @@ -0,0 +1,249 @@ +let replSessionId = null; +let multilineInput = []; +let historyIndex = -1; +let commandHistory = []; +let multilineBuffer = []; +let isMultilineMode = false; + +// Initialize REPL +async function initRepl() { + try { + const response = await fetch('/repl/init', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + const data = await response.json(); + if (data.sessionId) { + replSessionId = data.sessionId; + console.log('REPL session initialized:', replSessionId); + + // Display version info + appendOutput('Nasal REPL interpreter version ' + data.version); + appendOutput('Type your code below and press Enter to execute.'); + appendOutput('Use Shift+Enter for multiline input.\n'); + showPrompt(); + } else { + throw new Error('Failed to initialize REPL session'); + } + } catch (err) { + appendOutput(`Error: ${err.message}`, 'error-line'); + } +} + +// Format error messages to match command-line REPL +function formatError(error) { + // Split the error message into lines + const lines = error.split('\n'); + return lines.map(line => { + // Add appropriate indentation for the error pointer + if (line.includes('-->')) { + return ' ' + line; + } else if (line.includes('^')) { + return ' ' + line; + } + return line; + }).join('\n'); +} + +// Handle input +const input = document.getElementById('repl-input'); +input.addEventListener('keydown', async (e) => { + if (e.key === 'Enter') { + if (e.shiftKey) { + // Shift+Enter: add newline + const pos = input.selectionStart; + const value = input.value; + input.value = value.substring(0, pos) + '\n' + value.substring(pos); + input.selectionStart = input.selectionEnd = pos + 1; + input.style.height = 'auto'; + input.style.height = input.scrollHeight + 'px'; + e.preventDefault(); + return; + } + e.preventDefault(); + + const code = input.value.trim(); + + // Skip empty lines but still show prompt + if (!code) { + showPrompt(isMultilineMode ? '... ' : '>>> '); + return; + } + + try { + // First check if input is complete + const checkResponse = await fetch('/repl/eval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: replSessionId, + line: code, + check: true, + buffer: multilineBuffer // Send existing buffer + }) + }); + + const checkData = await checkResponse.json(); + + if (checkData.needsMore) { + // Add to multiline buffer and show continuation prompt + multilineBuffer.push(code); + isMultilineMode = true; + + // Display the input with continuation prompt + appendOutput(code, 'input-line', multilineBuffer.length === 1 ? '>>> ' : '... '); + + input.value = ''; + showPrompt('... '); + return; + } + + // If we were in multiline mode, add the final line + if (isMultilineMode) { + multilineBuffer.push(code); + } + + // Get the complete code to evaluate + const fullCode = isMultilineMode ? + multilineBuffer.join('\n') : code; + + // Display the input + appendOutput(code, 'input-line', isMultilineMode ? '... ' : '>>> '); + + // Evaluate the code + const response = await fetch('/repl/eval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: replSessionId, + line: fullCode + }) + }); + + const data = await response.json(); + + if (data.error) { + appendOutput(formatError(data.error.trim()), 'error-line'); + } else if (data.result) { + handleResult(data.result); + } + + // Reset multiline state + multilineBuffer = []; + isMultilineMode = false; + input.value = ''; + showPrompt('>>> '); + + } catch (err) { + appendOutput(`Error: ${err.message}`, 'error-line'); + multilineBuffer = []; + isMultilineMode = false; + input.value = ''; + showPrompt('>>> '); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (historyIndex > 0) { + historyIndex--; + input.value = commandHistory[historyIndex]; + } + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + if (historyIndex < commandHistory.length - 1) { + historyIndex++; + input.value = commandHistory[historyIndex]; + } else { + historyIndex = commandHistory.length; + input.value = ''; + } + } +}); + +// Auto-resize input +input.addEventListener('input', () => { + input.style.height = 'auto'; + input.style.height = input.scrollHeight + 'px'; +}); + +// Show prompt and scroll to bottom +function showPrompt(prompt = '>>> ') { + const promptSpan = document.querySelector('.repl-prompt'); + if (promptSpan) { + promptSpan.textContent = prompt; + } +} + +// Append output to REPL +function appendOutput(text, className = '', prefix = '') { + const output = document.getElementById('repl-output'); + const line = document.createElement('div'); + line.className = `output-line ${className}`; + + line.innerHTML = prefix + formatErrorMessage(text); + + + output.appendChild(line); + output.scrollTop = output.scrollHeight; +} + +// Clear REPL +function clearREPL() { + const output = document.getElementById('repl-output'); + output.innerHTML = ''; + appendOutput('Screen cleared', 'system-message'); + showPrompt(); +} + +// Example snippets +const examples = { + basic: `var x = 1011 + 1013; +println("x = ", x);`, + + loops: `var sum = 0; +for(var i = 1; i <= 5; i += 1) { + sum += i; +} +println("Sum:", sum);`, + + functions: `var factorial = func(n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +}; +factorial(5);` +}; + +// Insert example into input +function insertExample(type) { + input.value = examples[type]; + input.style.height = 'auto'; + input.style.height = input.scrollHeight + 'px'; + input.focus(); +} + +// Initialize REPL on page load +window.addEventListener('load', initRepl); + +// Add these utility functions +function formatErrorMessage(text) { + // Replace ANSI escape codes with CSS classes + return text + // Remove any existing formatting first + .replace(/\u001b\[\d+(?:;\d+)*m/g, '') + // Format the error line + .replace(/^parse: (.+)$/m, 'parse: $1') + // Format the file location + .replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '--> $1$2:$3') + // Format the code line + .replace(/^(\d+ \|)(.*)$/m, '$1$2') + // Format the error pointer + .replace(/^(\s*\|)(\s*)(\^+)$/m, '$1$2$3'); +} + +function handleResult(result) { + const lines = result.split('\n'); + lines.forEach(line => { + if (line.trim()) { + appendOutput(line.trim(), 'result-line'); + } + }); +} \ No newline at end of file diff --git a/nasal-web-app/server_repl.js b/nasal-web-app/server_repl.js new file mode 100644 index 0000000..76153a3 --- /dev/null +++ b/nasal-web-app/server_repl.js @@ -0,0 +1,176 @@ +const express = require('express'); +const ffi = require('ffi-napi'); +const path = require('path'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +// Parse command line arguments +const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .option('verbose', { + alias: 'v', + type: 'boolean', + description: 'Run with verbose logging' + }) + .option('port', { + alias: 'p', + type: 'number', + description: 'Port to run the server on', + default: 3001 + }) + .option('host', { + type: 'string', + description: 'Host to run the server on', + default: 'localhost' + }) + .help() + .alias('help', 'h') + .version() + .argv; + +const app = express(); +app.use(express.json()); +app.use(express.static('public')); + + +// Load Nasal REPL library functions +const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), { + 'nasal_repl_init': ['pointer', []], + 'nasal_repl_cleanup': ['void', ['pointer']], + 'nasal_repl_eval': ['string', ['pointer', 'string']], + 'nasal_repl_is_complete': ['int', ['pointer', 'string']], + 'nasal_repl_get_version': ['string', []], +}); + +// Store active REPL sessions +const replSessions = new Map(); + +// Clean up inactive sessions periodically (30 minutes timeout) +const SESSION_TIMEOUT = 30 * 60 * 1000; +setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of replSessions.entries()) { + if (now - session.lastAccess > SESSION_TIMEOUT) { + if (argv.verbose) { + console.log(`Cleaning up inactive session: ${sessionId}`); + } + nasalLib.nasal_repl_cleanup(session.context); + replSessions.delete(sessionId); + } + } +}, 60000); // Check every minute + +app.post('/repl/init', (req, res) => { + try { + const ctx = nasalLib.nasal_repl_init(); + const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const version = nasalLib.nasal_repl_get_version(); + + replSessions.set(sessionId, { + context: ctx, + lastAccess: Date.now() + }); + + if (argv.verbose) { + console.log(`New REPL session initialized: ${sessionId}`); + } + + res.json({ + sessionId, + version + }); + } catch (err) { + if (argv.verbose) { + console.error('Failed to initialize REPL session:', err); + } + res.status(500).json({ error: 'Failed to initialize REPL session' }); + } +}); + +app.post('/repl/eval', (req, res) => { + const { sessionId, line, check, buffer } = req.body; + + if (!sessionId || !replSessions.has(sessionId)) { + return res.status(400).json({ error: 'Invalid or expired session' }); + } + + if (!line) { + return res.status(400).json({ error: 'No code provided' }); + } + + try { + const session = replSessions.get(sessionId); + session.lastAccess = Date.now(); + + if (check) { + const codeToCheck = buffer ? [...buffer, line].join('\n') : line; + const isComplete = nasalLib.nasal_repl_is_complete(session.context, codeToCheck); + + if (isComplete === 1) { + return res.json({ needsMore: true }); + } else if (isComplete === -1) { + return res.json({ error: 'Invalid input' }); + } + } + + const result = nasalLib.nasal_repl_eval(session.context, line); + + if (argv.verbose) { + console.log(`REPL evaluation for session ${sessionId}:`, { line, result }); + } + + res.json({ result }); + } catch (err) { + if (argv.verbose) { + console.error(`REPL evaluation error for session ${sessionId}:`, err); + } + res.status(500).json({ error: err.message }); + } +}); + +app.post('/repl/cleanup', (req, res) => { + const { sessionId } = req.body; + + if (!sessionId || !replSessions.has(sessionId)) { + return res.status(400).json({ error: 'Invalid session' }); + } + + try { + const session = replSessions.get(sessionId); + nasalLib.nasal_repl_cleanup(session.context); + replSessions.delete(sessionId); + + if (argv.verbose) { + console.log(`REPL session cleaned up: ${sessionId}`); + } + + res.json({ message: 'Session cleaned up successfully' }); + } catch (err) { + if (argv.verbose) { + console.error(`Failed to cleanup session ${sessionId}:`, err); + } + res.status(500).json({ error: err.message }); + } +}); + +// Handle cleanup on server shutdown +process.on('SIGINT', () => { + console.log('\nCleaning up REPL sessions before exit...'); + for (const [sessionId, session] of replSessions.entries()) { + try { + nasalLib.nasal_repl_cleanup(session.context); + if (argv.verbose) { + console.log(`Cleaned up session: ${sessionId}`); + } + } catch (err) { + console.error(`Error cleaning up session ${sessionId}:`, err); + } + } + process.exit(0); +}); + +const PORT = argv.port || 3001; +app.listen(PORT, () => { + console.log(`REPL Server running on http://localhost:${PORT}`); + if (argv.verbose) console.log('Verbose logging enabled'); +}); \ No newline at end of file