function REPL( options ) {
	var ostream;
	var themes;
	var opts;
	var self;
	var err;
	var i;

	if ( !( this instanceof REPL ) ) {
		if ( arguments.length ) {
			return new REPL( options );
		}
		return new REPL();
	}
	self = this;

	opts = defaults();
	if ( arguments.length ) {
		err = validate( opts, options );
		if ( err ) {
			throw err;
		}
	}
	opts.isTTY = ( opts.isTTY === void 0 ) ? opts.output.isTTY : opts.isTTY;
	opts.settings.autoPage = ( opts.settings.autoPage === void 0 ) ? opts.isTTY : opts.settings.autoPage; // eslint-disable-line max-len
	opts.settings.bracketedPaste = ( opts.settings.bracketedPaste === void 0 && opts.isTTY ) ? true : opts.settings.bracketedPaste; // eslint-disable-line max-len
	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': '<readable_stream>',
		'output': '<writable_stream>',
		'sandbox': opts.sandbox,
		'timeout': opts.timeout,
		'isTTY': opts.isTTY,
		'inputPrompt': opts.inputPrompt,
		'outputPrompt': opts.outputPrompt,
		'padding': opts.padding,
		'welcome': opts.welcome,
		'load': opts.load,
		'save': opts.save,
		'log': opts.log,
		'quiet': opts.quiet,
		'settings': opts.settings
	}));

	// Call the parent constructor:
	EventEmitter.call( this );

	// Create an internal output stream:
	ostream = new OutputStream( this, opts.settings.autoPage );

	// Setup the output stream pipeline:
	ostream.pipe( opts.output );

	// Cache references to the input and output streams:
	setNonEnumerableReadOnly( this, '_istream', opts.input );
	setNonEnumerableReadOnly( this, '_ostream', ostream );
	setNonEnumerableReadOnly( this, '_wstream', opts.output );

	// Cache options:
	setNonEnumerableReadOnly( this, '_inputPrompt', opts.inputPrompt );
	setNonEnumerableReadOnly( this, '_outputPrompt', opts.outputPrompt );
	setNonEnumerableReadOnly( this, '_padding', opts.padding );
	setNonEnumerableReadOnly( this, '_timeout', opts.timeout );
	setNonEnumerableReadOnly( this, '_isTTY', opts.isTTY );
	setNonEnumerableReadOnly( this, '_sandbox', opts.sandbox );
	setNonEnumerableReadOnly( this, '_settings', opts.settings );
	setNonEnumerable( this, '_quiet', opts.quiet ); // allow this to be internally toggled

	// Initialize an internal data store:
	setNonEnumerableReadOnly( this, '_internal', {} );
	setNonEnumerableReadOnly( this._internal, 'presentation', {} );
	setNonEnumerableReadOnly( this._internal.presentation, 'cache', {} );
	setNonEnumerable( this._internal.presentation, 'counter', 0 );

	// Initialize an internal command queue:
	setNonEnumerableReadOnly( this, '_queue', fifo() );

	// Initialize a strided internal buffer for storing the command history:
	setNonEnumerableReadOnly( this, '_history', [] );

	// Initialize an internal buffer for storing the current command:
	setNonEnumerableReadOnly( this, '_cmd', [] );

	// Initialize a executed command counter:
	setNonEnumerable( this, '_count', -1 );

	// Initialize an internal buffer for saving regular expression matches between command evaluations:
	setNonEnumerableReadOnly( this, '_regexp', [ '', '', '', '', '', '', '', '', '', '' ] );

	// Initialize a strided internal cache of context globals:
	setNonEnumerableReadOnly( this, '_contextVars', [] );

	// For non-sandboxed REPLs, initialize an internal buffer for storing a (unique) sorted list of "built-in" REPL global variables/properties which need to be deleted upon closing a REPL in order to allow garbage collection and prevent memory leaks:
	setNonEnumerableReadOnly( this, '_globalVars', ( this._sandbox ) ? null : [] );

	// Initialize a strided internal cache of resolved global variables/properties in order to allow, e.g., help text look-up based on object reference, not just alias name (note: members alternate between `'<string_alias>'` and `<resolved_value>`):
	setNonEnumerableReadOnly( this, '_aliases', [] );

	// Initialize a strided internal buffer for storing user-defined documentation:
	setNonEnumerableReadOnly( this, '_userdocs', [] );

	// Initialize an internal buffer for storing a (unique) sorted list of workspace global variables/properties which were *initially* introduced during a REPL session (note: this overlaps with `_globalVars`, but more accurately represents a snapshot of the `global` state *before* a user begins entering commands, which is useful when wanting determine what variables/identifiers a user has introduced during a REPL session):
	setNonEnumerableReadOnly( this, '_workspace', [] );

	// Initialize a strided internal buffer for caching "base" workspace variables:
	setNonEnumerableReadOnly( this, '_workspaces', {} );
	setReadOnly( this._workspaces, 'base', [] );

	// Define the current workspace:
	setNonEnumerable( this, '_currentWorkspace', 'base' );

	// Initialize an internal flag indicating whether the REPL has been closed:
	setNonEnumerable( this, '_closed', false );

	// Initialize an internal flag indicating whether the REPL is currently busy with asynchronous processing:
	setNonEnumerable( this, '_busy', false );

	// Initialize an internal flag indicating whether we've received a `SIGINT` signal:
	setNonEnumerable( this, '_SIGINT', false );

	// Initialize an internal variable for caching the result of the last successfully evaluated command:
	setNonEnumerable( this, '_ans', void 0 );

	// Initialize an internal variable for setting a command callback:
	setNonEnumerable( this, '_done', void 0 );

	// Initialize internal variables for setting a keybinding:
	setNonEnumerable( this, '_isCapturingKeybinding', false );
	setNonEnumerable( this, '_targetAction', '' );

	// Create a REPL execution context:
	setNonEnumerable( this, '_context', this.createContext() );

	// Create a new TAB completer:
	setNonEnumerableReadOnly( this, '_completer', completerFactory( this ) );

	// Create an internal readline interface:
	debug( 'Creating readline interface...' );
	setNonEnumerableReadOnly( this, '_rli', readline.createInterface({
		'input': this._istream,
		'output': this._ostream,
		'terminal': opts.isTTY,
		'prompt': opts.inputPrompt
	}));

	// Initialize a multi-line handler:
	setNonEnumerableReadOnly( this, '_multilineHandler', new MultilineHandler( this, this._rli._ttyWrite ) );

	// Initialize keybindings:
	setNonEnumerableReadOnly( this, '_keybindings', new Keybindings( opts.keybindings ) );

	// Initialize an editor actions instance:
	setNonEnumerableReadOnly( this, '_editorActions', new EditorActions( this, this._rli._ttyWrite ) );

	// Create a new TAB completer engine:
	setNonEnumerableReadOnly( this, '_completerEngine', new CompleterEngine( this, this._completer, this._wstream, this._rli._ttyWrite ) );

	// Create a new auto-closer:
	setNonEnumerableReadOnly( this, '_autoCloser', new AutoCloser( this._rli, this._settings.autoClosePairs, this._settings.autoDeletePairs, this._multilineHandler ) );

	// Initialize a preview completer:
	setNonEnumerableReadOnly( this, '_previewCompleter', new PreviewCompleter( this._rli, this._completer, this._ostream, this._settings.completionPreviews ) );

	// 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 );

	// Overwrite the private `ttyWrite` method to allow processing input before a "keypress" event is triggered:
	this._rli._ttyWrite = beforeKeypress; // WARNING: overwriting a private property

	// Add event listeners:
	this._rli.on( 'close', onClose );
	this._rli.on( 'line', onLine );
	this._rli.on( 'SIGINT', onSIGINT );
	proc.on( 'SIGWINCH', onSIGWINCH ); // terminal resize

	// Add listener for "command" events:
	this.on( 'command', onCommand );

	// Instruct the input stream to begin emitting "keypress" events:
	readline.emitKeypressEvents( this._istream, this._rli );

	// Add a listener for "keypress" events:
	this._istream.on( 'keypress', onKeypress );

	// Write a welcome message:
	this._wstream.write( opts.welcome );

	// TODO: check whether to synchronously initialize a REPL history file

	// TODO: check whether to synchronously initialize a REPL log file

	// Add any provided user-defined themes...
	if ( opts.themes ) {
		themes = objectKeys( opts.themes );
		for ( i = 0; i < themes.length; i++ ) {
			this.addTheme( themes[ i ], opts.themes[ themes[ i ] ] );
		}
	}
	// Set the syntax highlighting theme...
	this.settings( 'theme', opts.settings.theme );

	// Initialize bracketed-paste:
	if ( opts.settings.bracketedPaste !== void 0 ) {
		this.settings( 'bracketedPaste', opts.settings.bracketedPaste );
	}
	// Check whether to load and execute a JavaScript file (e.g., prior REPL history) upon startup...
	if ( opts.load ) {
		this.load( opts.load );
	} else {
		displayPrompt( this, false );
		this._count += 1;
	}
	return this;

	/**
	* Callback invoked prior to emitting a "keypress" event.
	*
	* @private
	* @param {string} data - input data
	* @param {(Object|void)} key - key object
	*/
	function beforeKeypress( data, key ) {
		var completed;
		var FLG; // flag denoting whether to bypass the default keypress behavior

		if ( self._ostream.isPaging ) {
			self._ostream.beforeKeypress( data, key );
			return;
		}
		if ( self._isCapturingKeybinding ) {
			self._captureKeybinding( key );
			return;
		}
		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' ) ) {
			self._completerEngine.closeCompleter();
		} else if ( ( key && key.name === 'tab' ) || self._completerEngine.isNavigating() ) {
			self._completerEngine.beforeKeypress( data, key );
			return;
		}
		// If completion was auto-completed or an action was triggered, don't trigger multi-line keybindings to avoid double operations...
		if ( !completed && !FLG ) {
			self._multilineHandler.beforeKeypress( data, key );
			return;
		}
		if ( !FLG ) {
			self._ttyWrite.call( self._rli, data, key );
		}
	}

	/**
	* Callback invoked upon an input stream "keypress" event.
	*
	* @private
	* @param {string} data - input data
	* @param {(Object|void)} key - key object
	* @returns {void}
	*/
	function onKeypress( data, key ) {
		var autoClosed;

		autoClosed = self._autoCloser.onKeypress( data, key );

		// If auto-closing was performed, explicitly remove any currently displayed completion preview...
		if ( autoClosed ) {
			self._previewCompleter.clear();
		}
		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 );
	}

	/**
	* Callback invoked upon a readline interface "line" event.
	*
	* @private
	* @param {string} line - line data
	*/
	function onLine( line ) {
		self._SIGINT = false; // reset flag
		if ( self._closed === false ) {
			self._multilineHandler.processLine( line );
		}
	}

	/**
	* Callback invoked upon a readline interface "close" event.
	*
	* @private
	*/
	function onClose() {
		if ( self._settings.bracketedPaste && self._settings.autoDisableBracketedPasteOnExit ) { // eslint-disable-line max-len
			self._multilineHandler.disableBracketedPaste();
		}
		ostream.end();
		ostream.unpipe();

		debug( 'Readline interface closed.' );
		self._istream.removeListener( 'keypress', onKeypress );
		proc.removeListener( 'SIGWINCH', onSIGWINCH );

		debug( 'Exiting REPL...' );
		self.emit( 'exit' );
	}

	/**
	* Callback invoked upon receiving a "SIGWINCH" event (i.e., a terminal/console resize event).
	*
	* @private
	* @returns {void}
	*/
	function onSIGWINCH() {
		debug( 'Received a SIGWINCH event. Terminal was resized.' );
		self._ostream.onResize();
		self._completerEngine.onResize();
	}

	/**
	* Callback invoked upon receiving a "SIGINT" event (e.g., Ctrl-C).
	*
	* @private
	* @returns {void}
	*/
	function onSIGINT() {
		var isEmpty;

		debug( 'Received a SIGINT event.' );

		// If the REPL is currently busy executing a command, stop waiting for it to finish:
		if ( self._busy ) {
			self._done( new Error( 'unexpected error. Command execution terminated.' ) );
			return;
		}
		// Check whether the user has entered any characters:
		isEmpty = ( self._rli.line.length === 0 );

		// Close the completer engine:
		self._completerEngine.closeCompleter();

		// Clear the current line:
		self.clearLine();

		// In the absence of any entered partial and/or unevaluated commands, determine whether we should close the REPL...
		if ( self._cmd.length === 0 && isEmpty ) {
			if ( self._SIGINT ) {
				self._SIGINT = false;
				self.close();
				return;
			}
			self._ostream.write( 'To exit, enter ^D or ^C again or enter quit().\n' );
			self._SIGINT = true;
		} else {
			self._SIGINT = false;
		}
		// Reset the command queue:
		self._queue.clear();

		// Clear any command which has been buffered but not yet executed:
		self.clearCommand();

		// Display a new prompt:
		displayPrompt( self, false );
	}

	/**
	* Callback invoked upon executing a command.
	*
	* @private
	* @param {string} cmd - command
	* @param {boolean} success - boolean indicating whether the command successfully executed
	*/
	function onCommand( cmd, success ) {
		self._count += 1;
		debug( 'Command count: %d', self._count );

		// Update the internal command history buffer: [..., <id>, <cmd>, <success>, ...]
		self._history.push( self._count, cmd, success );

		// TODO: if successful and if necessary, (asynchronously?) write the command to a history file (question: do we only want to write successful commands to the history file? maybe we need to option for limiting to successful commands?)

		// TODO: if necessary, (asynchronously?) write the command and result to a log file (JSON serialization?)
	}
}
REPL constructor.

@constructor
@param {Options} options - constructor options
@param {string} [options.inputPrompt='In [%d]: '] - input prompt
@param {string} [options.outputPrompt='Out[%d]: '] - output prompt
@param {ReadableStream} [options.input=process.stdin] - input stream
@param {WritableStream} [options.output=process.stdout] - output stream
@param {boolean} [options.sandbox=true] - boolean indicating whether to run a REPL in a sandboxed context
@param {PositiveInteger} [options.timeout=4294967295] - number of milliseconds to execute a command before terminating execution
@param {boolean} [options.isTTY] - boolean indicating whether the input and output streams should be treated like a TTY (terminal) and whether the REPL should use ANSI/VT100 escape codes when writing to the output stream
@param {string} [options.welcome] - welcome message
@param {NonNegativeInteger} [options.padding=1] - number of empty lines between successive commands
@param {Object} [options.themes] - table containing color themes for syntax highlighting
@param {string} [options.load] - file path specifying a JavaScript file to load and evaluate line-by-line (e.g., a previous REPL history file)
@param {string} [options.save] - file path specifying where to save REPL command history
@param {string} [options.log] - file path specifying where to save REPL commands and printed output
@param {string} [options.quiet=false] - boolean indicating whether log information, confirmation messages, and other possible REPL diagnostics should be silenced
@param {Object} [options.keybindings] - REPL keybindings
@param {Object} [options.settings] - REPL settings
@param {boolean} [options.settings.autoClosePairs=true] - boolean indicating whether to automatically insert matching brackets, parentheses, and quotes
@param {boolean} [options.settings.autoDeletePairs=true] - boolean indicating whether to automatically delete adjacent matching brackets, parentheses, and quotes
@param {boolean} [options.settings.autoPage] - boolean indicating whether to automatically page return values requiring a display size exceeding the visible screen
@param {boolean} [options.settings.bracketedPaste] - boolean indicating whether to enable bracketed-paste mode
@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

@example
var debug = require( '@stdlib/streams/node/debug-sink' );

function onExit() {
    console.log( 'REPL closed.' );
}

// Create a new REPL:
var repl = new REPL({
    'output': debug()
});
repl.on( 'exit', onExit );

// ...

// Close the REPL:
repl.close();