about summary refs log tree commit diff stats
path: root/js/baba-yaga/scratch/js/repl.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/baba-yaga/scratch/js/repl.js')
-rw-r--r--js/baba-yaga/scratch/js/repl.js226
1 files changed, 226 insertions, 0 deletions
diff --git a/js/baba-yaga/scratch/js/repl.js b/js/baba-yaga/scratch/js/repl.js
new file mode 100644
index 0000000..b9c878d
--- /dev/null
+++ b/js/baba-yaga/scratch/js/repl.js
@@ -0,0 +1,226 @@
+// repl.js - Simple multi-line REPL for the Baba Yaga language (Node/Bun)
+// - Enter inserts a newline; type :run (or a single .) on its own line to execute
+// - :reset clears the current input buffer
+// - :clear clears the session (prior program)
+// - :load <path> loads a .baba file into the session
+// - :quit / :exit exits
+// - :help prints commands
+
+import fs from 'fs';
+import os from 'os';
+import { evaluate, makeCodeFrame } from './runner.js';
+import { createLexer } from './src/core/lexer.js';
+import { createParser } from './src/core/parser.js';
+
+// Synchronous line input for TTY. Keep it small and dependency-free.
+function readLineSync(promptText = '') {
+  if (promptText) process.stdout.write(promptText);
+  const fd = 0; // stdin
+  const buf = Buffer.alloc(1);
+  let line = '';
+  while (true) {
+    const bytes = fs.readSync(fd, buf, 0, 1, null);
+    if (bytes === 0) break; // EOF
+    const ch = buf.toString('utf8');
+    // Ctrl+C
+    if (ch === '\u0003') {
+      process.stdout.write('\n');
+      process.exit(0);
+    }
+    if (ch === '\n' || ch === '\r') break;
+    line += ch;
+  }
+  return line;
+}
+
+function countLines(text) {
+  if (!text) return 0;
+  return text.split(/\r?\n/).length;
+}
+
+function describeType(value) {
+  if (value && typeof value.value === 'number') {
+    return value.isFloat ? 'Float' : 'Int';
+  }
+  if (typeof value === 'number') {
+    return Number.isInteger(value) ? 'Int' : 'Float';
+  }
+  if (typeof value === 'string') return 'String';
+  if (typeof value === 'boolean') return 'Bool';
+  if (Array.isArray(value)) return 'List';
+  if (value && value.type === 'Object' && value.properties) return 'Table';
+  if (value && value.type === 'Result') return 'Result';
+  if (typeof value === 'undefined') return 'Unit';
+  return 'Unknown';
+}
+
+function displayValue(value) {
+  if (value && typeof value.value === 'number') return String(value.value);
+  if (Array.isArray(value)) return JSON.stringify(value.map(displayValue));
+  if (value && typeof value === 'object') {
+    if (value.type === 'NativeFunction' || value.type === 'Function') return '<fn>';
+    if (value.type === 'Object' && value.properties instanceof Map) {
+      const obj = Object.fromEntries(Array.from(value.properties.entries()).map(([k, v]) => [k, displayValue(v)]));
+      return JSON.stringify(obj);
+    }
+  }
+  return String(value);
+}
+
+function printHelp() {
+  console.log(`Commands:\n\
+  :run           Execute current buffer (or use a single '.' line)\n\
+  :reset         Clear current input buffer\n\
+  :clear         Clear entire session (prior program)\n\
+  :load <path>   Load a .baba file into the session\n\
+  :help          Show this help\n\
+  :quit | :exit  Exit`);
+}
+
+(function main() {
+  let priorSource = '';
+  let buffer = '';
+  const host = {
+    io: {
+      out: (...xs) => console.log(...xs.map(displayValue)),
+      in: () => readLineSync('input> '),
+    },
+  };
+
+  console.log('Baba Yaga REPL (multiline). Type :help for commands.');
+
+  while (true) {
+    const prompt = buffer ? '... ' : 'baba> ';
+    const line = readLineSync(prompt);
+
+    const trimmed = line.trim();
+    if (trimmed === ':quit' || trimmed === ':exit') break;
+    if (trimmed === ':help') { printHelp(); continue; }
+    if (trimmed === ':reset') { buffer = ''; continue; }
+    if (trimmed === ':clear') { priorSource = ''; buffer = ''; console.log('(session cleared)'); continue; }
+    if (trimmed === ':load') { console.error('Usage: :load <path>'); continue; }
+    if (trimmed.startsWith(':load')) {
+      let pathArg = trimmed.slice(5).trim(); // remove ':load'
+      if (!pathArg) { console.error('Usage: :load <path>'); continue; }
+      // Strip surrounding single/double quotes
+      if ((pathArg.startsWith('"') && pathArg.endsWith('"')) || (pathArg.startsWith("'") && pathArg.endsWith("'"))) {
+        pathArg = pathArg.slice(1, -1);
+      }
+      // Expand ~ to home directory
+      if (pathArg.startsWith('~')) {
+        pathArg = pathArg.replace(/^~(?=\/|$)/, os.homedir());
+      }
+      const loadPath = pathArg;
+      try {
+        const fileSource = fs.readFileSync(loadPath, 'utf8');
+        // Parse-only to validate. Do not execute on :load
+        try {
+          const lexer = createLexer(fileSource);
+          const tokens = lexer.allTokens();
+          const parser = createParser(tokens);
+          parser.parse();
+          priorSource = priorSource + '\n' + fileSource + '\n';
+          console.log(`Loaded ${loadPath}. Type :run to execute.`);
+        } catch (parseErr) {
+          const message = parseErr && parseErr.message ? parseErr.message : String(parseErr);
+          const match = / at (\d+):(\d+)/.exec(message);
+          const line = match ? Number(match[1]) : undefined;
+          const column = match ? Number(match[2]) : undefined;
+          const frame = makeCodeFrame(fileSource, line, column);
+          console.error(`Failed to parse ${loadPath}: ${message}`);
+          if (frame) console.error(frame);
+        }
+      } catch (e) {
+        console.error(`Failed to load ${loadPath}: ${e.message}`);
+      }
+      continue;
+    }
+
+    // Execute current buffer or previously loaded program
+    if (trimmed === ':run' || trimmed === '.') {
+      const hasBuffer = Boolean(buffer.trim());
+      const hasPrior = Boolean(priorSource.trim());
+      if (!hasBuffer && !hasPrior) { console.log('(empty)'); buffer = ''; continue; }
+      const combined = hasBuffer ? (priorSource + '\n' + buffer + '\n') : priorSource;
+      const res = evaluate(combined, host);
+      if (res.ok) {
+        const value = res.value;
+        const type = describeType(value);
+        console.log('— input —');
+        if (hasBuffer) {
+          process.stdout.write(buffer);
+        } else {
+          console.log('(loaded program)');
+        }
+        if (typeof value !== 'undefined') {
+          console.log('— result —');
+          console.log(`${displayValue(value)} : ${type}`);
+        } else {
+          console.log('— result —');
+          console.log('Unit');
+        }
+        priorSource = combined; // commit
+        buffer = hasBuffer ? '' : buffer;
+      } else {
+        // Prefer rendering code-frame relative to the buffer if possible
+        const linesBefore = countLines(priorSource);
+        const errLine = res.error.line;
+        if (hasBuffer && errLine && errLine > linesBefore) {
+          const localLine = errLine - linesBefore;
+          const localFrame = makeCodeFrame(buffer, localLine, res.error.column || 1);
+          console.error(res.error.message);
+          if (localFrame) console.error(localFrame);
+        } else {
+          console.error(res.error.message);
+          if (res.error.codeFrame) console.error(res.error.codeFrame);
+        }
+        // do not commit buffer
+      }
+      continue;
+    }
+
+    // Accumulate multi-line input
+    buffer += line + '\n';
+
+    // Immediate execution if current buffer ends with a double semicolon
+    // Treat the second semicolon as a submit marker; execute with a single trailing semicolon
+    const trimmedBuf = buffer.trimEnd();
+    if (trimmedBuf.endsWith(';;')) {
+      const submitBuffer = buffer.replace(/;\s*$/,''); // drop one trailing ';' for valid syntax
+      const combined = priorSource + '\n' + submitBuffer + '\n';
+      const res = evaluate(combined, host);
+      if (res.ok) {
+        const value = res.value;
+        const type = describeType(value);
+        console.log('— input —');
+        process.stdout.write(submitBuffer.endsWith('\n') ? submitBuffer : submitBuffer + '\n');
+        if (typeof value !== 'undefined') {
+          console.log('— result —');
+          console.log(`${displayValue(value)} : ${type}`);
+        } else {
+          console.log('— result —');
+          console.log('Unit');
+        }
+        priorSource = combined; // commit
+        buffer = '';
+      } else {
+        const linesBefore = countLines(priorSource);
+        const errLine = res.error.line;
+        if (errLine && errLine > linesBefore) {
+          const localLine = errLine - linesBefore;
+          const localFrame = makeCodeFrame(submitBuffer, localLine, res.error.column || 1);
+          console.error(res.error.message);
+          if (localFrame) console.error(localFrame);
+        } else {
+          console.error(res.error.message);
+          if (res.error.codeFrame) console.error(res.error.codeFrame);
+        }
+        // keep buffer for further editing unless you prefer clearing it
+      }
+    }
+  }
+
+  console.log('Bye.');
+  process.exit(0);
+})();
+