@jplorg/jpl
Version:
JPL interpreter
249 lines (212 loc) • 6.49 kB
JavaScript
const fs = require('fs');
const os = require('os');
const path = require('path');
const readline = require('readline');
const { default: jpl } = require('@jplorg/jpl');
const pkg = require('@jplorg/jpl/package.json');
const replKeys = [':', '!'];
const defaultReplKey = replKeys[0];
const defaultPrompt = '> ';
const multilinePrompt = '… ';
let multilineInput;
const homeDir = os.homedir();
const historyFile = homeDir ? path.join(homeDir, '.jpl_repl_history') : undefined;
const muted = !process.stdin.isTTY;
const rl = readline.createInterface({
input: process.stdin,
output: muted ? undefined : process.stdout,
history: readHistory(),
historySize: 50,
removeHistoryDuplicates: true,
prompt: defaultPrompt,
});
function readHistory() {
if (!historyFile) return [];
try {
return fs
.readFileSync(historyFile)
.toString()
.split(/\r?\n|\r/)
.filter(Boolean)
.reverse();
} catch {
return [];
}
}
if (historyFile) {
rl.on('history', (history) => {
try {
fs.writeFileSync(historyFile, history.filter(Boolean).reverse().join('\n') + '\n', {
mode: 0o600,
});
} catch {
// ignore
}
});
}
rl.on('close', () => {
process.exit(0);
});
rl.on('SIGINT', () => {
if (multilineInput != null) {
multilineInput = null;
rl.clearLine();
rl.setPrompt(defaultPrompt);
rl.prompt();
return undefined;
}
if (rl.cursor === 0) return process.exit(0);
process.stdout.write(`\nTo exit, press Ctrl+C again or type ${defaultReplKey}e`);
rl.clearLine();
rl.prompt();
return undefined;
});
let keep;
let inputs;
let measureTime;
main();
async function main() {
if (!muted) {
console.log(`Welcome to JPL v${pkg.version}.`);
console.log(`Type "${defaultReplKey}h" for more information.\n`);
}
const options = await jpl.getOptions();
options.runtime.vars.exit = jpl.nativeFunction(() => {
process.exit(0);
});
options.runtime.vars.clear = jpl.nativeFunction(() => {
console.clear();
return [];
});
rl.prompt();
for await (const line of rl) {
rl.pause();
await handle(line);
rl.resume();
}
}
async function handle(input) {
if (!keep || inputs.length === 0) inputs = [null];
const fullLine = (multilineInput ?? '') + input;
let line = fullLine;
const t = line.trimStart();
if (!t) {
rl.prompt();
return undefined;
}
if (replKeys.some((replKey) => t.startsWith(replKey))) {
const command = (t[1] ?? ' ').toLowerCase();
line = t.substring(2);
switch (command) {
case 'h':
printHelp();
break;
case 'e':
case 'q':
return process.exit(0);
case 'c':
console.clear();
break;
case 'k':
keep = parseBool(line, !keep, 'keep') ?? keep;
break;
case 't':
measureTime = parseBool(line, !measureTime, 'time') ?? measureTime;
break;
case 'i':
// reset prompt after potential previous multiline input
multilineInput = null;
rl.setPrompt(defaultPrompt);
try {
const program = await jpl.parse(line);
console.log(JSON.stringify(program.definition, null, 2));
} catch (err) {
if (jpl.JPLSyntaxError.is(err) && err.at >= err.src.length) {
// program is incomplete -> request additional input
multilineInput = fullLine + '\n';
rl.setPrompt(multilinePrompt);
rl.prompt();
return undefined;
}
if (jpl.JPLSyntaxError.is(err)) console.log(`${err.name ?? 'JPLError'}: ${err.message}`);
else console.log(err.stack);
}
break;
case ' ':
console.log('Error: missing REPL command\n');
printHelp();
break;
default:
console.log(`Error: unrecognized REPL command ${defaultReplKey}${command}\n`);
printHelp();
}
} else {
// reset prompt after potential previous multiline input
multilineInput = null;
rl.setPrompt(defaultPrompt);
try {
const program = await jpl.parse(line);
let before;
let diff;
if (measureTime) before = Date.now();
inputs = await program.run(inputs);
if (measureTime) diff = Date.now() - before;
console.log(inputs.map((output) => JSON.stringify(output, null, 2)).join(', '));
if (measureTime) console.log(` -> took ${diff / 1000}s`);
} catch (err) {
if (jpl.JPLSyntaxError.is(err) && err.at >= err.src.length) {
// program is incomplete -> request additional input
multilineInput = fullLine + '\n';
rl.setPrompt(multilinePrompt);
rl.prompt();
return undefined;
}
if (jpl.JPLSyntaxError.is(err) || jpl.JPLExecutionError.is(err))
console.log(`${err.name ?? 'JPLError'}: ${err.message}`);
else console.log(err.stack);
}
}
rl.prompt();
return undefined;
}
function parseBool(input, defaultValue, label) {
const b = input.trim().toLowerCase();
let v;
if (b.length === 0) v = defaultValue;
else if (b === 'on' || ['true', 'yes', 'enabled'].some((e) => e.startsWith(b))) v = true;
else if (b === 'off' || ['false', 'no', 'disabled'].some((e) => e.startsWith(b))) v = false;
if (typeof v === 'boolean') {
console.log(` -> ${label} ${v ? 'on' : 'off'}`);
return v;
}
console.log(`Error: invalid boolean ${b}`);
return null;
}
function printBool(value) {
return `boolean (${value ? 'on' : 'off'})`;
}
function printHelp() {
const commands = [
['c', '', 'Clear the console screen'],
['e', '', 'Exit the REPL'],
['h', '', 'Print this help message'],
['i', 'program', 'Interpret the specified program without executing it'],
[
'k',
printBool(keep),
'Set whether program output should be kept as input for the next program',
],
['t', printBool(measureTime), 'Set whether execution time should be measured'],
['q', '', 'Exit the REPL'],
];
const aLen = commands.reduce((sum, [, a]) => Math.max(sum, a.length), 0);
console.log(`JPL v${pkg.version} REPL reference\n`);
console.log(
`The following synonymous tokens may be used to precede a command: ${replKeys.join('')}\n`,
);
commands.forEach(([c, a, d]) =>
console.log(`${defaultReplKey}${c} ${a}${' '.repeat(aLen - a.length + 3)}${d}`),
);
console.log('\nPress Ctrl+C to abort current expression, Ctrl+D to exit the REPL');
}