diff options
Diffstat (limited to 'js/baba-yaga/scratch/js/repl.js')
-rw-r--r-- | js/baba-yaga/scratch/js/repl.js | 226 |
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); +})(); + |