Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add logic for eager evaluation in REPL #4277

Merged
merged 39 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8d80cee
feat: add draft logic of eager evaluation
Vinit-Pandit Dec 27, 2024
4a9205b
fix: fixing test failure
Vinit-Pandit Dec 27, 2024
4c15092
fix: fixing test failure
Vinit-Pandit Dec 27, 2024
e626b19
checking options.setting
Vinit-Pandit Dec 27, 2024
e43cab1
fix: style fix
Vinit-Pandit Dec 27, 2024
4f6efef
Merge branch 'stdlib-js:develop' into eager_evaluation_repl
Vinit-Pandit Jan 14, 2025
b43056d
chore: update copyright years
stdlib-bot Jan 14, 2025
6c8d7fd
style: fix indentation in manifest.json files
aayush0325 Jan 6, 2025
30f7796
fix: fixing clashing in end line of terminal
Vinit-Pandit Jan 24, 2025
72651d0
fix: removing unnessary conditions check
Vinit-Pandit Jan 24, 2025
7fce9d3
fix: adding suggested changes
Vinit-Pandit Jan 25, 2025
7f23f98
style: adding spaces in parenthesis
Vinit-Pandit Jan 25, 2025
9aa4c3e
refactor: change file name for consistency
Snehil-Shah Jan 29, 2025
7da041f
fix: add guard against invalid class initialization
Snehil-Shah Jan 29, 2025
e540283
refactor: use existing constant for hardcoded values
Snehil-Shah Jan 29, 2025
bebc0fe
refactor: move entangled logic outside and clean ups
Snehil-Shah Jan 29, 2025
cf2eaa7
fix: add stricter timeout and clean ups
Snehil-Shah Jan 30, 2025
81becd2
fix: update incorrect logic for handling multiline inputs
Snehil-Shah Jan 30, 2025
28c0f8d
style: update debug logs
Snehil-Shah Jan 30, 2025
998db52
feat: add logic to setting to toggle eagerevaluator
Vinit-Pandit Jan 30, 2025
36619b0
fix: make timeout even stricter
Snehil-Shah Jan 30, 2025
0f19131
style: clean ups
Snehil-Shah Jan 30, 2025
412fba3
style: clean ups
Vinit-Pandit Feb 1, 2025
b884fa6
style: clean up
Vinit-Pandit Feb 1, 2025
2e03eb3
style: clean up
Vinit-Pandit Feb 1, 2025
a40a99a
style: correctly place functions
Snehil-Shah Feb 1, 2025
f2f4c9b
docs: fix return type
kgryte Feb 2, 2025
08c68e5
docs: update description
kgryte Feb 2, 2025
8584782
docs: remove hyphen
kgryte Feb 2, 2025
8889314
docs: update description
kgryte Feb 2, 2025
69b4fe1
Apply suggestions from code review
kgryte Feb 2, 2025
d81e24f
docs: fix type
kgryte Feb 2, 2025
7260373
Apply suggestions from code review
kgryte Feb 2, 2025
35382da
docs: remove comment
kgryte Feb 2, 2025
55392c8
Apply suggestions from code review
kgryte Feb 2, 2025
ba01164
style: fix missing space
kgryte Feb 2, 2025
e94acec
refactor: remove unnecessary branch
kgryte Feb 2, 2025
c63d4f5
refactor: update debug message
kgryte Feb 2, 2025
6393eaf
refactor: update debug messages
kgryte Feb 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/node_modules/@stdlib/repl/lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};
}
Expand Down
326 changes: 326 additions & 0 deletions lib/node_modules/@stdlib/repl/lib/eager_evaluator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
/**
* @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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for @Snehil-Shah: in a future refactoring, we should make the command array a read-only class instance for consumers (e.g., internal functions) which only need read-only access. Should there be a module which explicitly needs to manipulate the command array, that could be through a separate parent class which allows mutation. In general, passing around and caching an array like this makes me nervous, as it makes debugging more difficult due to not knowing who or what is potentially mutating the command array.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Although, the contents of the cmd array is pretty short lived, and is supposed to be frequently mutated such as when a new line is added or previous lines are edited in a multiline command, and is cleaned up after every execution as well.


// 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
kgryte marked this conversation as resolved.
Show resolved Hide resolved
* @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
kgryte marked this conversation as resolved.
Show resolved Hide resolved
* @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 ) {
kgryte marked this conversation as resolved.
Show resolved Hide resolved
return;
}
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( 'Code is not side-effect free, exiting eager-evaluation...' );
kgryte marked this conversation as resolved.
Show resolved Hide resolved
return;
}
debug( 'Trying to process command...' );
tmp = processCommand( code );
if ( tmp instanceof Error ) {
debug( 'Error encountered when processing command: %s', tmp.message );
return;
}
debug( 'Trying to compile command...' );
kgryte marked this conversation as resolved.
Show resolved Hide resolved
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;
Loading