diff --git a/lib/node_modules/@stdlib/repl/lib/defaults.js b/lib/node_modules/@stdlib/repl/lib/defaults.js index 0c0789ec801c..0f3a2cc67919 100644 --- a/lib/node_modules/@stdlib/repl/lib/defaults.js +++ b/lib/node_modules/@stdlib/repl/lib/defaults.js @@ -106,7 +106,10 @@ function defaults() { 'syntaxHighlighting': void 0, // Theme for syntax highlighting: - 'theme': 'stdlib-ansi-basic' + 'theme': 'stdlib-ansi-basic', + + // Flag indicating whether to enable eager evaluation (note: default depends on whether TTY): + 'eagerEvaluation': void 0 } }; } diff --git a/lib/node_modules/@stdlib/repl/lib/eager_evaluator.js b/lib/node_modules/@stdlib/repl/lib/eager_evaluator.js new file mode 100644 index 000000000000..c743dd6b786b --- /dev/null +++ b/lib/node_modules/@stdlib/repl/lib/eager_evaluator.js @@ -0,0 +1,323 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2025 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/* eslint-disable no-underscore-dangle, no-restricted-syntax, no-invalid-this, max-len */ + +'use strict'; + +// MODULES // + +var readline = require( 'readline' ); +var inspect = require( 'util' ).inspect; +var logger = require( 'debug' ); +var parse = require( 'acorn' ).parse; +var replace = require( '@stdlib/string/replace' ); +var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); +var copy = require( '@stdlib/array/base/copy' ); +var max = require( '@stdlib/math/base/special/max' ); +var processCommand = require( './process_command.js' ); +var compileCommand = require( './compile_command.js' ); +var ANSI_COLORS = require( './ansi_colors.js' ); + + +// VARIABLES // + +var debug = logger( 'repl:eager-evaluator' ); +var AOPTS = { + 'ecmaVersion': 'latest' +}; +var ROPTS = { + 'timeout': 100, // (in milliseconds) this controls how long eagerly evaluated commands have to execute; we need to avoid setting this too high in order to avoid eager evaluation interfering with the UX when naturally typing + 'displayErrors': false, + 'breakOnSigint': true // Node.js >=6.3.0 +}; +var tempDB = { + 'base_sin': { + 'isPure': true + } +}; +var ANSI_GRAY = ANSI_COLORS[ 'brightBlack' ]; +var ANSI_RESET = ANSI_COLORS[ 'reset' ]; + + +// FUNCTIONS // + +/** +* Recursively traverses the node to determine whether the node is side-effect-free. +* +* @private +* @param {Object} node - ast node +* @returns {boolean} boolean indicating whether the node is side-effect-free +*/ +function traverse( node ) { + var fname; + var i; + if ( !node ) { + return false; + } + if ( node.type === 'Literal' || node.type === 'Identifier' || node.type === 'MemberExpression' ) { + return true; + } + if ( node.type === 'BinaryExpression' ) { + if ( traverse( node.left ) && traverse( node.right ) ) { + return true; + } + } else if ( node.type === 'ExpressionStatement' ) { + if ( traverse( node.expression ) ) { + return true; + } + } else if ( node.type === 'CallExpression' ) { + fname = getFunctionName( node.callee ); + if ( tempDB[fname] && tempDB[fname].isPure ) { + // Examine each function argument for potential side-effects... + for ( i = 0; i < node.arguments.length; i++ ) { + if ( !traverse( node.arguments[ i ] ) ) { + return false; + } + } + return true; + } + } + return false; +} + +/** +* Resolves the function name associated with a provided AST node. +* +* @private +* @param {Object} node - ast node +* @returns {string} function name representing the node +*/ +function getFunctionName( node ) { + if ( !node ) { + return ''; + } + if ( node.type === 'MemberExpression' ) { + return getFunctionName( node.object ) + '_' + node.property.name; + } + if ( node.type === 'Identifier' ) { + return node.name; + } + return ''; +} + + +// MAIN // + +/** +* Constructor for creating an eager evaluator. +* +* @private +* @param {REPL} repl - repl instance +* @param {Object} rli - readline instance +* @param {boolean} enabled - boolean indicating whether the eager evaluator should be initially enabled +* @returns {EagerEvaluator} eager evaluator instance +*/ +function EagerEvaluator( repl, rli, enabled ) { + if ( !(this instanceof EagerEvaluator) ) { + return new EagerEvaluator( repl, rli, enabled ); + } + debug( 'Creating a new eager evaluator...' ); + + // Cache a reference to the provided REPL instance: + this._repl = repl; + + // Cache a reference to the readline interface: + this._rli = rli; + + // Cache a reference to the command array: + this._cmd = repl._cmd; + + // Initialize a flag indicating whether the eager evaluator is enabled: + this._enabled = enabled; + + // Initialize a flag indicating whether we are currently previewing eagerly-evaluated output: + this._isPreviewing = false; + + return this; +} + +/** +* Checks whether provided code is free of side-effects. +* +* @private +* @name _isSideEffectFree +* @memberof EagerEvaluator.prototype +* @type {Function} +* @param {string} code - input code +* @returns {boolean} boolean indicating whether provided code is free of side-effects +*/ +setNonEnumerableReadOnly( EagerEvaluator.prototype, '_isSideEffectFree', function isSideEffectFree( code ) { + var ast; + var i; + + try { + ast = parse( code, AOPTS ); + } catch ( err ) { + debug( 'Encountered an error when generating AST: %s', err.message ); + return false; + } + for ( i = 0; i < ast.body.length; i++ ) { + if ( !traverse( ast.body[ i ] ) ) { + return false; + } + } + return true; +}); + +/** +* Clears eagerly-evaluated output. +* +* @private +* @name clear +* @memberof EagerEvaluator.prototype +* @type {Function} +* @returns {void} +*/ +setNonEnumerableReadOnly( EagerEvaluator.prototype, 'clear', function clear() { + var cursorPosition; + + cursorPosition = this._rli.cursor; + readline.moveCursor( this._repl._ostream, 0, 1 ); + readline.clearLine( this._repl._ostream, 0 ); + readline.moveCursor( this._repl._ostream, 0, -1 ); + readline.cursorTo( this._repl._ostream, cursorPosition + this._repl.promptLength() ); + this._isPreviewing = false; +}); + +/** +* Disables the eager evaluator. +* +* @private +* @name disable +* @memberof EagerEvaluator.prototype +* @type {Function} +* @returns {EagerEvaluator} eager evaluator instance +*/ +setNonEnumerableReadOnly( EagerEvaluator.prototype, 'disable', function disable() { + this._enabled = false; + return this; +}); + +/** +* Enables the eager evaluator. +* +* @private +* @name enable +* @memberof EagerEvaluator.prototype +* @type {Function} +* @returns {EagerEvaluator} eager evaluator instance +*/ +setNonEnumerableReadOnly( EagerEvaluator.prototype, 'enable', function enable() { + this._enabled = true; + return this; +}); + +/** +* Callback which should be invoked **before** a "keypress" event. +* +* @private +* @name beforeKeypress +* @memberof EagerEvaluator.prototype +* @type {Function} +* @param {string} data - input data +* @param {(Object|void)} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( EagerEvaluator.prototype, 'beforeKeypress', function beforeKeypress() { + if ( this._isPreviewing ) { + this.clear(); + } +}); + +/** +* Callback for handling a "keypress" event. +* +* @private +* @name onKeypress +* @memberof EagerEvaluator.prototype +* @type {Function} +* @param {string} data - input data +* @param {(Object|void)} key - key object +* @returns {void} +*/ +setNonEnumerableReadOnly( EagerEvaluator.prototype, 'onKeypress', function onKeypress() { + var cursorPosition; + var executable; + var index; + var code; + var cmd; + var pre; + var res; + var tmp; + + if ( !this._enabled || this._rli.line === '' ) { + return; + } + + // Build the final command: + cmd = copy( this._cmd ); + cmd[ max( cmd.length - 1, 0 ) ] = this._rli.line; // eager-evaluation should only work when on the last line, hence updating the last index + + code = cmd.join( '\n' ); + debug( 'Eagerly evaluating: %s', code ); + if ( !this._isSideEffectFree( code ) ) { + debug( 'Unable to perform eager-evaluation due to potential side-effects. Skipping...' ); + return; + } + debug( 'Processing command...' ); + tmp = processCommand( code ); + if ( tmp instanceof Error ) { + debug( 'Error encountered when processing command: %s', tmp.message ); + return; + } + debug( 'Compiling command...' ); + executable = compileCommand( tmp ); + if ( executable instanceof Error ) { + debug( 'Error encountered when compiling command: %s', executable.message ); + return; + } + try { + if ( this._repl._sandbox ) { + res = executable.compiled.runInContext( this._repl._context, ROPTS ); + } else { + res = executable.compiled.runInThisContext( ROPTS ); + } + } catch ( err ) { + debug( 'Encountered an error when executing the command: %s', err.message ); + return; + } + + res = inspect( res ); + index = res.indexOf( '\n' ); + if ( index !== -1 ) { + res = res.slice( 0, index ) + '...'; + } + cursorPosition = this._rli.cursor; + pre = replace( this._repl._outputPrompt, '%d', ( this._repl._count+1 ).toString() ); + this._repl._ostream.write( '\n' + ANSI_GRAY + pre + res + ANSI_RESET ); + readline.moveCursor( this._repl._ostream, 0, -1 ); + readline.cursorTo( this._repl._ostream, cursorPosition + this._repl.promptLength() ); + this._isPreviewing = true; + debug( 'Successfully evaluated command.' ); +}); + + +// EXPORTS // + +module.exports = EagerEvaluator; diff --git a/lib/node_modules/@stdlib/repl/lib/main.js b/lib/node_modules/@stdlib/repl/lib/main.js index b04bcf12da61..8298dd1a723d 100644 --- a/lib/node_modules/@stdlib/repl/lib/main.js +++ b/lib/node_modules/@stdlib/repl/lib/main.js @@ -16,7 +16,7 @@ * limitations under the License. */ -/* eslint-disable no-restricted-syntax, no-invalid-this, no-underscore-dangle, max-lines, max-lines-per-function */ +/* eslint-disable no-restricted-syntax, no-invalid-this, no-underscore-dangle, max-lines, max-lines-per-function, max-statements */ 'use strict'; @@ -68,6 +68,7 @@ var CompleterEngine = require( './completer_engine.js' ); var PreviewCompleter = require( './completer_preview.js' ); var AutoCloser = require( './auto_close_pairs.js' ); var SyntaxHighlighter = require( './syntax_highlighter.js' ); +var EagerEvaluator = require( './eager_evaluator.js' ); var ALIAS_OVERRIDES = require( './alias_overrides.js' ); var SETTINGS = require( './settings.js' ); var SETTINGS_VALIDATORS = require( './settings_validators.js' ); @@ -108,6 +109,7 @@ var debug = logger( 'repl' ); * @param {boolean} [options.settings.completionPreviews] - boolean indicating whether to enable completion previews for auto-completion * @param {boolean} [options.settings.autoDisableBracketedPasteOnExit] - boolean indicating whether to automatically disable bracketed-paste upon exiting the REPL * @param {boolean} [options.settings.syntaxHighlighting] - boolean indicating whether to enable syntax highlighting +* @param {boolean} [options.settings.eagerEvaluation] - boolean indicating whether to enable eager evaluation * @param {string} [options.settings.theme] - initial color theme for syntax highlighting * @throws {Error} must provide valid options * @returns {REPL} REPL instance @@ -159,6 +161,7 @@ function REPL( options ) { opts.settings.completionPreviews = ( opts.settings.completionPreviews === void 0 ) ? opts.isTTY : opts.settings.completionPreviews; // eslint-disable-line max-len opts.settings.autoDisableBracketedPasteOnExit = ( opts.settings.autoDisableBracketedPasteOnExit === void 0 ) ? opts.isTTY : opts.settings.autoDisableBracketedPasteOnExit; // eslint-disable-line max-len opts.settings.syntaxHighlighting = ( opts.settings.syntaxHighlighting === void 0 ) ? opts.isTTY : opts.settings.syntaxHighlighting; // eslint-disable-line max-len + opts.settings.eagerEvaluation = ( opts.settings.eagerEvaluation === void 0 ) ? opts.isTTY : opts.settings.eagerEvaluation; // eslint-disable-line max-len debug( 'Options: %s', JSON.stringify({ 'input': '', @@ -293,6 +296,9 @@ function REPL( options ) { // Initialize a syntax-highlighter: setNonEnumerableReadOnly( this, '_syntaxHighlighter', new SyntaxHighlighter( this, this._ostream, this._settings.syntaxHighlighting ) ); + // Initialize an eager evaluator: + setNonEnumerableReadOnly( this, '_eagerEvaluator', new EagerEvaluator( this, this._rli, this._settings.eagerEvaluation ) ); + // Cache a reference to the private readline interface `ttyWrite` to allow calling the method when wanting default behavior: setNonEnumerableReadOnly( this, '_ttyWrite', this._rli._ttyWrite ); @@ -362,6 +368,7 @@ function REPL( options ) { self._autoCloser.beforeKeypress( data, key ); completed = self._previewCompleter.beforeKeypress( data, key ); FLG = self._editorActions.beforeKeypress( data, key ); + self._eagerEvaluator.beforeKeypress( data, key ); // If ENTER keypress is encountered or if a preview was completed while navigating, gracefully close the completer... if ( completed || ( key && key.name === 'return' ) ) { @@ -399,6 +406,9 @@ function REPL( options ) { } self._completerEngine.onKeypress( data, key ); self._multilineHandler.onKeypress( data, key ); + if ( !self._completerEngine.isNavigating() ) { + self._eagerEvaluator.onKeypress( data, key ); + } self._syntaxHighlighter.onKeypress(); self._previewCompleter.onKeypress( data, key ); } @@ -1508,6 +1518,12 @@ setNonEnumerableReadOnly( REPL.prototype, 'settings', function settings() { } else { this._multilineHandler.disableBracketedPaste(); } + } else if ( name === 'eagerEvaluation' ) { + if ( value ) { + this._eagerEvaluator.enable(); + } else { + this._eagerEvaluator.disable(); + } } return this; diff --git a/lib/node_modules/@stdlib/repl/lib/settings.js b/lib/node_modules/@stdlib/repl/lib/settings.js index 53a38eaffc00..429e21b3d1a5 100644 --- a/lib/node_modules/@stdlib/repl/lib/settings.js +++ b/lib/node_modules/@stdlib/repl/lib/settings.js @@ -56,6 +56,10 @@ var SETTINGS = { 'desc': 'Enable syntax highlighting.', 'type': 'boolean' }, + 'eagerEvaluation': { + 'desc': 'Enable eager evaluation.', + 'type': 'boolean' + }, 'theme': { 'desc': 'Set the syntax highlighting theme.', 'type': 'string' diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.auto_close_pairs.js b/lib/node_modules/@stdlib/repl/test/integration/test.auto_close_pairs.js index 6a69fdcc52e2..67e6198e2c58 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.auto_close_pairs.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.auto_close_pairs.js @@ -145,7 +145,9 @@ function defaultSettings() { 'autoDeletePairs': false, 'autoPage': false, 'completionPreviews': false, - 'syntaxHighlighting': false + 'syntaxHighlighting': false, + 'eagerEvaluation': false + }; } diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js index c17512bc9985..ffcc95ba8562 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completer_engine.js @@ -48,6 +48,7 @@ function defaultSettings() { 'autoDisableBracketedPasteOnExit': false, 'completionPreviews': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoPage': false }; } diff --git a/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js b/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js index ef9840c6d725..6f572938b9a7 100644 --- a/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js +++ b/lib/node_modules/@stdlib/repl/test/integration/test.completion_previews.js @@ -47,6 +47,7 @@ tape( 'a REPL instance supports displaying a completion preview of user-defined 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -92,6 +93,7 @@ tape( 'a REPL instance supports displaying a completion preview for common prefi 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -138,6 +140,7 @@ tape( 'a REPL instance supports displaying a completion preview for recognized i 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -183,6 +186,7 @@ tape( 'a REPL instance supports displaying a completion preview when a cursor is 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -237,6 +241,7 @@ tape( 'a REPL instance supports auto-completing a completion candidate by moving 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -290,6 +295,7 @@ tape( 'a REPL instance supports auto-completing a completion preview and executi 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -344,6 +350,7 @@ tape( 'a REPL instance does not display a completion preview when no completion 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } }; @@ -403,6 +410,7 @@ tape( 'a REPL instance does not display a completion preview once a user enters 'settings': { 'autoPage': false, 'syntaxHighlighting': false, + 'eagerEvaluation': false, 'autoDisableBracketedPasteOnExit': false } };