Skip to content

Commit 6cb7d13

Browse files
Vinit-PanditkgryteSnehil-Shah
authored andcommitted
feat: add logic for eager evaluation in REPL
PR-URL: stdlib-js#4277 Ref: stdlib-js#2073 Co-authored-by: Athan Reines <[email protected]> Co-authored-by: Snehil Shah <[email protected]> Reviewed-by: Athan Reines <[email protected]> Reviewed-by: Snehil Shah <[email protected]>
1 parent 50d75d6 commit 6cb7d13

File tree

7 files changed

+359
-2
lines changed

7 files changed

+359
-2
lines changed

lib/node_modules/@stdlib/repl/lib/defaults.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ function defaults() {
106106
'syntaxHighlighting': void 0,
107107

108108
// Theme for syntax highlighting:
109-
'theme': 'stdlib-ansi-basic'
109+
'theme': 'stdlib-ansi-basic',
110+
111+
// Flag indicating whether to enable eager evaluation (note: default depends on whether TTY):
112+
'eagerEvaluation': void 0
110113
}
111114
};
112115
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* @license Apache-2.0
3+
*
4+
* Copyright (c) 2025 The Stdlib Authors.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/* eslint-disable no-underscore-dangle, no-restricted-syntax, no-invalid-this, max-len */
20+
21+
'use strict';
22+
23+
// MODULES //
24+
25+
var readline = require( 'readline' );
26+
var inspect = require( 'util' ).inspect;
27+
var logger = require( 'debug' );
28+
var parse = require( 'acorn' ).parse;
29+
var replace = require( '@stdlib/string/replace' );
30+
var setNonEnumerableReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' );
31+
var copy = require( '@stdlib/array/base/copy' );
32+
var max = require( '@stdlib/math/base/special/max' );
33+
var processCommand = require( './process_command.js' );
34+
var compileCommand = require( './compile_command.js' );
35+
var ANSI_COLORS = require( './ansi_colors.js' );
36+
37+
38+
// VARIABLES //
39+
40+
var debug = logger( 'repl:eager-evaluator' );
41+
var AOPTS = {
42+
'ecmaVersion': 'latest'
43+
};
44+
var ROPTS = {
45+
'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
46+
'displayErrors': false,
47+
'breakOnSigint': true // Node.js >=6.3.0
48+
};
49+
var tempDB = {
50+
'base_sin': {
51+
'isPure': true
52+
}
53+
};
54+
var ANSI_GRAY = ANSI_COLORS[ 'brightBlack' ];
55+
var ANSI_RESET = ANSI_COLORS[ 'reset' ];
56+
57+
58+
// FUNCTIONS //
59+
60+
/**
61+
* Recursively traverses the node to determine whether the node is side-effect-free.
62+
*
63+
* @private
64+
* @param {Object} node - ast node
65+
* @returns {boolean} boolean indicating whether the node is side-effect-free
66+
*/
67+
function traverse( node ) {
68+
var fname;
69+
var i;
70+
if ( !node ) {
71+
return false;
72+
}
73+
if ( node.type === 'Literal' || node.type === 'Identifier' || node.type === 'MemberExpression' ) {
74+
return true;
75+
}
76+
if ( node.type === 'BinaryExpression' ) {
77+
if ( traverse( node.left ) && traverse( node.right ) ) {
78+
return true;
79+
}
80+
} else if ( node.type === 'ExpressionStatement' ) {
81+
if ( traverse( node.expression ) ) {
82+
return true;
83+
}
84+
} else if ( node.type === 'CallExpression' ) {
85+
fname = getFunctionName( node.callee );
86+
if ( tempDB[fname] && tempDB[fname].isPure ) {
87+
// Examine each function argument for potential side-effects...
88+
for ( i = 0; i < node.arguments.length; i++ ) {
89+
if ( !traverse( node.arguments[ i ] ) ) {
90+
return false;
91+
}
92+
}
93+
return true;
94+
}
95+
}
96+
return false;
97+
}
98+
99+
/**
100+
* Resolves the function name associated with a provided AST node.
101+
*
102+
* @private
103+
* @param {Object} node - ast node
104+
* @returns {string} function name representing the node
105+
*/
106+
function getFunctionName( node ) {
107+
if ( !node ) {
108+
return '';
109+
}
110+
if ( node.type === 'MemberExpression' ) {
111+
return getFunctionName( node.object ) + '_' + node.property.name;
112+
}
113+
if ( node.type === 'Identifier' ) {
114+
return node.name;
115+
}
116+
return '';
117+
}
118+
119+
120+
// MAIN //
121+
122+
/**
123+
* Constructor for creating an eager evaluator.
124+
*
125+
* @private
126+
* @param {REPL} repl - repl instance
127+
* @param {Object} rli - readline instance
128+
* @param {boolean} enabled - boolean indicating whether the eager evaluator should be initially enabled
129+
* @returns {EagerEvaluator} eager evaluator instance
130+
*/
131+
function EagerEvaluator( repl, rli, enabled ) {
132+
if ( !(this instanceof EagerEvaluator) ) {
133+
return new EagerEvaluator( repl, rli, enabled );
134+
}
135+
debug( 'Creating a new eager evaluator...' );
136+
137+
// Cache a reference to the provided REPL instance:
138+
this._repl = repl;
139+
140+
// Cache a reference to the readline interface:
141+
this._rli = rli;
142+
143+
// Cache a reference to the command array:
144+
this._cmd = repl._cmd;
145+
146+
// Initialize a flag indicating whether the eager evaluator is enabled:
147+
this._enabled = enabled;
148+
149+
// Initialize a flag indicating whether we are currently previewing eagerly-evaluated output:
150+
this._isPreviewing = false;
151+
152+
return this;
153+
}
154+
155+
/**
156+
* Checks whether provided code is free of side-effects.
157+
*
158+
* @private
159+
* @name _isSideEffectFree
160+
* @memberof EagerEvaluator.prototype
161+
* @type {Function}
162+
* @param {string} code - input code
163+
* @returns {boolean} boolean indicating whether provided code is free of side-effects
164+
*/
165+
setNonEnumerableReadOnly( EagerEvaluator.prototype, '_isSideEffectFree', function isSideEffectFree( code ) {
166+
var ast;
167+
var i;
168+
169+
try {
170+
ast = parse( code, AOPTS );
171+
} catch ( err ) {
172+
debug( 'Encountered an error when generating AST: %s', err.message );
173+
return false;
174+
}
175+
for ( i = 0; i < ast.body.length; i++ ) {
176+
if ( !traverse( ast.body[ i ] ) ) {
177+
return false;
178+
}
179+
}
180+
return true;
181+
});
182+
183+
/**
184+
* Clears eagerly-evaluated output.
185+
*
186+
* @private
187+
* @name clear
188+
* @memberof EagerEvaluator.prototype
189+
* @type {Function}
190+
* @returns {void}
191+
*/
192+
setNonEnumerableReadOnly( EagerEvaluator.prototype, 'clear', function clear() {
193+
var cursorPosition;
194+
195+
cursorPosition = this._rli.cursor;
196+
readline.moveCursor( this._repl._ostream, 0, 1 );
197+
readline.clearLine( this._repl._ostream, 0 );
198+
readline.moveCursor( this._repl._ostream, 0, -1 );
199+
readline.cursorTo( this._repl._ostream, cursorPosition + this._repl.promptLength() );
200+
this._isPreviewing = false;
201+
});
202+
203+
/**
204+
* Disables the eager evaluator.
205+
*
206+
* @private
207+
* @name disable
208+
* @memberof EagerEvaluator.prototype
209+
* @type {Function}
210+
* @returns {EagerEvaluator} eager evaluator instance
211+
*/
212+
setNonEnumerableReadOnly( EagerEvaluator.prototype, 'disable', function disable() {
213+
this._enabled = false;
214+
return this;
215+
});
216+
217+
/**
218+
* Enables the eager evaluator.
219+
*
220+
* @private
221+
* @name enable
222+
* @memberof EagerEvaluator.prototype
223+
* @type {Function}
224+
* @returns {EagerEvaluator} eager evaluator instance
225+
*/
226+
setNonEnumerableReadOnly( EagerEvaluator.prototype, 'enable', function enable() {
227+
this._enabled = true;
228+
return this;
229+
});
230+
231+
/**
232+
* Callback which should be invoked **before** a "keypress" event.
233+
*
234+
* @private
235+
* @name beforeKeypress
236+
* @memberof EagerEvaluator.prototype
237+
* @type {Function}
238+
* @param {string} data - input data
239+
* @param {(Object|void)} key - key object
240+
* @returns {void}
241+
*/
242+
setNonEnumerableReadOnly( EagerEvaluator.prototype, 'beforeKeypress', function beforeKeypress() {
243+
if ( this._isPreviewing ) {
244+
this.clear();
245+
}
246+
});
247+
248+
/**
249+
* Callback for handling a "keypress" event.
250+
*
251+
* @private
252+
* @name onKeypress
253+
* @memberof EagerEvaluator.prototype
254+
* @type {Function}
255+
* @param {string} data - input data
256+
* @param {(Object|void)} key - key object
257+
* @returns {void}
258+
*/
259+
setNonEnumerableReadOnly( EagerEvaluator.prototype, 'onKeypress', function onKeypress() {
260+
var cursorPosition;
261+
var executable;
262+
var index;
263+
var code;
264+
var cmd;
265+
var pre;
266+
var res;
267+
var tmp;
268+
269+
if ( !this._enabled || this._rli.line === '' ) {
270+
return;
271+
}
272+
273+
// Build the final command:
274+
cmd = copy( this._cmd );
275+
cmd[ max( cmd.length - 1, 0 ) ] = this._rli.line; // eager-evaluation should only work when on the last line, hence updating the last index
276+
277+
code = cmd.join( '\n' );
278+
debug( 'Eagerly evaluating: %s', code );
279+
if ( !this._isSideEffectFree( code ) ) {
280+
debug( 'Unable to perform eager-evaluation due to potential side-effects. Skipping...' );
281+
return;
282+
}
283+
debug( 'Processing command...' );
284+
tmp = processCommand( code );
285+
if ( tmp instanceof Error ) {
286+
debug( 'Error encountered when processing command: %s', tmp.message );
287+
return;
288+
}
289+
debug( 'Compiling command...' );
290+
executable = compileCommand( tmp );
291+
if ( executable instanceof Error ) {
292+
debug( 'Error encountered when compiling command: %s', executable.message );
293+
return;
294+
}
295+
try {
296+
if ( this._repl._sandbox ) {
297+
res = executable.compiled.runInContext( this._repl._context, ROPTS );
298+
} else {
299+
res = executable.compiled.runInThisContext( ROPTS );
300+
}
301+
} catch ( err ) {
302+
debug( 'Encountered an error when executing the command: %s', err.message );
303+
return;
304+
}
305+
306+
res = inspect( res );
307+
index = res.indexOf( '\n' );
308+
if ( index !== -1 ) {
309+
res = res.slice( 0, index ) + '...';
310+
}
311+
cursorPosition = this._rli.cursor;
312+
pre = replace( this._repl._outputPrompt, '%d', ( this._repl._count+1 ).toString() );
313+
this._repl._ostream.write( '\n' + ANSI_GRAY + pre + res + ANSI_RESET );
314+
readline.moveCursor( this._repl._ostream, 0, -1 );
315+
readline.cursorTo( this._repl._ostream, cursorPosition + this._repl.promptLength() );
316+
this._isPreviewing = true;
317+
debug( 'Successfully evaluated command.' );
318+
});
319+
320+
321+
// EXPORTS //
322+
323+
module.exports = EagerEvaluator;

0 commit comments

Comments
 (0)