-
-
Notifications
You must be signed in to change notification settings - Fork 602
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
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 4a9205b
fix: fixing test failure
Vinit-Pandit 4c15092
fix: fixing test failure
Vinit-Pandit e626b19
checking options.setting
Vinit-Pandit e43cab1
fix: style fix
Vinit-Pandit 4f6efef
Merge branch 'stdlib-js:develop' into eager_evaluation_repl
Vinit-Pandit b43056d
chore: update copyright years
stdlib-bot 6c8d7fd
style: fix indentation in manifest.json files
aayush0325 30f7796
fix: fixing clashing in end line of terminal
Vinit-Pandit 72651d0
fix: removing unnessary conditions check
Vinit-Pandit 7fce9d3
fix: adding suggested changes
Vinit-Pandit 7f23f98
style: adding spaces in parenthesis
Vinit-Pandit 9aa4c3e
refactor: change file name for consistency
Snehil-Shah 7da041f
fix: add guard against invalid class initialization
Snehil-Shah e540283
refactor: use existing constant for hardcoded values
Snehil-Shah bebc0fe
refactor: move entangled logic outside and clean ups
Snehil-Shah cf2eaa7
fix: add stricter timeout and clean ups
Snehil-Shah 81becd2
fix: update incorrect logic for handling multiline inputs
Snehil-Shah 28c0f8d
style: update debug logs
Snehil-Shah 998db52
feat: add logic to setting to toggle eagerevaluator
Vinit-Pandit 36619b0
fix: make timeout even stricter
Snehil-Shah 0f19131
style: clean ups
Snehil-Shah 412fba3
style: clean ups
Vinit-Pandit b884fa6
style: clean up
Vinit-Pandit 2e03eb3
style: clean up
Vinit-Pandit a40a99a
style: correctly place functions
Snehil-Shah f2f4c9b
docs: fix return type
kgryte 08c68e5
docs: update description
kgryte 8584782
docs: remove hyphen
kgryte 8889314
docs: update description
kgryte 69b4fe1
Apply suggestions from code review
kgryte d81e24f
docs: fix type
kgryte 7260373
Apply suggestions from code review
kgryte 35382da
docs: remove comment
kgryte 55392c8
Apply suggestions from code review
kgryte ba01164
style: fix missing space
kgryte e94acec
refactor: remove unnecessary branch
kgryte c63d4f5
refactor: update debug message
kgryte 6393eaf
refactor: update debug messages
kgryte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
// 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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.