Source: FishProgram.js

"use strict";

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

const InstructionPointer = require("./InstructionPointer");
const FishStack = require("./FishStack");
const DirectionChangers = require("./DirectionChangers");
const Operators = require("./Operators");
const Reflection = require("./Reflection");
const Stream = require("./Stream");

/*************************
 * The FishProgram class *
 *************************/

/**
 * A runnable ><> program
 */
class FishProgram {
	/**
	 * Creates a new ><> program
	 *
	 * @param {String} source	The program's source code
	 * @param {Number[]} [initialStack]	The program's initial stack. Defaults to an empty stack
	 */
	constructor(source, initialStack = []) {
		/**
		 * The source code of the program
		 *
		 * @type String
		 *
		 * @private
		 */
		this._source = source;

		// Convert the source code to a grid
		let grid = source
		  .split("\n")
		  .map((line) => line.split(""));

		/**
		 * The width of the program
		 *
		 * @type Integer
		 *
		 * @private
		 */
		this._width = grid.reduce((longest, line) => Math.max(longest, line.length), 0);

		/**
		 * The height of the program
		 *
		 * @type Integer
		 *
		 * @private
		 */
		this._height = grid.length;

		// Make sure every line is the same length
		grid = grid
		  .map((line) => line.concat(new Array(this.width - line.length).fill(" ")));

		// Put it on this object
		/**
		 * The program grid
		 *
		 * @type String[][]
		 *
		 * @private
		 */
		this._grid = grid;

		/**
		 * The program's instruction pointer
		 *
		 * @type InstructionPointer
		 *
		 * @private
		 */
		this._ip = new InstructionPointer(-1, 0, InstructionPointer.DIRECTION_EAST, this);	// XXX -1 in x direction so that it starts at (0,0) after first advance

		/**
		 * The program's stack
		 *
		 * @type FishStack
		 *
		 * @private
		 */
		this._stack = new FishStack(this, initialStack);

		/**
		 * The program's input stream
		 *
		 * @type Stream
		 *
		 * @private
		 */
		this._inputStream = new Stream();

		/**
		 * The program's output stream
		 *
		 * @type Stream
		 *
		 * @private
		 */
		this._outputStream = new Stream();

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

		/**
		 * Error if an error has occured
		 *
		 * @type Error
		 *
		 * @private
		 */
		this._error = null;

		/**
		 * If reading a string, this is the string termination character. If not, this is null
		 *
		 * @type {String|null}
		 *
		 * @private
		 */
		this._reading = null;
	}

	/**
	 * Width of the program
	 *
	 * @type Integer
	 *
	 * @readonly
	 */
	get width() {
		return this._width;
	}

	/**
	 * Height of the program
	 *
	 * @type Integer
	 *
	 * @readonly
	 */
	get height() {
		return this._height;
	}

	/**
	 * The source code of the program
	 *
	 * @type String
	 *
	 * @readonly
	 */
	get source() {
		return this._source;
	}

	/**
	 * The program grid
	 *
	 * @type String[][]
	 *
	 * @readonly
	 */
	get grid() {
		// Return a clone
		return this._grid
		  .map((line) => [...line]);
	}

	/**
	 * The instruction pointer of the program
	 *
	 * @type InstructionPointer
	 *
	 * @readonly
	 */
	get instructionPointer() {
		return this._ip;
	}

	/**
	 * The program's stack
	 *
	 * @type FishStack
	 *
	 * @readonly
	 */
	get stack() {
		return this._stack;
	}

	/**
	 * Whether or not the program has finished
	 *
	 * @type Boolen
	 *
	 * readonly
	 */
	get hasTerminated() {
		return this._hasTerminated;
	}

	/**
	 * Gives a character to the program's input stream
	 *
	 * @param {String} c	The character to give
	 *
	 * @throws {Error}	If c is not a string with exactly one character
	 */
	giveInput(c) {
		// Check that it is indeed a character
		if (typeof c !== "string" || c.length !== 1) {
			throw new Error("Input must be a single character");
		}

		// Write it to the input stream
		this._inputStream.write(c);
	}

	/**
	 * A snapshot of the program's input stream's buffer
	 *
	 * @type Object[]
	 */
	get inputBuffer() {
		return this._inputStream.snapshot;
	}

	/**
	 * Reads a value from the program's output stream
	 *
	 * @throws {Error}	If there is nothing to read
	 */
	readOutput() {
		return this._outputStream.read();
	}

	/**
	 * Advances the program one step
	 *
	 * @returns {Boolean}	True if the program finished with this advance. False otherwise
	 *
	 * @throws {Error}	If something smells fishy, or the program has already terminated
	 */
	advance() {
		// Check if the program has terminated
		if (this._hasTerminated) {
			// Refuse to do anything
			throw new Error("Program has already terminated");
		}

		try {
			// Move the instruction pointer
			this._ip.advance();

			// Get the character at the new position
			const c = this._grid[this._ip.y][this._ip.x];

			// Check if a string is being read
			if (this._reading !== null) {

				// Check if this is the end of the string
				if (c === this._reading) {
					// Yup. Stop reading
					this._reading = null;
				} else {
					// Push the unicode value of the character to the stack
					this._stack.push(c.charCodeAt(0));
				}
			} else {
				// This is an instruction. Figure out what to do with it
				if (FishProgram.LITERALS.includes(c)) {

					// The char is a hex value. Push it on the stack
					const number = Number.parseInt(c, 16);
					this._stack.push(number);

				} else if (FishProgram.DIRECTION_CHANGERS.includes(c)) {

					// Ask the DirectionChangers for the new direction
					const newDirection = DirectionChangers[c](this._ip.direction);

					// Change it
					this._ip.changeDirection(newDirection);

				} else if (FishProgram.MATH_OPERATORS.includes(c)) {

					// Consult the Operators object
					Operators[c](this._stack);

				} else if (FishProgram.STACK_MANIPULATORS.includes(c)) {

					// Consult the stack
					this._stack[c]();

				} else if (FishProgram.REFLECTION_INSTRUCTIONS.includes(c)) {

					// Consult the Reflection object
					Reflection[c](this._stack, this._grid);

				} else {
					// Some other instruction. Find out exactly what
					switch (c) {
						// Noop
						case " ":
							break;
						// Trampolines
						case "?":
							// Conditional trampoline. Advance the IP again if the top stack element is zero
							if (this._stack.pop() === 0) {
								this._ip.advance();
							}
							break;
						case "!":
							// Unconditional trampoline. Advance the IP again
							this._ip.advance();
							break;
						// Teleport
						case ".":
							// The new IP coordinates are the two top stack values
							const y = this._stack.pop();
							const x = this._stack.pop();

							// Teleport the IP
							this._ip.teleport(x, y);
							break;
						// String start
						case "'":
						case '"':
							// Start reading a string
							this._reading = c;
							break;
						// I/O
						case "o": {
							// Pop the top and output it as a character
							const num = this._stack.pop();
							this._outputStream.write(String.fromCharCode(num));
							break;
						}
						case "n": {
							// Pop the top and output it as a number
							const num = this._stack.pop();
							this._outputStream.write(num);
							break;
						}
						case "i": {
							// Read one character from stdin and push its numerical value onto the stack
							let num = null;
							try {
								const c = this._inputStream.read();
								num = c.charCodeAt(0);
							} catch (err) {
								// Nothing to read. Default to -1
								num = -1;
							}
							this._stack.push(num);
							break;
						}
						// Termination
						case ";":
							// Terminate the program
							this._hasTerminated = true;
							break;
						// Catch all
						default:
							// Invalid instruction
							throw new Error("Invalid instruction");
					}
				}
			}
		} catch (err) {
			// Terminate the program and set the error message
			this._hasTerminated = true;
			this._error = new Error("Something smells fishy...");

			// Be a little bit kind and log the error to the console
			console.error(err.message);
		}
	}

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

	/**
	 * List of characters which should be treated as literal hex values
	 *
	 * @type String[]
	 *
	 * @constant
	 */
	static get LITERALS() {
		return "0123456789abcdef".split("");
	}

	/**
	 * List of dirtection changing characters
	 *
	 * @type String[]
	 *
	 * @constant
	 */
	static get DIRECTION_CHANGERS() {
		return "<>^v/\\_|#x".split("");
	}

	/**
	 * List of math operator characters
	 *
	 * @type String[]
	 *
	 * @constant
	 */
	static get MATH_OPERATORS() {
		return "+-*,%=()".split("");
	}

	/**
	 * List of stack manipulator characters
	 *
	 * @type String[]
	 *
	 * @constant
	 */
	static get STACK_MANIPULATORS() {
		return ":~$@}{rl[]&".split("");
	}

	/**
	 * List of reflection characters
	 *
	 * @type String[]
	 *
	 * @constant
	 */
	static get REFLECTION_INSTRUCTIONS() {
		return "gp".split("");
	}
}

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

module.exports = FishProgram;