about summary refs log tree commit diff stats
path: root/js/baba-yaga/web/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/baba-yaga/web/app.js')
-rw-r--r--js/baba-yaga/web/app.js497
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();
+});
+