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, '
'); 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(); });