/********************************************************************************
* The contents of this file are subject to the GNU General Public License *
* (GPL) Version 2 or later (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at *
* http://www.gnu.org/copyleft/gpl.html *
* *
* Software distributed under the License is distributed on an "AS IS" basis, *
* without warranty of any kind, either expressed or implied. See the License *
* for the specific language governing rights and limitations under the *
* License. *
* *
* This file was originally developed as part of the software suite that *
* supports the book "The Elements of Computing Systems" by Nisan and Schocken, *
* MIT Press 2005. If you modify the contents of this file, please document and *
* mark your changes clearly, for the benefit of others. *
********************************************************************************/
package Hack.VMEmulator;
import Hack.ComputerParts.ComputerPartGUI;
import Hack.ComputerParts.InteractiveComputerPart;
import Hack.Controller.ProgramException;
import Hack.Events.ProgramEvent;
import Hack.Events.ProgramEventListener;
import Hack.Utilities.Definitions;
import Hack.Utilities.HackFileFilter;
import Hack.VirtualMachine.HVMInstructionSet;
import java.io.*;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.Vector;
/**
* A list of VM instructions, with a program counter.
*/
public class VMProgram extends InteractiveComputerPart
implements ProgramEventListener {
// pseudo address for returning to built-in functions
public static final short BUILTIN_FUNCTION_ADDRESS = -1;
// Possible values for the current status - has the user allowed
// access to built-in vm functions?
private static final int BUILTIN_ACCESS_UNDECIDED = 0;
private static final int BUILTIN_ACCESS_AUTHORIZED = 1;
private static final int BUILTIN_ACCESS_DENIED = 2;
// listeners to program changes
private Vector listeners;
// The list of VM instructions
private VMEmulatorInstruction[] instructions;
private int instructionsLength;
private int visibleInstructionsLength;
// The program counter - points to the next instruction that should be executed.
private short nextPC;
private short currentPC;
private short prevPC;
// The gui of the program.
private VMProgramGUI gui;
// The address of the initial instruction
private short startAddress;
// Mapping from file names to an array of two elements, containing the start and
// end addresses of the corresponding static segment.
private Hashtable staticRange;
// Addresses of functions by name
private Hashtable functions;
private short infiniteLoopForBuiltInsAddress;
// The current index of the static variables
private int currentStaticIndex;
// The largest static variable index found in the current file.
private int largestStaticIndex;
// Has the user allowed access to built-in vm functions?
private int builtInAccessStatus;
// Is the program currently being read in the middle of a /* */ comment?
private boolean isSlashStar;
/**
* Constructs a new empty program with the given GUI.
*/
public VMProgram(VMProgramGUI gui) {
super(gui != null);
this.gui = gui;
listeners = new Vector();
staticRange = new Hashtable();
functions = new Hashtable();
if (hasGUI) {
gui.addProgramListener(this);
gui.addErrorListener(this);
}
reset();
}
/**
* Creates a vm program. If the given file is a dir, creates a program composed of the vm
* files in the dir.
* The vm files are scanned twice: in the first scan a symbol table (that maps
* function & label names into addresses) is built. In the second scan, the instructions
* array is built.
* Throws ProgramException if an error occurs while loading the program.
*/
public void loadProgram(String fileName) throws ProgramException {
File file = new File(fileName);
if (!file.exists())
throw new ProgramException("cannot find " + fileName);
File[] files;
if (file.isDirectory()) {
files = file.listFiles(new HackFileFilter(".vm"));
if (files == null || files.length == 0)
throw new ProgramException("No vm files found in " + fileName);
}
else
files = new File[]{file};
if (displayChanges)
gui.showMessage("Loading...");
// First scan
staticRange.clear();
functions.clear();
builtInAccessStatus = BUILTIN_ACCESS_UNDECIDED;
Hashtable symbols = new Hashtable();
nextPC = 0;
for (int i = 0; i < files.length; i++) {
String name = files[i].getName();
String className = name.substring(0, name.indexOf("."));
// put some dummy into static range - just to tell the function
// getAddress in the second pass which classes exist
staticRange.put(className, new Boolean(true));
try {
updateSymbolTable(files[i], symbols, functions);
} catch (ProgramException pe) {
if (displayChanges)
gui.hideMessage();
throw new ProgramException(name + ": " + pe.getMessage());
}
}
boolean addCallBuiltInSysInit = false;
if ((file.isDirectory() || symbols.get("Main.main") != null) &&
symbols.get("Sys.init") == null) {
// If the program is in multiple files or there's a Main.main
// function it is assumed that it should be run by calling Sys.init.
// If no Sys.init is found, add an invisible line with a call
// to Sys.init to start on - the builtin version will be called.
addCallBuiltInSysInit = true;
getAddress("Sys.init"); // confirm calling the built-in Sys.init
++nextPC; // A "call Sys.init 0" line will be added
}
instructions = new VMEmulatorInstruction[nextPC+4];
// Second scan
nextPC = 0;
currentStaticIndex = Definitions.VAR_START_ADDRESS;
for (int i = 0; i < files.length; i++) {
String name = files[i].getName();
String className = name.substring(0, name.indexOf("."));
largestStaticIndex = -1;
int[] range = new int[2];
range[0] = currentStaticIndex;
try {
// functions is not passed as an argument since it is accessed
// through getAddress()
buildProgram(files[i], symbols);
} catch (ProgramException pe) {
if (displayChanges)
gui.hideMessage();
throw new ProgramException(name + ": " + pe.getMessage());
}
currentStaticIndex += largestStaticIndex + 1;
range[1] = currentStaticIndex - 1;
staticRange.put(className, range);
}
instructionsLength = visibleInstructionsLength = nextPC;
if (builtInAccessStatus == BUILTIN_ACCESS_AUTHORIZED) {
// Add some "invisible" code in the end to make everything work
instructionsLength += 4;
if (addCallBuiltInSysInit) {
instructionsLength += 1;
}
short indexInInvisibleCode = 0;
// Add a jump to the end (noone should get here since
// both calls to built-in functions indicate that
// that this is a function-based program and not a script
// a-la proj7, but just to be on the safe side...).
instructions[nextPC] =
new VMEmulatorInstruction(HVMInstructionSet.GOTO_CODE,
(short)instructionsLength,
indexInInvisibleCode);
instructions[nextPC].setStringArg("afterInvisibleCode");
nextPC++;
// Add a small infinite loop for built-in
// methods to call (for example when Sys.halt is
// called it must call a non-built-in infinite loop
// because otherwise the current script would not
// finish running - a problem for the OS tests.
instructions[nextPC] =
new VMEmulatorInstruction(HVMInstructionSet.LABEL_CODE,
(short)-1);
instructions[nextPC].setStringArg("infiniteLoopForBuiltIns");
nextPC++;
infiniteLoopForBuiltInsAddress = nextPC;
instructions[nextPC] =
new VMEmulatorInstruction(HVMInstructionSet.GOTO_CODE,
nextPC, ++indexInInvisibleCode);
instructions[nextPC].setStringArg("infiniteLoopForBuiltIns");
nextPC++;
if (addCallBuiltInSysInit) { // Add a call to the built-in Sys.init
instructions[nextPC] =
new VMEmulatorInstruction(HVMInstructionSet.CALL_CODE,
getAddress("Sys.init"), (short)0,
++indexInInvisibleCode);
instructions[nextPC].setStringArg("Sys.init");
startAddress = nextPC;
nextPC++;
}
// Add the label that the first invisible code line jumps to
instructions[nextPC] =
new VMEmulatorInstruction(HVMInstructionSet.LABEL_CODE,
(short)-1);
instructions[nextPC].setStringArg("afterInvisibleCode");
nextPC++;
}
if (!addCallBuiltInSysInit) {
Short sysInitAddress = (Short)symbols.get("Sys.init");
if (sysInitAddress == null) // Single file, no Sys.init - start at 0
startAddress = 0;
else // Implemented Sys.init - start there
startAddress = sysInitAddress.shortValue();
}
if (displayChanges)
gui.hideMessage();
nextPC = startAddress;
setGUIContents();
notifyProgramListeners(ProgramEvent.LOAD, fileName);
}
// Scans the given file and creates symbols for its functions & label names.
private void updateSymbolTable(File file, Hashtable symbols, Hashtable functions) throws ProgramException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file.getAbsolutePath()));
} catch (FileNotFoundException fnfe) {
throw new ProgramException("file " + file.getName() + " does not exist");
}
String line;
String currentFunction = null;
String label;
int lineNumber = 0;
isSlashStar = false;
try {
while ((line = unCommentLine(reader.readLine())) != null) {
lineNumber++;
if (!line.trim().equals("")) {
if (line.startsWith("function ")) {
StringTokenizer tokenizer = new StringTokenizer(line);
tokenizer.nextToken();
currentFunction = tokenizer.nextToken();
if (symbols.containsKey(currentFunction))
throw new ProgramException("subroutine " + currentFunction +
" already exists");
functions.put(currentFunction, new Short(nextPC));
symbols.put(currentFunction, new Short(nextPC));
}
else if (line.startsWith("label ")) {
StringTokenizer tokenizer = new StringTokenizer(line);
tokenizer.nextToken();
label = currentFunction + "$" + tokenizer.nextToken();
symbols.put(label, new Short((short)(nextPC + 1)));
}
nextPC++;
}
}
reader.close();
} catch (IOException ioe) {
throw new ProgramException("Error while reading from file");
} catch (NoSuchElementException nsee) {
throw new ProgramException("In line " + lineNumber + ": unexpected end of command");
}
if (isSlashStar) {
throw new ProgramException("Unterminated /* comment at end of file");
}
}
// Scans the given file and creates symbols for its functions & label names.
private void buildProgram(File file, Hashtable symbols)
throws ProgramException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file.getAbsolutePath()));
} catch (FileNotFoundException fnfe) {
throw new ProgramException("file does not exist");
}
int lineNumber = 0;
String line;
String label;
String instructionName;
String currentFunction = null;
short indexInFunction = 0;
byte opCode;
short arg0, arg1;
short pc = nextPC;
HVMInstructionSet instructionSet = HVMInstructionSet.getInstance();
isSlashStar = false;
try {
while ((line = unCommentLine(reader.readLine())) != null) {
lineNumber++;
if (!line.trim().equals("")) {
StringTokenizer tokenizer = new StringTokenizer(line);
instructionName = tokenizer.nextToken();
opCode = instructionSet.instructionStringToCode(instructionName);
if (opCode == HVMInstructionSet.UNKNOWN_INSTRUCTION)
throw new ProgramException("in line " + lineNumber +
": unknown instruction - " + instructionName);
switch (opCode) {
case HVMInstructionSet.PUSH_CODE:
String segment = tokenizer.nextToken();
try {
arg0 = translateSegment(segment, instructionSet, file.getName());
} catch (ProgramException pe) {
throw new ProgramException("in line " + lineNumber + pe.getMessage());
}
arg1 = Short.parseShort(tokenizer.nextToken());
if (arg1 < 0)
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
if (arg0 == HVMInstructionSet.STATIC_SEGMENT_CODE && arg1 > largestStaticIndex)
largestStaticIndex = arg1;
instructions[pc] = new VMEmulatorInstruction(opCode, arg0, arg1,
indexInFunction);
break;
case HVMInstructionSet.POP_CODE:
int n = tokenizer.countTokens();
segment = tokenizer.nextToken();
try {
arg0 = translateSegment(segment, instructionSet, file.getName());
} catch (ProgramException pe) {
throw new ProgramException("in line " + lineNumber + pe.getMessage());
}
arg1 = Short.parseShort(tokenizer.nextToken());
if (arg1 < 0)
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
if (arg0 == HVMInstructionSet.STATIC_SEGMENT_CODE && arg1 > largestStaticIndex)
largestStaticIndex = arg1;
instructions[pc] = new VMEmulatorInstruction(opCode, arg0, arg1,
indexInFunction);
break;
case HVMInstructionSet.FUNCTION_CODE:
currentFunction = tokenizer.nextToken();
indexInFunction = 0;
arg0 = Short.parseShort(tokenizer.nextToken());
if (arg0 < 0)
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
instructions[pc] = new VMEmulatorInstruction(opCode, arg0, indexInFunction);
instructions[pc].setStringArg(currentFunction);
break;
case HVMInstructionSet.CALL_CODE:
String functionName = tokenizer.nextToken();
try {
arg0 = getAddress(functionName);
} catch (ProgramException pe) {
throw new ProgramException("in line " +
lineNumber + ": " +
pe.getMessage());
}
arg1 = Short.parseShort(tokenizer.nextToken());
if (arg1 < 0 || ((arg0 < 0 || arg0 > Definitions.ROM_SIZE) && arg0 != BUILTIN_FUNCTION_ADDRESS))
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
instructions[pc] = new VMEmulatorInstruction(opCode, arg0, arg1,
indexInFunction);
instructions[pc].setStringArg(functionName);
break;
case HVMInstructionSet.LABEL_CODE:
label = currentFunction + "$" + tokenizer.nextToken();
instructions[pc] = new VMEmulatorInstruction(opCode, (short)(-1));
instructions[pc].setStringArg(label);
indexInFunction--; // since Label is not a "physical" instruction
break;
case HVMInstructionSet.GOTO_CODE:
label = currentFunction + "$" + tokenizer.nextToken();
Short labelAddress = (Short)symbols.get(label);
if (labelAddress == null)
throw new ProgramException("in line " + lineNumber +
": Unknown label - " + label);
arg0 = labelAddress.shortValue();
if (arg0 < 0 || arg0 > Definitions.ROM_SIZE)
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
instructions[pc] = new VMEmulatorInstruction(opCode, arg0, indexInFunction);
instructions[pc].setStringArg(label);
break;
case HVMInstructionSet.IF_GOTO_CODE:
label = currentFunction + "$" + tokenizer.nextToken();
labelAddress = (Short)symbols.get(label);
if (labelAddress == null)
throw new ProgramException("in line " + lineNumber +
": Unknown label - " + label);
arg0 = labelAddress.shortValue();
if (arg0 < 0 || arg0 > Definitions.ROM_SIZE)
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
instructions[pc] = new VMEmulatorInstruction(opCode, arg0, indexInFunction);
instructions[pc].setStringArg(label);
break;
// All other instructions have either 1 or 0 arguments and require no
// special treatment
default:
if (tokenizer.countTokens() == 0) {
instructions[pc] = new VMEmulatorInstruction(opCode, indexInFunction);
}
else {
arg0 = Short.parseShort(tokenizer.nextToken());
if (arg0 < 0)
throw new ProgramException("in line " + lineNumber +
": Illegal argument - " + line);
instructions[pc] = new VMEmulatorInstruction(opCode, arg0,
indexInFunction);
}
break;
}
// check end of command
if (tokenizer.hasMoreTokens())
throw new ProgramException("in line " + lineNumber +
": Too many arguments - " + line);
pc++;
indexInFunction++;
}
nextPC = pc;
}
reader.close();
} catch (IOException ioe) {
throw new ProgramException("Error while reading from file");
} catch (NumberFormatException nfe) {
throw new ProgramException("Illegal 16-bit value");
} catch (NoSuchElementException nsee) {
throw new ProgramException("In line " + lineNumber + ": unexpected end of command");
}
if (isSlashStar) {
throw new ProgramException("Unterminated /* comment at end of file");
}
}
// Returns the "un-commented" version of the given line.
// Comments can be either with // or /*.
// The field isSlashStar holds the current /* comment state.
private String unCommentLine(String line) {
String result = line;
if (line != null) {
if (isSlashStar) {
int posStarSlash = line.indexOf("*/");
if (posStarSlash >= 0) {
isSlashStar = false;
result = unCommentLine(line.substring(posStarSlash+2));
} else {
result = "";
}
} else {
int posSlashSlash = line.indexOf("//");
int posSlashStar = line.indexOf("/*");
if (posSlashSlash >= 0 &&
(posSlashStar < 0 || posSlashStar > posSlashSlash)) {
result = line.substring(0, posSlashSlash);
} else if (posSlashStar >= 0) {
isSlashStar = true;
result = line.substring(0, posSlashStar) +
unCommentLine(line.substring(posSlashStar+2));
}
}
}
return result;
}
/**
* Returns the static variable address range of the given class name, in the
* form of a 2-elements array {startAddress, endAddress}.
* If unknown class name, returns null.
*/
public int[] getStaticRange(String className) {
return (int[])staticRange.get(className);
}
/**
* Returns the size of the program.
*/
public int getSize() {
return instructionsLength;
}
public short getAddress(String functionName) throws ProgramException {
Short address = (Short)functions.get(functionName);
if (address != null) {
return address.shortValue();
} else {
String className =
functionName.substring(0, functionName.indexOf("."));
if (staticRange.get(className) == null) {
// The class is not implemented by a VM file - search for a
// built-in implementation later. Display a popup to confirm
// this as this is not a feature from the book but a later
// addition.
if (builtInAccessStatus == BUILTIN_ACCESS_UNDECIDED) {
if (hasGUI && gui.confirmBuiltInAccess()) {
builtInAccessStatus = BUILTIN_ACCESS_AUTHORIZED;
} else {
builtInAccessStatus = BUILTIN_ACCESS_DENIED;
}
}
if (builtInAccessStatus == BUILTIN_ACCESS_AUTHORIZED) {
return BUILTIN_FUNCTION_ADDRESS;
}
}
// Either:
// 1.The class is implemented by a VM file and no implementation
// for the function is found - don't override with built-in
// - or -
// 2.The user did not authorize using built-in implementations.
throw new ProgramException(className + ".vm not found " +
"or function " + functionName +
" not found in " + className + ".vm");
}
}
/**
* Returns the next program counter.
*/
public short getPC() {
return nextPC;
}
/**
* Returns the current value of the program counter.
*/
public short getCurrentPC() {
return currentPC;
}
/**
* Returns the previous value of the program counter.
*/
public short getPreviousPC() {
return prevPC;
}
/**
* Sets the program counter with the given address.
*/
public void setPC(short address) {
prevPC = currentPC;
currentPC = nextPC;
nextPC = address;
setGUIPC();
}
/**
* Sets the program counter to a specially created infinite loop in the
* end of the programs for access by built-in functions, de-facto halting
* the program.
* important so that tests and other scripts finish counting
* (since a built-in infinite loop doesn't count as steps).
* also needed because there is no good way to use the stop button to
* stop an infinite loop in a built-in jack class.
* A message containing information may be provided (can be null).
*/
public void setPCToInfiniteLoopForBuiltIns(String message) {
if (hasGUI) {
gui.notify(message);
}
setPC(infiniteLoopForBuiltInsAddress);
}
/**
* Returns the next VMEmulatorInstruction and increments the PC by one.
* The PC will be incremented by more if the next instruction is a label.
*/
public VMEmulatorInstruction getNextInstruction() {
VMEmulatorInstruction result = null;
if (nextPC < instructionsLength) {
result = instructions[nextPC];
prevPC = currentPC;
currentPC = nextPC;
do {
nextPC++;
} while (nextPC < instructionsLength &&
instructions[nextPC].getOpCode() == HVMInstructionSet.LABEL_CODE);
setGUIPC();
}
return result;
}
/**
* Restarts the program from the beginning.
*/
public void restartProgram() {
currentPC = -999;
prevPC = -999;
nextPC = startAddress;
setGUIPC();
}
/**
* Resets the program (erases all commands).
*/
public void reset() {
instructions = new VMEmulatorInstruction[0];
visibleInstructionsLength = instructionsLength = 0;
currentPC = -999;
prevPC = -999;
nextPC = -1;
setGUIContents();
}
/**
* Returns the GUI of the computer part.
*/
public ComputerPartGUI getGUI() {
return gui;
}
/**
* Called when the current program file/directory is changed.
* The event contains the source object, the event type and the program's file/dir (if any).
*/
public void programChanged(ProgramEvent event) {
switch (event.getType()) {
case ProgramEvent.LOAD:
LoadProgramTask task = new LoadProgramTask(event.getProgramFileName());
Thread t = new Thread(task);
t.start();
break;
case ProgramEvent.CLEAR:
reset();
notifyProgramListeners(ProgramEvent.CLEAR, null);
break;
}
}
// Returns the numeric representation of the given string segment.
// Throws an exception if unknown segment.
private byte translateSegment(String segment, HVMInstructionSet instructionSet,
String fileName)
throws ProgramException {
byte code = instructionSet.segmentVMStringToCode(segment);
if (code == HVMInstructionSet.UNKNOWN_SEGMENT)
throw new ProgramException(": Illegal memory segment - "
+ segment);
return code;
}
// Sets the gui's contents (if a gui exists)
private void setGUIContents() {
if (displayChanges) {
gui.setContents(instructions, visibleInstructionsLength);
gui.setCurrentInstruction(nextPC);
}
}
// Sets the GUI's current instruction index
private void setGUIPC() {
if (displayChanges)
gui.setCurrentInstruction(nextPC);
}
// The task that loads a new program into the emulator
class LoadProgramTask implements Runnable {
private String fileName;
public LoadProgramTask(String fileName) {
this.fileName = fileName;
}
public void run() {
clearErrorListeners();
try {
loadProgram(fileName);
} catch (ProgramException pe) {
notifyErrorListeners(pe.getMessage());
}
}
}
public void refreshGUI() {
if (displayChanges) {
gui.setContents(instructions, visibleInstructionsLength);
gui.setCurrentInstruction(nextPC);
}
}
/**
* Registers the given ProgramEventListener as a listener to this GUI.
*/
public void addProgramListener(ProgramEventListener listener) {
listeners.add(listener);
}
/**
* Un-registers the given ProgramEventListener from being a listener to this GUI.
*/
public void removeProgramListener(ProgramEventListener listener) {
listeners.remove(listener);
}
/**
* Notifies all the ProgramEventListeners on a change in the VM's program by creating
* a ProgramEvent (with the new event type and program's file name) and sending it using the
* programChanged function to all the listeners.
*/
protected void notifyProgramListeners(byte eventType, String programFileName) {
ProgramEvent event = new ProgramEvent(this, eventType, programFileName);
for (int i = 0; i < listeners.size(); i++) {
((ProgramEventListener)listeners.elementAt(i)).programChanged(event);
}
}
}