Skip to content
Snippets Groups Projects
Commit e7163ddd authored by jm15g21's avatar jm15g21
Browse files

Crazy interpreter mk I

parent 503df360
Branches stack
No related tags found
No related merge requests found
Showing
with 808 additions and 0 deletions
package Exceptions;
public class InvalidIdentifierException extends Exception {
public InvalidIdentifierException(String errorMessage) {
super(errorMessage);
}
}
package Exceptions;
public class NegativeVariableException extends Exception {
public NegativeVariableException(String errorMessage) {
super(errorMessage);
}
}
package Interpreter;
import java.util.Arrays;
public class Instruction {
public Instruction(int instructionIndex, String lineText) {
this.instructionIndex = instructionIndex;
//Parse the line text and get the identifier and arguments
//Sanitize away bad whitespace, including multiple spaces,
//a random space at the start of a line for no reason and all
//without removing the spaces between arguments
String sanitizedText = lineText.replaceAll("[^\\S ]| +|^ *|;$", "");
if(sanitizedText.contains(" ")) {
identifier = sanitizedText.substring(0, sanitizedText.indexOf(" "));
//Gets all but the first element of the lineText array as arguments
arguments = sanitizedText.substring(sanitizedText.indexOf(" ") + 1).split(" ");
} else {
identifier = sanitizedText;
arguments = new String[]{ };
}
//Debugging mode
System.out.println(instructionIndex + ": " + identifier + "(" + String.join(", ", arguments) + ")");
}
//The index of the instruction in the list of instructions
private int instructionIndex;
//Identifier of the instruction
private String identifier;
//Arguments of the instruction
private String arguments[];
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public String[] getArguments() {
return arguments;
}
public void setArguments(String[] arguments) {
this.arguments = arguments;
}
public int getInstructionIndex() {
return instructionIndex;
}
public void setInstructionIndex(int instructionIndex) {
this.instructionIndex = instructionIndex;
}
}
package Interpreter;
import Exceptions.InvalidIdentifierException;
import Exceptions.NegativeVariableException;
import Managers.IManager;
import Managers.LoopManager;
import Operations.*;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
public class Interpreter {
private final boolean SIMPLIFIED_VIEW = false;
//A store of all the variables
private Hashtable<String, Integer> variableStore = new Hashtable<>();
public Interpreter() {
setupManagers();
setupOperations();
}
//=======================
// Basic Functionality
//=======================
public int executeProgram(int maxIterations) {
//Set the instruction pointer to the first one
instructionPointer = 0;
int remainingIterations = maxIterations;
while(remainingIterations > 0 && !isFinished())
{
try {
executeInstruction();
} catch (Exception e) {
e.printStackTrace();
return 1;
}
//Sanity Check
remainingIterations --;
}
if(remainingIterations <= 0)
{
System.out.println("REACHED MAXIMUM REPETITION DEPTH: 5000");
}
//Successful operation, return 0
return 0;
}
/**
* Executes a single instruction and moves on
* Works kind of like a CPU in some weird and abstract way
* which is pretty cool... I guess
*/
public void executeInstruction() throws NegativeVariableException, InvalidIdentifierException {
//Fetch the current instruction
Instruction currentInstruction = getCurrentInstruction();
//Get the operation from the instruction
IOperation instructionsOperation = fetchOperationFromIdentifier(currentInstruction.getIdentifier());
//Check
if(instructionsOperation == null)
{
System.err.println("Unknown instruction: " + currentInstruction.getIdentifier());
System.exit(1);
}
//Check for altered instruction pointer
int lastInstrPointer = instructionPointer;
//Execute the instruction
instructionsOperation.operate(currentInstruction, this);
//Print the state of the interpreter
if(lastInstrPointer == instructionPointer) {
printState();
}
//Increment the instruction pointer
instructionPointer ++;
}
/**
* @return Returns true if we have completed execution of the current program
*/
public boolean isFinished() {
return instructionPointer >= instructions.size();
}
/**
* Ah finally, after writing the entire interpreter without running a single test on anything
* the ability to read a program finally exists.
* This reads a program from a file and then loads it into the interpreter.
*/
public void readProgram(String fileName) {
//Clear out old instructions
instructions.clear();
//Create the stuff required to read a file
BufferedReader bufferedReader;
//Create the file reader and buffered reader.
try {
FileReader fileReader = new FileReader(fileName);
bufferedReader = new BufferedReader(fileReader);
} catch (FileNotFoundException e) {
System.out.println("File could not be located!");
return;
}
//Begin reading
try {
//Get the first line
String line = bufferedReader.readLine();
//Keep track of the index, it will be important later
int index = 0;
//Keep reading until we reach the end
while (line != null) {
//Process this line, add the new instruction to the instruction list
Instruction instruction = new Instruction(index, line);
instructions.add(instruction);
//Read the next line
line = bufferedReader.readLine();
//Increment the index
index ++;
}
} catch (IOException e) {
e.printStackTrace();
return;
}
System.out.println("====PROGRAM READ====");
}
public Hashtable<String, Integer> getVariableStore() {
return variableStore;
}
//=======================
// Operations
// A unique set of singleton objects that determine how to act
// when a specific operation is called.
// There exists 1 for each operation. (clear sets variable to 0 etc.)
//=======================
//A store of the operations
private Hashtable<String, IOperation> interpreterOperations = new Hashtable<>();
public IOperation fetchOperationFromIdentifier(String identifier) {
return interpreterOperations.get(identifier);
}
private void setupOperations() {
addOperation(new OperationClear());
addOperation(new OperationDecr());
addOperation(new OperationEnd());
addOperation(new OperationIncr());
addOperation(new OperationWhile());
}
private void addOperation(IOperation operation)
{
interpreterOperations.put(operation.getOperationIdentifier(), operation);
}
//=======================
// Instructions
// The list of instructions we are reading, line by line.
//=======================
//Pointer to where the current instruction is
private int instructionPointer;
private ArrayList<Instruction> instructions = new ArrayList<>();
public Instruction getCurrentInstruction() {
return instructions.get(instructionPointer);
}
public Instruction fetchInstruction(int lineNumber) {
return instructions.get(lineNumber);
}
/**
* Jumps to an instruction
* Note that the instruction jumping jumps to BEFORE the instruction, not after it
*/
public void jumpToInstruction(Instruction instruction) {
instructionPointer = instruction.getInstructionIndex();
}
/**
* Jumps to an instruction
* Note that the instruction jumping jumps to BEFORE the instruction, not after it
*/
public void jumpToInstructionIndex(int instructionIndex) {
instructionPointer = instructionIndex;
}
//=======================
// Managers
// what is a manager?
// A manager is a way of storing stuff on the interpreter without putting it directly on
// I don't know why I did it this way tbh seems pointless but clean I guess?
//=======================
//A dictionary of all the managers with their type as the key.
private Hashtable<Class, IManager> managerList;
private void setupManagers() {
managerList = new Hashtable<>();
addManager(new LoopManager());
}
private void addManager(IManager manager) {
managerList.put(manager.getClass(), manager);
}
public IManager getManager(Class managerType) {
return managerList.get(managerType);
}
//========== UTIL
public void printState() {
if(SIMPLIFIED_VIEW)
{
System.out.println("\n" + getCurrentInstruction().getIdentifier() + "(" + String.join(", ", getCurrentInstruction().getArguments()) + ")");
for (String variableName: variableStore.keySet()) {
int value = variableStore.get(variableName);
System.out.print(variableName + ": " + value + " | ");
}
return;
}
System.out.println("===========================");
System.out.println(" CURRENT INTERPRETER STATE ");
System.out.println("===========================");
System.out.println(" STATUS");
System.out.println("===========================");
System.out.println("Instruction Pointer: " + instructionPointer);
System.out.println("Instruction Count: " + instructions.size());
if(instructionPointer < instructions.size()) {
System.out.println("Instruction: " + getCurrentInstruction().getIdentifier());
System.out.println("Arguments: " + String.join(", ", getCurrentInstruction().getArguments()));
}
System.out.println("===========================");
System.out.println(" VARIABLES");
System.out.println("===========================");
for (String variableName: variableStore.keySet()) {
int value = variableStore.get(variableName);
System.out.println(variableName + ": " + value);
}
System.out.println("===========================");
}
}
import Interpreter.Interpreter;
public class Main {
public static void main(String args[]) {
Interpreter interpreter = new Interpreter();
//TODO: Im lazy so we hard code the max iterations and program to run
interpreter.readProgram(args.length == 0 ? "Programs/MultipleTwoNumbers.bb" : args[0]);
interpreter.executeProgram(5000);
}
}
package Managers;
public interface IManager {
}
package Managers;
import Interpreter.Instruction;
import Operations.ILoopOperation;
import java.util.Stack;
public class LoopManager implements IManager {
//The loop stack
private Stack<Instruction> loopStack = new Stack<>();
public void addLoopToStack(Instruction parentInstruction) {
loopStack.push(parentInstruction);
}
public Instruction getParentLoop() {
return loopStack.peek();
}
public void breakLoop() {
loopStack.pop();
}
}
package Operations;
public interface ILoopOperation {
}
package Operations;
import Exceptions.InvalidIdentifierException;
import Exceptions.NegativeVariableException;
import Interpreter.Interpreter;
import Interpreter.Instruction;
public interface IOperation {
/**
* Gets the identifier of the operation, what is used to call it.
* @return a string that represents the ID of the operation
*/
public String getOperationIdentifier();
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
public boolean validate(String args[]);
/**
* Performs the operation
* @param instruction The line number that the operation was called on
* @param master The master interpreter that called this operation
*/
void operate(Instruction instruction, Interpreter master) throws NegativeVariableException, InvalidIdentifierException;
}
package Operations;
import Interpreter.Interpreter;
import Interpreter.Instruction;
public class OperationClear implements IOperation {
/**
* Gets the identifier of the operation, what is used to call it.
*
* @return a string that represents the ID of the operation
*/
@Override
public String getOperationIdentifier() {
return "clear";
}
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
*
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
@Override
public boolean validate(String[] args) {
//Clear operation requires a single argument, the variable being cleared.
return args.length == 1;
}
/**
* Performs the operation
* @param instruction
* @param master The master interpreter that called this operation
*/
@Override
public void operate(Instruction instruction, Interpreter master) {
//Remove the variable from the variable store, if it exists.
master.getVariableStore().remove(instruction.getArguments()[0]);
}
}
package Operations;
import Exceptions.NegativeVariableException;
import Interpreter.Interpreter;
import Interpreter.Instruction;
import java.util.Hashtable;
public class OperationDecr implements IOperation{
/**
* Gets the identifier of the operation, what is used to call it.
*
* @return a string that represents the ID of the operation
*/
@Override
public String getOperationIdentifier() {
return "decr";
}
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
*
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
@Override
public boolean validate(String[] args) {
//Decrement takes a single argument, the variable to be decremented.
return args.length == 1;
}
/**
* Performs the operation
* @param instruction
* @param master The master interpreter that called this operation
*/
@Override
public void operate(Instruction instruction, Interpreter master) throws NegativeVariableException {
//Get the variable store, returned as a reference so is the same as the one in the interpreter
//It could be static I guess, but we can have multiple interpreters running at once if we ever
//needed that for some reason.
Hashtable<String, Integer> variableStoreReference = master.getVariableStore();
//Check if the variable already exists in the variable store
if (variableStoreReference.containsKey(instruction.getArguments()[0]))
{
//Decrement the variable with the name passed in as argument by 1.
int integerObjectReference = variableStoreReference.get(instruction.getArguments()[0]);
if(integerObjectReference == 1) {
//Remove the reference to it, since it is now 0
variableStoreReference.remove(instruction.getArguments()[0]);
}
else {
//Put it back in, referencing doesn't work on Integer class?
variableStoreReference.replace(instruction.getArguments()[0], integerObjectReference - 1);
}
}
else
{
throw new NegativeVariableException("Variable " + instruction.getArguments()[0] + " decremented without existing.");
}
}
}
package Operations;
import Exceptions.InvalidIdentifierException;
import Exceptions.NegativeVariableException;
import Interpreter.Interpreter;
import Interpreter.Instruction;
import Managers.LoopManager;
public class OperationEnd implements IOperation {
/**
* Gets the identifier of the operation, what is used to call it.
*
* @return a string that represents the ID of the operation
*/
@Override
public String getOperationIdentifier() {
return "end";
}
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
*
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
@Override
public boolean validate(String[] args) {
return false;
}
/**
* Performs the operation
* @param instruction The line number that the operation was called on
* @param master The master interpreter that called this operation
*/
@Override
public void operate(Instruction instruction, Interpreter master) throws NegativeVariableException, InvalidIdentifierException {
//Fetch the loop manager from the master
LoopManager loopManager = (LoopManager) master.getManager(LoopManager.class);
//Peek at our master loop's line
Instruction parentLoopInstruction = loopManager.getParentLoop();
//Get the operation for that line so we can determine validity of loop condition
OperationLoop loopOperation = (OperationLoop) master.fetchOperationFromIdentifier(
parentLoopInstruction.getIdentifier());
//if the condition is satisfied jump the interpreter backwards
//otherwise break out of that loop (by continuing as usual)
if(loopOperation.isSatisfied(master, parentLoopInstruction))
{
//Print the state to show we reached the end
master.printState();
//Jump the interpreter to the line after the parent loop and continue
master.jumpToInstructionIndex(parentLoopInstruction.getInstructionIndex());
}
else
{
//We just broke out of the loop, so indicate this to the loop manager so at the next loop
//we jump back to the correct loop and not the old one
loopManager.breakLoop();
}
}
}
package Operations;
import Interpreter.Interpreter;
import Interpreter.Instruction;
import java.util.Hashtable;
public class OperationIncr implements IOperation{
/**
* Gets the identifier of the operation, what is used to call it.
*
* @return a string that represents the ID of the operation
*/
@Override
public String getOperationIdentifier() {
return "incr";
}
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
*
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
@Override
public boolean validate(String[] args) {
//Increment takes a single argument, the variable to be incremented.
return args.length == 1;
}
/**
* Performs the operation
* @param instruction
* @param master The master interpreter that called this operation
*/
@Override
public void operate(Instruction instruction, Interpreter master) {
//Get the variable store, returned as a reference so is the same as the one in the interpreter
//It could be static I guess, but we can have multiple interpreters running at once if we ever
//needed that for some reason.
Hashtable<String, Integer> variableStoreReference = master.getVariableStore();
//Check if the variable already exists in the variable store
if (variableStoreReference.containsKey(instruction.getArguments()[0]))
{
//Incremember the variable with the name passed in as argument by 1.
Integer integerObjectReference = variableStoreReference.get(instruction.getArguments()[0]);
integerObjectReference ++;
//Put it back in, referencing doesn't work on Integer class?
variableStoreReference.replace(instruction.getArguments()[0], integerObjectReference);
}
else
{
variableStoreReference.put(instruction.getArguments()[0], new Integer(1));
}
}
}
package Operations;
import Exceptions.InvalidIdentifierException;
import Exceptions.NegativeVariableException;
import Interpreter.Interpreter;
import Interpreter.Instruction;
import Managers.LoopManager;
public class OperationLoop implements ILoopOperation, IOperation {
/**
* Gets the identifier of the operation, what is used to call it.
*
* @return a string that represents the ID of the operation
*/
@Override
public String getOperationIdentifier() {
return "";
}
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
*
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
@Override
public boolean validate(String[] args) {
return true;
}
/**
* Performs the operation
* @param instruction
* @param master The master interpreter that called this operation
*/
@Override
public void operate(Instruction instruction, Interpreter master) throws NegativeVariableException {
//Fetch the manager from the master
LoopManager loopManager = (LoopManager) master.getManager(LoopManager.class);
//Add this loop to the loop manager's stack
loopManager.addLoopToStack(instruction);
}
/***
* Determines if the loop condition is satisfied or not
*/
public boolean isSatisfied(Interpreter interpreter, Instruction instruction) throws InvalidIdentifierException {
return false;
}
}
package Operations;
import Exceptions.InvalidIdentifierException;
import Interpreter.Interpreter;
import Interpreter.Instruction;
import java.util.Objects;
public class OperationWhile extends OperationLoop {
/**
* Gets the identifier of the operation, what is used to call it.
*
* @return a string that represents the ID of the operation
*/
@Override
public String getOperationIdentifier() {
return "while";
}
/**
* Validates that the operation is written in the correct format (It has the right amount of arguments).
*
* @param args the arguments passed to the operation
* @return True if the operation is valid, False if it is invalid
*/
@Override
public boolean validate(String[] args) {
return args.length == 4 && args[1] == "not" && args[2] == "0" && args[3] == "do";
}
/***
* Determines if the loop condition is satisfied or not
*/
@Override
public boolean isSatisfied(Interpreter interpreter, Instruction instruction) throws InvalidIdentifierException {
//Ensure that it's actually the real deal; our way of handling operations is very strange
//and really operations and instructions should be merged into a unique object for each line
if(!Objects.equals(instruction.getIdentifier(), "while"))
{
throw new InvalidIdentifierException("Error: Line " + instruction.getInstructionIndex() +
" was expecting a while " +
"loop operation, got something else (" + instruction.getIdentifier() + ")");
}
//Check satisfaction of the loop condition
String variableToCheck = instruction.getArguments()[0];
//The while condition is always not 0
//This means that if the variable exists in the interpreters variable
//store, then we have passed the condition
if(interpreter.getVariableStore().containsKey(variableToCheck))
{
return true;
}
//The variable does not exist in the variable store, meaning it is 0.
return false;
}
}
The interpreter runs line by line and executes .bb programs.
.bb is a plaintext .txt file with a barebones program inside it.
The interpreter has multiple parts:
The variable store - This stores all non-0 variables in a dictionary with the
variable's name as the key. If a variable goes down to 0, it is removed
from this dictionary.
Operations - Operations are a list of IOperation implemented objects that can
be called by the interpreter. Only a single operation object exists per
possible operation (There is a single incr operation, a single decr operation etc.).
Instructions - Instructions are a list of the instructions that the interpreter
should act upon. There exists 1 instruction object for each line of code.
Instructions hold the operation to call, the arguments to call it with and the
index of that instruction.
Managers - Managers just store stuff, they allow things like loop management
to be done in a place that isn't the interpreter which just cleans up the
interpreter class a little.
Multiple interpreters can be created at once, if you wanted to do that for some
reason. Could make it multi-threaded in the future I guess.
The aim of this was to be easily scalable cause it looks like the next week
challenge will probably be to extend this mess.
Adding new operations
==============================
Adding a new operation to the interpreter is simple.
Create an operation class in the operations package and have it implement
IOperation.
Override the default methods:
getOperationIdentifier must be overriden to return a unique string. This string
determines what in code will call the operation. For example OperationIncrement's
identifier is incr, so incr x will call the increment operator.
operate will be called when the operation is executed. Override this and implement
custom behaviour for the operation. The instruction paramater contains the
instruction object that called the operation, which contains the current line
number of the program as well as the arguments. Master is a reference to the
interpreter that called the operation, which holds the program's variables.
validate is currently unused, but should be used to validate that commands are
actually formatted properly. This should be used to have the interpreter stop
when something is formatted rather than crash or perform weirdly.
Once all this is done add the new operation to the list of operations by
adding `addOperation(new OperationX());` to setupOperations() inside of
the interpreter class.
Accessing Interpreter Variables
===========================
getVariableStore will return the hashtable used by the interpreter to store
variables. The key is the name of the variable and the value is an integer
object.
Important to note is that the integer values inside the variable store can't
be directly modified and will have to be replaced.
(IE: Instead of doing variableStore.get("X") ++, you need to do
variableStore.replace("X", variableStore.get("X") + 1))
TODO: Refactor variable store to have a helper method on the interpreter.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment