Source: FishExecutor.js

"use strict";

/**************************
 * Import important stuff *
 **************************/

const FishProgram = require("./FishProgram");

/**************************
 * The FishExecutor class *
 **************************/

/**
 * Executes a ><> program
 */
class FishExecutor {
	/**
	 * Creates a new FishExecutor
	 *
	 * @param {String} source	Source code for the program to execute
	 * @param {Number[]} [initialStack]	Initial stack for the program. Defaults to an empty list
	 */
	constructor(source, initialStack) {
		/**
		 * The program itself
		 *
		 * @type FishProgram
		 *
		 * @private
		 */
		this._program = new FishProgram(source, initialStack);

		/**
		 * Output string generated by the program
		 *
		 * @type String
		 *
		 * @private
		 */
		this._output = "";

		/**
		 * Whether or not the program is paused
		 *
		 * @type Boolean
		 *
		 * @private
		 */
		this._isPaused = false;

		/**
		 * Whether or not the program has started
		 *
		 * @type Boolean
		 *
		 * @private
		 */
		this._hasStarted = false;

		/**
		 * Interval (ms) between two advances when running at intervals
		 *
		 * @type Integer
		 *
		 * @private
		 */
		this._intervalTime = 100;

		/**
		 * The timeout for the next run
		 *
		 * @type {Identifier}
		 *
		 * @private
		 */
		this._timeout = null;

		/**
		 * Listeners for updates
		 *
		 * @type Function[]
		 *
		 * @private
		 */
		this._updateListeners = [];

		/**
		 * Keep track of how many times the program has advanced
		 *
		 * @type Integer
		 *
		 * @private
		 */
		this._advances = 0;

		// Bind methods
		this.run = this.run.bind(this);
		this.pause = this.pause.bind(this);
		this.resume = this.resume.bind(this);
		this.step = this.step.bind(this);
		this.giveInput = this.giveInput.bind(this);
	}

	/**
	 * Number of advances the program has made
	 *
	 * @type Integer
	 *
	 * @readonly
	 */
	get advances() {
		return this._advances;
	}

	/**
	 * The error the program threw, if any. Null otherwise
	 *
	 * @type Error
	 *
	 * @readonly
	 */
	get error() {
		return this._program.error;
	}

	/**
	 * Sets a listener for program updates
	 *
	 * @param {Function} func	Function which is called on every update. Gets this executor as its parameter
	 */
	onUpdate(func) {
		// Make sure the parameter is a function
		if (!(func instanceof Function)) {
			throw new Error("Listener must be a function");
		}

		this._updateListeners.push(func);
	}

	/**
	 * Removes a listener for program updates
	 *
	 * @param {Function} func	The listener to remove
	 */
	offUpdate(func) {
		// Find its index
		const i = this._updateListeners.indexOf(func);

		// If it is there, splice it out
		if (i >= 0) {
			this._updateListeners.splice(i, 1);
		}
	}

	/**
	 * Notifies listeners that something has updated
	 *
	 * @private
	 */
	_updateHappened() {
		this._updateListeners.forEach((listener) => listener(this));
	}

	/**
	 * Snapshot of the program's grid
	 *
	 * @type String[][]
	 *
	 * @readonly
	 */
	get grid() {
		return this._program.grid;
	}

	/**
	 * The program's instruction pointer. Don't mess with it
	 *
	 * @type InstructionPointer
	 */
	get instructionPointer() {
		return this._program.instructionPointer;
	}

	/**
	 * Characters waiting to be read in the input stream
	 *
	 * @type String[]
	 *
	 * @readonly
	 */
	get inputBuffer() {
		return this._program.inputBuffer;
	}

	/**
	 * Gives a character as input to the program
	 *
	 * @param {String} c	The character to give
	 *
	 * @throws {Error}	If c is not a string or is not exactly one character
	 */
	giveInput(c) {
		// Pass it on to the program
		this._program.giveInput(c);

		// Notify listeners
		this._updateHappened();
	}

	/**
	 * The program's current stack
	 *
	 * @type Number[]
	 *
	 * @readonly
	 */
	get stackSnapshot() {
		return this._program.stack.snapshot;
	}

	/**
	 * Interval (ms) between two advances when running at intervals
	 *
	 * @type Integer
	 */
	get intervalTime() {
		return this._intervalTime;
	}
	set intervalTime(newTime) {
		// Verify that it is a number
		if (Number(newTime) === newTime) {
			// Set it, but no lower than 0
			this._intervalTime = Math.max(newTime, 0);

			// Clear the run timeout
			this._clearTimeout();

			// Run it again if it has started
			if (this._hasStarted) {
				this.run();
			}

			// Notify listerners
			this._updateHappened();
		}
	}

	/**
	 * Whether or not the program has started
	 *
	 * @type Boolean
	 *
	 * @readonly
	 */
	get hasStarted() {
		return this._hasStarted;
	}

	/**
	 * Pauses the program execution
	 */
	pause() {
		// Set the flag
		this._isPaused = true;

		// Notify the listeners
		this._updateHappened();
	}

	/**
	 * Resumes program execution
	 */
	resume() {
		// Set the flag to false
		this._isPaused = false;

		// Notify the listeners
		this._updateHappened();

		// Do a run
		this.run();
	}

	/**
	 * Advances the program one step, if the execution is paused
	 */
	step() {
		// Don't do anything if the execution is not paused
		if (!this.isPaused) {
			return;
		}

		// Resume and immediately pause
		this.resume();
		this.pause();
	}

	/**
	 * Sets a new run timeout
	 *
	 * @param {Integer} ms	Millisecons to wait before next run
	 *
	 * @private
	 */
	_setTimeout(ms) {
		this._timeout = setTimeout(() => {
			// Clear the timeout, so `run` wil actually run
			this._clearTimeout();

			// Do another step
			this.run();
		}, ms);
	}

	/**
	 * Clears the run timeout
	 *
	 * @private
	 */
	_clearTimeout() {
		// Clear and reset it
		clearTimeout(this._timeout);
		this._timeout = null;
	}

	/**
	 * Whether or not the execution is paused
	 *
	 * @type Boolean
	 *
	 * @readonly
	 */
	get isPaused() {
		return this._isPaused;
	}

	/**
	 * Whether or not the program has terminated
	 *
	 * @type Boolean
	 *
	 * @readonly
	 */
	get hasTerminated() {
		return this._program.hasTerminated;
	}

	/**
	 * Starts running the program
	 */
	run() {
		// Set the started flag
		this._hasStarted = true;

		// Don't do anything if the execution is paused, or if a run has been scheduled
		if (this.isPaused || this.hasTerminated || this._timeout !== null) {
			return;
		}

		// Check if it should run as fast as possible or at an interval
		let advances = 0;
		if (this._intervalTime === 0) {
			advances = this._runAtFullSpeed();
		} else {
			advances = this._runAtInterval();
		}

		// Update advance count
		this._advances += advances;

		// Call all update listeners
		this._updateHappened();
	}

	/**
	 * Runs the program, yielding after every step and resuming after a timeout
	 *
	 * @private
	 */
	_runAtInterval() {
		// Do one step
		this._advance();

		// Schedule next run
		this._setTimeout(this._intervalTime);

		// One advance was made
		return 1;
	}

	/**
	 * Runs the program at full speed. Yields after a number of advances to allow other things to run
	 *
	 * @private
	 */
	_runAtFullSpeed() {
		// Keep track of how many advances has been made since start, so pauses can be enforced at regular intervals
		let advances = 0;

		// Run a predefined number of advances or until the program terminates
		while (!this.hasTerminated && advances < FishExecutor._FULL_SPEED_ADVANCES) {
			this._advance();

			// Count the advance
			advances++;
		}

		// Schedule next run
		this._setTimeout(0);

		// Return number of advances made
		return advances;
	}

	/**
	 * Advances the program one step
	 *
	 * @private
	 */
	_advance() {
		// Advance it
		this._program.advance();

		try {
			// Read the output
			const o = this._program.readOutput();
			this._output += o;
		} catch (err) {
			// Nothing could be read. Don't do anything
		}
	}

	/**
	 * The program itself
	 *
	 * @type FishProgram
	 *
	 * @readonly
	 */
	get program() {
		return this._program;
	}

	/**
	 * Output from the program
	 *
	 * @type String
	 *
	 * @readonly
	 */
	get output() {
		return this._output;
	}

	/**
	 * Number of advances to do before yielding when running at full speed
	 *
	 * @type Integer
	 *
	 * @private
	 *
	 * @constant
	 */
	static get _FULL_SPEED_ADVANCES() {
		return 10000;
	}
}

/*************
 * Export it *
 *************/

module.exports = FishExecutor;