@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
451 lines • 21.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.describeCompletion = describeCompletion;
exports.replCompleter = replCompleter;
exports.completionSuggestion = completionSuggestion;
exports.makeDefaultReplReadline = makeDefaultReplReadline;
exports.handleString = handleString;
exports.toFileStatement = toFileStatement;
exports.extractWatchPath = extractWatchPath;
exports.stopWatching = stopWatching;
exports.startWatching = startWatching;
exports.replProcessAnswer = replProcessAnswer;
exports.repl = repl;
exports.loadReplHistory = loadReplHistory;
/**
* Basically a helper file to allow the main 'flowr' script (located in the source root) to provide its repl
* @module
*/
const prompt_1 = require("./prompt");
const path_input_1 = require("./path-input");
const readline = __importStar(require("readline"));
const ghost_hint_1 = require("./ghost-hint");
const repl_execute_1 = require("./commands/repl-execute");
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const args_1 = require("../../util/text/args");
const ansi_1 = require("../../util/text/ansi");
const repl_commands_1 = require("./commands/repl-commands");
const scripts_info_1 = require("../common/scripts-info");
const retriever_1 = require("../../r-bridge/retriever");
const repl_main_1 = require("./commands/repl-main");
const log_1 = require("../../util/log");
const query_1 = require("../../queries/query");
const repl_signature_1 = require("./commands/repl-signature");
const strings_1 = require("../../util/text/strings");
const instrument_dataflow_count_1 = require("../../dataflow/instrument/instrument-dataflow-count");
const proc_1 = require("../../util/proc");
let _replCompleterKeywords = undefined;
function replCompleterKeywords() {
if (_replCompleterKeywords === undefined) {
_replCompleterKeywords = Array.from((0, repl_commands_1.getCommandNames)(), s => `:${s}`);
}
return _replCompleterKeywords;
}
const defaultHistoryFile = path_1.default.join(os_1.default.tmpdir(), '.flowrhistory');
/** Labels a completion with what it does, see {@link CommandCompletions#labels|labels}. */
function describeCompletion(insert, describe) {
return `${insert} ${ansi_1.ansiFormatter.format(describe, { style: 2 /* FontStyles.Faint */ })}`;
}
function computeCompletions(line, config) {
const splitLine = (0, args_1.splitAtEscapeSensitive)(line);
// did we just type a space (and are starting a new arg right now)?
const startingNewArg = line.endsWith(' ');
// if we typed a command fully already, autocomplete the arguments
if (splitLine.length > 1 || startingNewArg) {
const commandNameColon = replCompleterKeywords().find(k => splitLine[0] === k);
if (commandNameColon) {
let completions = [];
let currentArg = startingNewArg ? '' : splitLine[splitLine.length - 1];
let labels;
let hints;
let preFiltered = false;
const commandName = commandNameColon.slice(1);
const cmd = (0, repl_commands_1.getCommand)(commandName);
if (cmd?.script === true) {
// autocomplete script arguments
const options = scripts_info_1.scripts[commandName].options;
completions = completions.concat((0, scripts_info_1.getValidOptionsForCompletion)(options, splitLine).map(o => `${o} `));
}
else if (commandName.startsWith('query')) {
const { completions: queryCompletions, argumentPart: splitArg, labels: queryLabels, hints: queryHints, preFiltered: queryPreFiltered } = replQueryCompleter(splitLine, startingNewArg, config);
if (splitArg !== undefined) {
currentArg = splitArg;
}
completions = completions.concat(queryCompletions);
labels = queryLabels;
hints = queryHints;
preFiltered = queryPreFiltered ?? false;
}
else if (cmd === repl_signature_1.signatureCommand) {
const { completions: sigCompletions, argumentPart: splitArg } = (0, repl_signature_1.replSignatureCompleter)(splitLine, startingNewArg);
if (splitArg !== undefined) {
currentArg = splitArg;
}
completions = completions.concat(sigCompletions);
}
else if (cmd?.isCodeCommand) {
completions.push(retriever_1.fileProtocol);
completions.push(path_input_1.watchProtocol);
}
// prefix matches when any exist, else fuzzy (subsequence) matches, so e.g. `+sg` still completes to `+specializeConfig`
return [preFiltered ? completions : (0, strings_1.matchByPrefixOrSubsequence)(completions, currentArg), currentArg, labels, hints?.filter(h => h.startsWith(currentArg))];
}
}
// if no command is already typed, just return all commands that match
return [replCompleterKeywords().filter(k => k.startsWith(line)).map(k => `${k} `), line];
}
/**
* Used by the repl to provide automatic completions for a given (partial) input line. Returns every matching
* option, so Tab shows the full menu and only completes when a single option remains (standard readline behavior).
*/
function replCompleter(line, config) {
const [completions, fragment, labels] = computeCompletions(line, config);
if (labels === undefined || completions.length <= 1) {
return [completions, fragment];
}
return [completions.map(c => labels.get(c) ?? c), fragment];
}
/**
* The remaining text of the best (first) completion, i.e. what the inline ghost previews as you type; empty when
* there is nothing to suggest. Independent of {@link replCompleter}: the ghost hints the best match even when Tab
* would still offer several.
*/
function completionSuggestion(line, config) {
if (line.length === 0) {
return '';
}
const [completions, fragment, , hints] = computeCompletions(line, config);
// prefer an insertable completion; otherwise preview a display-only hint (e.g. a `<string>` type placeholder)
const best = completions[0] ?? hints?.[0];
if (best === undefined || !best.startsWith(fragment) || best.length <= fragment.length) {
return '';
}
else if (best === retriever_1.fileProtocol || best === path_input_1.watchProtocol) {
return '';
}
return best.slice(fragment.length);
}
function replQueryCompleter(splitLine, startingNewArg, config) {
const nonEmpty = splitLine.slice(1).map(s => s.trim()).filter(s => s.length > 0);
/* `?<type>` documents a query instead of running it, and completes to the same names */
const asking = !startingNewArg && nonEmpty.length === 1 && nonEmpty[0].startsWith('?');
const queryShorts = asking ? Object.keys(query_1.SupportedQueries).map(q => `?${q}`)
: ['help'].concat(Object.keys(query_1.SupportedQueries).map(q => `@${q}`));
if (asking || nonEmpty.length === 0 || (nonEmpty.length === 1 && queryShorts.some(q => q.startsWith(nonEmpty[0]) && nonEmpty[0] !== q && !startingNewArg))) {
return { completions: queryShorts.map(q => `${q} `) };
}
else {
const q = nonEmpty[0].slice(1);
const queryElement = query_1.SupportedQueries[q];
if (queryElement?.completer) {
return queryElement.completer(nonEmpty.slice(1), startingNewArg, config);
}
}
return { completions: [] };
}
/**
* Produces default readline options for the flowR REPL
*/
function makeDefaultReplReadline(config, historyFile = defaultHistoryFile) {
return {
input: process.stdin,
output: process.stdout,
tabSize: 4,
terminal: true,
history: historyFile ? loadReplHistory(historyFile) : [],
removeHistoryDuplicates: true,
completer: (c) => replCompleter(c, config)
};
}
/**
* Handles a string input for the REPL, returning the parsed string and any remaining input.
*/
function handleString(code) {
return {
rCode: code.length === 0 ? undefined : (0, strings_1.startAndEndsWith)(code, '"') ? JSON.parse(code) : code,
remaining: []
};
}
/** Convert a statement containing {@link watchProtocol} into one using {@link fileProtocol} so it can be executed. */
function toFileStatement(statement) {
return statement.replaceAll(path_input_1.watchProtocol, retriever_1.fileProtocol);
}
/** Return the watch path from the first `watch://<path>` token in `statement`, or `undefined` if none is present. */
function extractWatchPath(statement) {
return /watch:\/\/(\S+)/.exec(statement)?.[1];
}
let activeWatcher;
/** Stop the active watcher. Pass `notify = false` when silently switching to a new watch target. */
function stopWatching(output, notify = true) {
if (activeWatcher !== undefined) {
try {
activeWatcher.close();
}
catch { /* already closed */ }
activeWatcher = undefined;
if (notify) {
output.stdout('Watch mode stopped.');
}
}
}
/** `watchImpl` is injectable so tests can pass a fake `fs.watch`. */
function startWatching(watchPath, output, onFire, watchImpl = fs_1.default.watch) {
const faint = (s) => output.formatter.format(s, { style: 2 /* FontStyles.Faint */ });
const isDir = fs_1.default.existsSync(watchPath) && fs_1.default.statSync(watchPath).isDirectory();
let debounce;
const watcher = watchImpl(watchPath, { recursive: isDir }, () => {
clearTimeout(debounce);
debounce = setTimeout(() => {
output.stdout(faint(`\nChange detected in '${watchPath}', re-running...`));
onFire();
}, 50);
});
output.stdout(faint(`Watching ${isDir ? 'folder' : 'file'} '${watchPath}'. Press Ctrl+C or enter another command to stop.`));
return watcher;
}
async function executeStatement(output, statement, analyzer, allowRSessionAccess) {
const time = Date.now();
const heatMap = new Map();
if (analyzer.inspectContext().config.repl.dfProcessorHeat) {
analyzer.context().config.solver.instrument.dataflowExtractors = (0, instrument_dataflow_count_1.instrumentDataflowCount)(heatMap, map => map.clear());
}
if (statement.startsWith(':')) {
const command = statement.slice(1).split(' ')[0].toLowerCase();
const bold = (s) => output.formatter.format(s, { style: 1 /* FontStyles.Bold */ });
const reportFailure = (e) => {
output.stderr(`${bold(`Failed to execute command ${command}`)}: ${e?.message}. Using the ${bold('--verbose')} flag on startup may provide additional information.\n`);
if (log_1.log.settings.minLevel < 6 /* LogLevel.Fatal */) {
console.error(e);
}
};
try {
const processor = (0, repl_commands_1.getCommand)(command);
if (processor) {
await (0, query_1.genericWrapReplFailIfNoRequest)(async () => {
const remainingLine = statement.slice(command.length + 2).trim();
if (processor.isCodeCommand) {
const args = processor.argsParser(remainingLine);
if (args.rCode) {
const rawPath = args.rCode.startsWith(retriever_1.fileProtocol)
? args.rCode.slice(retriever_1.fileProtocol.length)
: undefined;
const alreadyKnown = rawPath !== undefined
&& analyzer.context().files.getFileByPath(rawPath) !== undefined;
if (!alreadyKnown) {
analyzer.reset();
analyzer.addRequest((0, path_input_1.handlePathLikeInput)(output, args.rCode, analyzer.flowrConfig));
}
}
await processor.fn({ output, analyzer, remainingArgs: args.remaining });
}
else {
await processor.fn({ output, analyzer, remainingLine, allowRSessionAccess });
}
}, output, analyzer);
}
else {
output.stderr(`the command '${command}' is unknown, try ${bold(':help')} for more information\n`);
}
}
catch (e) {
reportFailure(e);
}
}
else {
await (0, repl_execute_1.tryExecuteRShellCommand)({ output, analyzer, remainingLine: statement, allowRSessionAccess });
}
if (analyzer.inspectContext().config.repl.quickStats) {
try {
const duration = Date.now() - time;
console.log(output.formatter.format(`[REPL Stats] Processed in ${duration}ms`, {
style: 3 /* FontStyles.Italic */,
effect: ansi_1.ColorEffect.Foreground,
color: 7 /* Colors.White */
}));
const memoryUsage = process.memoryUsage();
const memoryUsageStr = Object.entries(memoryUsage).map(([key, value]) => `${key}: ${(value / 1024 / 1024).toFixed(2)} MB`).join(', ');
console.log(output.formatter.format(`[REPL Stats] Memory Usage: ${memoryUsageStr}`, {
style: 3 /* FontStyles.Italic */,
effect: ansi_1.ColorEffect.Foreground,
color: 7 /* Colors.White */
}));
}
catch {
// do nothing, this is just a nice-to-have
}
}
if (heatMap.size > 0 && analyzer.inspectContext().config.repl.dfProcessorHeat) {
const sorted = Array.from(heatMap.entries()).sort((a, b) => b[1] - a[1]);
console.log(output.formatter.format('[REPL Stats] Dataflow Processor Heatmap:', {
style: 3 /* FontStyles.Italic */,
effect: ansi_1.ColorEffect.Foreground,
color: 7 /* Colors.White */
}));
const longestKey = Math.max(...Array.from(heatMap.keys(), k => k.length));
const longestValue = Math.max(...Array.from(heatMap.values(), v => v.toString().length));
for (const [rType, count] of sorted) {
console.log(output.formatter.format(` - ${(rType + ':').padEnd(longestKey + 1, ' ')} ${count.toString().padStart(longestValue, ' ')}`, {
style: 3 /* FontStyles.Italic */,
effect: ansi_1.ColorEffect.Foreground,
color: 7 /* Colors.White */
}));
}
}
}
async function replProcessStatement(output, statement, analyzer, allowRSessionAccess) {
stopWatching(output, !statement.includes(path_input_1.watchProtocol));
if (statement.includes(path_input_1.watchProtocol)) {
const watchPath = extractWatchPath(statement);
const effective = toFileStatement(statement);
await executeStatement(output, effective, analyzer, allowRSessionAccess);
if (watchPath) {
try {
activeWatcher = startWatching(watchPath, output, () => {
analyzer.context().files.getFileByPath(watchPath)?.invalidate();
void executeStatement(output, effective, analyzer, allowRSessionAccess);
});
activeWatcher.unref();
activeWatcher.on('error', (err) => {
output.stderr(`Watch error for '${watchPath}': ${err.message}`);
stopWatching(output, false);
});
}
catch (e) {
output.stderr(`Cannot watch '${watchPath}': ${e.message}`);
}
}
return;
}
await executeStatement(output, statement, analyzer, allowRSessionAccess);
}
/**
* This function interprets the given `expr` as a REPL command (see {@link repl} for more on the semantics).
* @param analyzer - The flowR analyzer to use.
* @param output - Defines two methods that every function in the repl uses to output its data.
* @param expr - The expression to process.
* @param allowRSessionAccess - If true, allows the execution of arbitrary R code.
*/
async function replProcessAnswer(analyzer, output, expr, allowRSessionAccess) {
const statements = (0, args_1.splitAtEscapeSensitive)(expr, false, /^;\s*:/);
for (const statement of statements) {
await replProcessStatement(output, statement.trim(), analyzer, allowRSessionAccess);
}
}
/**
* Provides a never-ending repl (read-evaluate-print loop) processor that can be used to interact with a {@link RShell} as well as all flowR scripts.
*
* The repl allows for two kinds of inputs:
* - Starting with a colon `:`, indicating a command (probe `:help`, and refer to {@link commands}) </li>
* - Starting with anything else, indicating default R code to be directly executed. If you kill the underlying shell, that is on you! </li>
* @param options - The options for the repl. See {@link FlowrReplOptions} for more information.
*
* For the execution, this function makes use of {@link replProcessAnswer}.
*/
async function repl({ analyzer, historyFile = defaultHistoryFile, rl = readline.createInterface(makeDefaultReplReadline(analyzer.flowrConfig, historyFile)), output = repl_main_1.standardReplOutput, allowRSessionAccess = false }) {
if (historyFile) {
rl.on('history', h => fs_1.default.writeFileSync(historyFile, h.join('\n'), { encoding: 'utf-8' }));
}
const ghostHint = (0, ghost_hint_1.installGhostHint)(rl, analyzer.flowrConfig, {
suggest: line => completionSuggestion(line, analyzer.flowrConfig)
});
let sigintCount = 0;
rl.on('SIGINT', () => {
process.stdout.write('\n');
if (activeWatcher !== undefined) {
stopWatching(output);
rl.setPrompt((0, prompt_1.prompt)());
sigintCount = 0;
}
else {
sigintCount++;
if (sigintCount >= 2) {
stopWatching(output, false);
(0, proc_1.exitSafe)(0);
}
else {
output.stdout('(Press Ctrl+C again or type :quit to exit)');
setTimeout(() => {
sigintCount = 0;
}, 2000).unref();
}
}
rl.prompt();
});
rl.on('close', () => {
stopWatching(output, false);
(0, proc_1.exitSafe)(0);
});
// the incredible repl :D, we kill it with ':quit'
// noinspection InfiniteLoopJS
while (true) {
await new Promise((resolve, reject) => {
rl.question(activeWatcher !== undefined ? '' : (0, prompt_1.prompt)(), answer => {
ghostHint.clear();
sigintCount = 0;
rl.pause();
replProcessAnswer(analyzer, output, answer, allowRSessionAccess).then(() => {
rl.resume();
resolve();
}).catch(reject);
});
if (activeWatcher === undefined) {
ghostHint.show();
}
});
}
}
/**
* Loads the REPL history from the given file.
*/
function loadReplHistory(historyFile) {
try {
if (!fs_1.default.existsSync(historyFile)) {
return undefined;
}
return fs_1.default.readFileSync(historyFile, { encoding: 'utf-8' }).split('\n');
}
catch (e) {
log_1.log.error(`Failed to load repl history from ${historyFile}: ${e?.message}`);
log_1.log.error(e?.stack);
return undefined;
}
}
//# sourceMappingURL=core.js.map