/*******************************************************************************
*
* @file bf-program.js A Brain Fuck Program Parser
*
* @author Omar Essilfie-Quaye <omareq08+githubio@gmail.com>
* @version 1.0
* @date 08-February-2026
* @link https://omareq.github.io/bf-interpreter/
* @link https://omareq.github.io/bf-interpreter/docs/
*
*******************************************************************************
*
* GNU General Public License V3.0
* --------------------------------
*
* Copyright (C) 2026 Omar Essilfie-Quaye
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*****************************************************************************/
"use strict";
// TODO: Think about 8/16/32/64/BIGINT bit instruction set
/**
* A class which is the interface for CPU Instructions.
*
* @see BFCpu
* @see BFProgram
*/
class BFInstruction {
/**
* Constructor for the BFInstructor Abstract Interface class.
*
* @param symbol {String} - The symbol for the instruction
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*
* @throws {Error} Abstract class BFInstruction can't be instantiated
*/
constructor(symbol, locationIndex) {
let err = "Abstract class BFInstruction can't be instantiated.";
if(this.constructor == BFInstruction) {
throw new Error(err);
}
this.symbol = symbol;
this.locationIndex = locationIndex;
}
/**
* Applies the Instruction operation to the CPU.
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*
* @throws {Error} Method Operation() must be implemented.
*/
operation(cpu) {
throw new Error("Method 'operation()' must be implemented.");
}
}
/**
* The plus instruction that increments the current data cell
*/
class Plus extends BFInstruction {
/**
* Constructor for the plus instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*/
constructor(locationIndex) {
super("+", locationIndex);
}
/**
* Applies the plus instruction to the cpu
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
cpu.setCurrentCell(cpu.getCurrentCell() + 1);
}
}
/**
* The Minus instruction that decrements the current data cell
*/
class Minus extends BFInstruction {
/**
* Constructor for the minus instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*/
constructor(locationIndex) {
super("-", locationIndex);
}
/**
* Applies the minus instruction to the cpu
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
cpu.setCurrentCell(cpu.getCurrentCell() - 1);
}
}
/**
* The left shift instruction that shifts the data pointer to the left
*/
class LShift extends BFInstruction {
/**
* Constructor for the Left Shift instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*/
constructor(locationIndex) {
super("<", locationIndex);
}
/**
* Applies the left shift instruction to the cpu
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
cpu.setDataPtr(cpu.getDataPtr() - 1);
}
}
/**
* The right shift instruction that shifts the data pointer to the right
*/
class RShift extends BFInstruction {
/**
* Constructor for the right shift instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*/
constructor(locationIndex) {
super(">", locationIndex);
}
/**
* Applies the right shift instruction to the cpu
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
cpu.setDataPtr(cpu.getDataPtr() + 1);
}
}
/**
* The input instruction that reads the input and places it in the current data
* cell. Uses ASCII formatting.
*/
class Input extends BFInstruction {
/**
* Constructor for the input instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*/
constructor(locationIndex) {
super(",", locationIndex);
}
/**
* Applies the input instruction to the cpu
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
// TODO: Implement Input
cpu.setCurrentCell(0);
}
}
/**
* The output instruction that writes the output from the current cell. Uses
* ASCII formatting.
*/
class Output extends BFInstruction {
/**
* Constructor for the output instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
*/
constructor(locationIndex) {
super(".", locationIndex);
}
/**
* Applies the output instruction to the cpu
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
const char = cpu.getCurrentCell();
const ascii = String.fromCharCode(char);
// TODO: Consider how to buffer
console.log(char + " ASCII: " + ascii);
document.getElementById("output-text").textContent+=ascii;
}
}
/**
* The paired instruction. This saves two locations one for the current
* instruction and one for a jump location.
*
* @see BFInstruction
* @see LBrack
* @see RBrack
*/
class PairedInstruction extends BFInstruction {
/**
* Constructor for the paired instruction abstract class interface
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
* @param pairedLocationIndex {Number} - The location of the jump instruction within the unoptimised source code
*
* @throws {Error} Abstract class PairedInstruction can't be instantiated
*/
constructor(symbol, locationIndex, pairedLocationIndex) {
super(symbol, locationIndex);
let err = "Abstract class PairedInstruction can't be instantiated.";
if(this.constructor == PairedInstruction) {
throw new Error(err);
}
this.pairedLocationIndex = pairedLocationIndex;
}
/**
* Gets the jump location index of the paired instruction
*
* @returns {Number} - The jump location index
*/
jumpLocation() {
return this.pairedLocationIndex;
}
}
/**
* The left bracket instruction which starts a while loop. This is paired with
* the right bracket instruction to close the loop.
*
* @see BFInstruction
* @see PairedInstruction
* @see RBrack
*/
class LBrack extends PairedInstruction {
/**
* Constructor for the Left Bracket instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
* @param pairedLocationIndex {Number} - The location of the jump instruction within the unoptimised source code
*/
constructor(locationIndex, pairedLocationIndex) {
super("[", locationIndex, pairedLocationIndex);
}
/**
* Applies the Left bracket instruction to the cpu. Only jumps to the close
* bracket if the value at the current data cell is zero.
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
if(cpu.getCurrentCell() == 0) {
cpu.setInstructionPtr(this.pairedLocationIndex);
}
}
}
/**
* The right bracket instruction which closes a while loop. This is paired with
* the left bracket instruction to start the loop.
*
* @see BFInstruction
* @see PairedInstruction
* @see LBrack
*/
class RBrack extends PairedInstruction {
/**
* Constructor for the Right Bracket instruction
*
* @param locationIndex {Number} - The location of the instruction within the unoptimised source code
* @param pairedLocationIndex {Number} - The location of the jump instruction within the unoptimised source code
*/
constructor(locationIndex, pairedLocationIndex) {
super("]", locationIndex, pairedLocationIndex);
}
/**
* Applies the right bracket instruction to the cpu. Only jumps to the start
* bracket if the value at the current data cell is not zero. Notionally
* this instruction can be an unconditional jump as the left bracket will
* do this check anyway. However doing the check at the close saves an
* additional cpu cycle jumping back and then forward again.
*
* @param cpu {BFCpu} - The cpu to apply the operation on.
*/
operation(cpu) {
if(cpu.getCurrentCell() != 0) {
cpu.setInstructionPtr(this.pairedLocationIndex);
}
}
}
/**
* A function to remove comments and excess white space from the source code.
*
* @param rawProgramtext {String} - The raw source code
*
* @returns {String} - The preprocessed source code
*/
function preProcess(rawProgramtext) {
const textArr = rawProgramtext.split("");
const program = textArr.filter(operation =>
operation.includes(["+"]) ||
operation.includes(["-"]) ||
operation.includes(["."]) ||
operation.includes([","]) ||
operation.includes(["<"]) ||
operation.includes([">"]) ||
operation.includes(["["]) ||
operation.includes(["]"])
);
return program;
}
/**
* Function to find the matching jump end for a particular left bracket. If not
* matching returns undefined.
*
* @returns {Number} - Jump index location
*/
function getJumpEnd(program, jumpStart) {
// TODO: throw on error
// TODO: Refactor get jump locations into one function with char params and dir
// assert(jumpStart >= program.instructions && jumpStart < program.end);
let i_ptr = jumpStart;
let bracket_cnt = 1;
while (bracket_cnt > 0) {
i_ptr++;
if(i_ptr >= program.length) {
return;
}
if(program[i_ptr] == '[') {
bracket_cnt++;
} else if(program[i_ptr] == ']') {
bracket_cnt--;
}
}
return i_ptr;
}
/**
* Function to find the matching jump start for a particular right bracket. If
* not matching returns undefined.
*
* @returns {Number} - Jump index location
*/
function getJumpStart(program, jumpEnd) {
// TODO: throw on error
// assert(jumpEnd >= program.instructions && jumpEnd < program.end);
let i_ptr = jumpEnd;
let bracket_cnt = 1;
while (bracket_cnt > 0) {
i_ptr--;
if(i_ptr < 0) {
return;
}
if(program[i_ptr] == ']') {
bracket_cnt++;
} else if(program[i_ptr] == '[') {
bracket_cnt--;
}
}
return i_ptr;
}
/**
* Function to compress program using run length encoding for a given char. If
* a run of more than 1 character is found then the sequence of repeated chars
* is replaced with the length of the sequence then the char. EG
*
* optimiseRLE("++++", "+") -> Returns "4+""
*
* Note that the return type is now an array of strings not chars.
*
* @param program {Array<Char>} - Array of source code characters
* @param char {char} - The character to compress
*
* @returns {Array<String>} - Returns array of RLE compressed source code
*/
function optimiseRLE(program, char) {
let optimised = [];
let encodeChar = char;
for(let i = 0; i < program.length; i++) {
if(program[i] == encodeChar && program[i+1] == encodeChar) {
let charCount = 1;
// TODO: replace while loop with for loop ending at end of program
while(program[i + charCount] == encodeChar) {
charCount++;
}
optimised.push(charCount + encodeChar);
i += charCount -1;
continue;
}
optimised.push(program[i]);
}
// console.log("RLE: " + optimised);
return optimised;
}
/**
* Applies optimisations to the source code of the program
* Note that the return type is now an array of strings not chars.
*
* @param program {Array<char>} - The pre processed source code with no comments
*
* @returns {Array<String>} - The optimised source code
*/
function optimiseProgramTxt(program) {
let optimised = optimiseRLE(program, "+");
optimised = optimiseRLE(optimised, "-");
optimised = optimiseRLE(optimised, "<");
optimised = optimiseRLE(optimised, ">");
return optimised;
}
/**
* A function to parse the optimised source code into BFInstruction objects
*
* @param programText {Array<char>} - The source code
*
* @returns {Array<BFInstruction>} - The list of instructions
*/
function parse(programTxt) {
//TODO: Search through BFInstructions compare to symbols automatically
let instructionsList = new Array(programTxt.length);
for(let i = 0; i < programTxt.length; i++) {
if(programTxt[i] == "+") {
instructionsList[i] = new Plus(i);
} else if(programTxt[i] == "-") {
instructionsList[i] = new Minus(i);
} else if(programTxt[i] == "<") {
instructionsList[i] = new LShift(i);
} else if(programTxt[i] == ">") {
instructionsList[i] = new RShift(i);
} else if(programTxt[i] == "[") {
const jumpEndIndex = getJumpEnd(programTxt, i);
instructionsList[i] = new LBrack(i, jumpEndIndex);
} else if(programTxt[i] == "]") {
const jumpStartIndex = getJumpStart(programTxt, i);
instructionsList[i] = new RBrack(i, jumpStartIndex);
} else if(programTxt[i] == ".") {
instructionsList[i] = new Output(i);
} else if(programTxt[i] == ",") {
instructionsList[i] = new Input(i);
}
}
return instructionsList;
}
/**
* A class containing a list of BFInstructions and the length of the code.
*/
class BFProgram {
/**
* The constructor for a BF program
*
* @param instructionsList {Array<BFInstructions>} - The instructions list
*/
constructor(instructionsList) {
this.instructionsList = instructionsList;
this.size = instructionsList.length;
this.length = instructionsList.length;
}
}