// 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 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 ''; 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 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 '); continue; } if (trimmed.startsWith(':load')) { let pathArg = trimmed.slice(5).trim(); // remove ':load' if (!pathArg) { console.error('Usage: :load '); 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); })();