diff options
Diffstat (limited to 'js/baba-yaga/web/app.js')
-rw-r--r-- | js/baba-yaga/web/app.js | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/js/baba-yaga/web/app.js b/js/baba-yaga/web/app.js new file mode 100644 index 0000000..ad92716 --- /dev/null +++ b/js/baba-yaga/web/app.js @@ -0,0 +1,497 @@ +import { createLexer } from '../lexer.js'; +import { createParser } from '../parser.js'; +import { createInterpreter } from '../interpreter.js'; + +/** + * Baba Yaga REPL Application + * A mobile-first, accessible CLI-style REPL for the Baba Yaga language + */ + +class BabaYagaREPL { + constructor() { + this.history = []; + this.historyIndex = -1; + this.sessionCode = ''; + + // DOM elements + this.messagesEl = document.getElementById('messages'); + this.inputEl = document.getElementById('input'); + this.sendBtn = document.getElementById('send'); + this.clearBtn = document.getElementById('clear'); + this.loadBtn = document.getElementById('load'); + this.fileInputEl = document.getElementById('fileInput'); + this.examplesBtn = document.getElementById('examples'); + this.helpBtn = document.getElementById('help'); + + this.setupEventListeners(); + this.showWelcome(); + } + + setupEventListeners() { + // Execute code + this.sendBtn.addEventListener('click', () => this.executeCode()); + this.inputEl.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.executeCode(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + this.navigateHistory(-1); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + this.navigateHistory(1); + } + }); + + // Auto-resize textarea + this.inputEl.addEventListener('input', () => this.autoResize()); + + // Clear output + this.clearBtn.addEventListener('click', () => this.clearOutput()); + + // Load file + this.loadBtn.addEventListener('click', () => this.fileInputEl.click()); + this.fileInputEl.addEventListener('change', (e) => this.handleFileLoad(e)); + + // Load examples + this.examplesBtn.addEventListener('click', () => this.loadExamples()); + + // Help + this.helpBtn.addEventListener('click', () => this.showHelp()); + + // Focus management + this.inputEl.focus(); + } + + showWelcome() { + this.addMessage('Baba Yaga REPL', 'info'); + this.addMessage('Type /help for available commands', 'info'); + } + + executeCode() { + const code = this.inputEl.value.trim(); + if (!code) return; + + // Add to history + this.addToHistory(code); + + // Display input + this.addInputMessage(code); + + try { + // Check for slash commands + if (code.startsWith('/')) { + this.handleSlashCommand(code); + this.clearInput(); + return; + } + + // Execute Baba Yaga code with session persistence + const result = this.evaluateWithSession(code); + + if (result.ok) { + if (result.value !== undefined) { + this.addMessage(this.formatValue(result.value), 'output'); + } + } else { + this.addMessage(`Error: ${result.error.message}`, 'error'); + if (result.error.codeFrame) { + this.addMessage(result.error.codeFrame, 'error'); + } + } + } catch (error) { + this.addMessage(`Unexpected error: ${error.message}`, 'error'); + } + + this.clearInput(); + } + + evaluate(source) { + try { + const lexer = createLexer(source); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const host = { + io: { + out: (...args) => { + const text = args.map(arg => this.formatValue(arg)).join(' '); + this.addMessage(text, 'output'); + }, + in: () => { + // For now, return empty string - could be enhanced with stdin + return ''; + } + } + }; + + const interpreter = createInterpreter(ast, host); + const value = interpreter.interpret(); + + return { ok: true, value }; + } catch (error) { + const message = error?.message || String(error); + const lineMatch = / at (\d+):(\d+)/.exec(message); + const line = lineMatch ? Number(lineMatch[1]) : null; + const column = lineMatch ? Number(lineMatch[2]) : null; + const codeFrame = this.makeCodeFrame(source, line, column); + + return { + ok: false, + error: { + message, + line, + column, + codeFrame + } + }; + } + } + + evaluateWithSession(source) { + try { + // Combine session code with new code + const fullSource = this.sessionCode + source; + + const lexer = createLexer(fullSource); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const host = { + io: { + out: (...args) => { + const text = args.map(arg => this.formatValue(arg)).join(' '); + this.addMessage(text, 'output'); + }, + in: () => { + // For now, return empty string - could be enhanced with stdin + return ''; + } + } + }; + + const interpreter = createInterpreter(ast, host); + const value = interpreter.interpret(); + + // If successful, add to session code + this.sessionCode += source + '\n'; + + return { ok: true, value }; + } catch (error) { + const message = error?.message || String(error); + const lineMatch = / at (\d+):(\d+)/.exec(message); + const line = lineMatch ? Number(lineMatch[1]) : null; + const column = lineMatch ? Number(lineMatch[2]) : null; + const codeFrame = this.makeCodeFrame(source, line, column); + + return { + ok: false, + error: { + message, + line, + column, + codeFrame + } + }; + } + } + + formatValue(value) { + if (value === null || value === undefined) { + return 'null'; + } + + if (typeof value === 'number') { + return value.toString(); + } + + if (typeof value === 'string') { + return value; + } + + if (Array.isArray(value)) { + return JSON.stringify(value); + } + + if (value && typeof value === 'object') { + if (value.type === 'Object' && value.properties) { + const obj = {}; + for (const [key, val] of value.properties.entries()) { + obj[key] = this.formatValue(val); + } + return JSON.stringify(obj, null, 2); + } + + if (value.value !== undefined) { + return value.value.toString(); + } + + // Handle native functions - don't show the function object + if (value.type === 'NativeFunction') { + return ''; // Return empty string for native functions + } + + // Handle function objects - don't show the function definition + if (value.type === 'Function') { + return ''; // Return empty string for function objects + } + } + + return JSON.stringify(value, null, 2); + } + + makeCodeFrame(source, line, column) { + if (!line || !column) return ''; + + const lines = source.split(/\r?\n/); + const idx = line - 1; + const context = [idx - 1, idx, idx + 1].filter(i => i >= 0 && i < lines.length); + const pad = n => String(n + 1).padStart(4, ' '); + const caret = ' '.repeat(column - 1) + '^'; + const rows = context.map(i => `${pad(i)} | ${lines[i]}`); + rows.splice(context.indexOf(idx) + 1, 0, ` | ${caret}`); + return rows.join('\n'); + } + + addMessage(text, type = 'output') { + const messageDiv = document.createElement('div'); + messageDiv.className = 'message'; + + const contentDiv = document.createElement('div'); + contentDiv.className = `message-${type}`; + contentDiv.innerHTML = text.replace(/\n/g, '<br>'); + + messageDiv.appendChild(contentDiv); + this.messagesEl.appendChild(messageDiv); + + // Auto-scroll to bottom after adding message + this.scrollToBottom(); + } + + addInputMessage(code) { + const messageDiv = document.createElement('div'); + messageDiv.className = 'message'; + + const inputDiv = document.createElement('div'); + inputDiv.className = 'message-input'; + + const promptDiv = document.createElement('div'); + promptDiv.className = 'prompt'; + promptDiv.textContent = '>'; + + const codeDiv = document.createElement('div'); + codeDiv.className = 'code'; + codeDiv.textContent = code; + + inputDiv.appendChild(promptDiv); + inputDiv.appendChild(codeDiv); + messageDiv.appendChild(inputDiv); + + this.messagesEl.appendChild(messageDiv); + + // Auto-scroll to bottom after adding input message + this.scrollToBottom(); + } + + clearInput() { + this.inputEl.value = ''; + this.autoResize(); + } + + clearOutput() { + this.messagesEl.innerHTML = ''; + this.sessionCode = ''; + this.showWelcome(); + } + + autoResize() { + this.inputEl.style.height = 'auto'; + this.inputEl.style.height = Math.min(this.inputEl.scrollHeight, 120) + 'px'; + } + + scrollToBottom() { + this.messagesEl.scrollTop = this.messagesEl.scrollHeight; + } + + addToHistory(code) { + this.history.push(code); + this.historyIndex = this.history.length; + } + + navigateHistory(direction) { + if (this.history.length === 0) return; + + this.historyIndex += direction; + + if (this.historyIndex < 0) { + this.historyIndex = 0; + } else if (this.historyIndex >= this.history.length) { + this.historyIndex = this.history.length; + this.inputEl.value = ''; + } else { + this.inputEl.value = this.history[this.historyIndex]; + } + + this.autoResize(); + } + + handleSlashCommand(command) { + const cmd = command.toLowerCase(); + + switch (cmd) { + case '/help': + this.showHelp(); + break; + case '/clear': + this.clearOutput(); + break; + case '/examples': + this.loadExamples(); + break; + case '/run': + this.runSession(); + break; + case '/load': + this.fileInputEl.click(); + break; + default: + this.addMessage(`Unknown command: ${command}. Type /help for available commands.`, 'error'); + } + } + + runSession() { + if (!this.sessionCode.trim()) { + this.addMessage('No code in session to run. Load a file or type some code first.', 'info'); + return; + } + + this.addMessage('Running session code...', 'info'); + + try { + const result = this.evaluate(this.sessionCode); + + if (result.ok) { + if (result.value !== undefined) { + this.addMessage(this.formatValue(result.value), 'output'); + } + this.addMessage('Session executed successfully.', 'info'); + } else { + this.addMessage(`Error: ${result.error.message}`, 'error'); + if (result.error.codeFrame) { + this.addMessage(result.error.codeFrame, 'error'); + } + } + } catch (error) { + this.addMessage(`Unexpected error: ${error.message}`, 'error'); + } + } + + showHelp() { + const helpText = `Available slash commands: + /help - Show this help message + /clear - Clear the output + /examples - Load example code + /load - Load a .baba file + /run - Execute all code in session + +Keyboard shortcuts: + Cmd/Ctrl + Enter - Execute code + ↑/↓ - Navigate history + +Baba Yaga language features: + - Immutable variables: name : value; + - Functions: name : params -> body; + - Pattern matching: when expr is pattern then result; + - Lists: [1, 2, 3] + - Tables: {key: value} + - String concatenation: str1 .. str2 + - IO: io.out "Hello"; io.in + +Example function definition: + add : x y -> x + y; + result : add 3 4;`; + + this.addMessage(helpText, 'info'); + } + + async handleFileLoad(event) { + const file = event.target.files?.[0]; + if (!file) return; + + try { + const text = await file.text(); + + // Validate the file content by trying to parse it + const lexer = createLexer(text); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + parser.parse(); + + // If parsing succeeds, add to session and display + this.sessionCode += text + '\n'; + + this.addMessage(`Loaded ${file.name}`, 'info'); + this.addInputMessage(text); + this.addMessage('File loaded successfully. Click "Run" to execute or add more code.', 'info'); + + } catch (error) { + const message = error?.message || String(error); + this.addMessage(`Failed to load ${file.name}: ${message}`, 'error'); + } finally { + // Reset the input so the same file can be selected again + event.target.value = ''; + } + } + + loadExamples() { + const examples = [ + { + title: 'Basic arithmetic', + code: `result : 2 + 3 * 4; +io.out result;` + }, + { + title: 'Simple function', + code: `add : x y -> x + y; +result : add 5 3; +io.out result;` + }, + { + title: 'Variables and expressions', + code: `a : 10; +b : 20; +sum : a + b; +io.out sum;` + }, + { + title: 'String operations', + code: `greeting : "Hello"; +name : "World"; +message : greeting .. " " .. name; +io.out message;` + }, + { + title: 'String functions', + code: `io.out str.substring "hello world" 0 5; +io.out str.replace "hello hello" "hello" "hi";` + } + ]; + + this.addMessage('Available examples:', 'info'); + + examples.forEach(example => { + this.addMessage(`// ${example.title}`, 'output'); + this.addInputMessage(example.code); + }); + + this.addMessage('Copy and paste any example above to try it out!', 'info'); + } +} + +// Initialize the REPL when the page loads +document.addEventListener('DOMContentLoaded', () => { + new BabaYagaREPL(); +}); + |