More refactoring, easier assembly testing (#227)

This commit is contained in:
simon987 2020-07-29 21:08:52 -04:00
parent ba78d2fd93
commit ad0124508c
17 changed files with 325 additions and 98 deletions

View File

@ -8,6 +8,7 @@ import net.simon987.mar.server.assembly.Assembler;
import net.simon987.mar.server.assembly.AssemblyResult;
import net.simon987.mar.server.assembly.CPU;
import net.simon987.mar.server.assembly.exception.CancelledException;
import net.simon987.mar.server.event.CpuInitialisationEvent;
import net.simon987.mar.server.event.GameEvent;
import net.simon987.mar.server.event.GameEventListener;
import net.simon987.mar.server.event.UserCreationEvent;
@ -55,13 +56,20 @@ public class UserCreationListener implements GameEventListener {
//Create CPU
try {
cubot.setCpu(new CPU(GameServer.INSTANCE.getConfig(), cubot));
CPU cpu = new CPU(config);
cubot.setCpu(cpu);
cubot.getCpu().setHardwareHost(cubot);
user.setUserCode(config.getString("new_user_code"));
GameEvent initEvent = new CpuInitialisationEvent(cpu, cubot);
GameServer.INSTANCE.getEventDispatcher().dispatch(event);
if (initEvent.isCancelled()) {
throw new CancelledException();
}
//Compile user code
AssemblyResult ar = new Assembler(cubot.getCpu().getInstructionSet(), cubot.getCpu().getRegisterSet(),
GameServer.INSTANCE.getConfig()).parse(user.getUserCode());
AssemblyResult ar = new Assembler(cpu.getInstructionSet(), cpu.getRegisterSet(),
config).parse(user.getUserCode());
cubot.getCpu().getMemory().clear();

View File

@ -22,7 +22,7 @@ public class AssemblyResult {
/**
* A list of labels
*/
HashMap<String, Character> labels = new HashMap<>(20);
public HashMap<String, Character> labels = new HashMap<>(20);
/**
* List of exceptions encountered during the assembly attempt,
* they will be displayed in the editor

View File

@ -98,7 +98,7 @@ public class CPU implements MongoSerializable {
/**
* Creates a new CPU
*/
public CPU(IServerConfiguration config, ControllableUnit unit) throws CancelledException {
public CPU(IServerConfiguration config) throws CancelledException {
instructionSet = new DefaultInstructionSet();
registerSet = new DefaultRegisterSet();
codeSectionOffset = config.getInt("org_offset");
@ -132,12 +132,6 @@ public class CPU implements MongoSerializable {
status = new Status();
memory = new Memory(config.getInt("memory_size"));
GameEvent event = new CpuInitialisationEvent(this, unit);
GameServer.INSTANCE.getEventDispatcher().dispatch(event);
if (event.isCancelled()) {
throw new CancelledException();
}
}
public void reset() {
@ -398,15 +392,20 @@ public class CPU implements MongoSerializable {
public static CPU deserialize(Document obj, ControllableUnit unit) throws CancelledException {
CPU cpu = new CPU(GameServer.INSTANCE.getConfig(), unit);
CPU cpu = new CPU(GameServer.INSTANCE.getConfig());
cpu.codeSectionOffset = obj.getInteger("codeSegmentOffset");
cpu.memory = new Memory((Document) obj.get("memory"));
cpu.registerSet = RegisterSet.deserialize((Document) obj.get("registerSet"));
return cpu;
GameEvent event = new CpuInitialisationEvent(cpu, unit);
GameServer.INSTANCE.getEventDispatcher().dispatch(event);
if (event.isCancelled()) {
throw new CancelledException();
}
return cpu;
}
public InstructionSet getInstructionSet() {
@ -466,4 +465,15 @@ public class CPU implements MongoSerializable {
public void setHardwareHost(HardwareHost hardwareHost) {
this.hardwareHost = hardwareHost;
}
/**
* For testing/debugging, this creates an copy (please be mindful of the memory usage)
*/
public CpuState getState() {
return new CpuState(
registerSet.clone(),
memory.clone(),
status.clone()
);
}
}

View File

@ -0,0 +1,16 @@
package net.simon987.mar.server.assembly;
public class CpuState {
public RegisterSet registers;
public Memory memory;
public Status status;
public CpuState(RegisterSet registers, Memory memory, Status status) {
this.registers = registers;
this.memory = memory;
this.status = status;
}
}

View File

@ -9,14 +9,13 @@ class DefaultRegisterSet extends RegisterSet {
DefaultRegisterSet() {
super();
addRegister(1, new Register("A"));
addRegister(2, new Register("B"));
addRegister(3, new Register("C"));
addRegister(4, new Register("D"));
addRegister(5, new Register("X"));
addRegister(6, new Register("Y"));
addRegister(7, new Register("SP"));
addRegister(8, new Register("BP"));
put(1, new Register("A"));
put(2, new Register("B"));
put(3, new Register("C"));
put(4, new Register("D"));
put(5, new Register("X"));
put(6, new Register("Y"));
put(7, new Register("SP"));
put(8, new Register("BP"));
}
}

View File

@ -21,7 +21,7 @@ import java.util.zip.InflaterOutputStream;
/**
* Represents the available memory for a CPU in the game universe
*/
public class Memory implements Target, MongoSerializable {
public class Memory implements Target, MongoSerializable, Cloneable {
/**
@ -105,6 +105,7 @@ public class Memory implements Target, MongoSerializable {
address = (char) address;
if (address >= words.length) {
Thread.dumpStack();
LogManager.LOGGER.info("DEBUG: Trying to set memory out of bounds: " + address);
return;
}
@ -117,25 +118,25 @@ public class Memory implements Target, MongoSerializable {
*
* @param blockSize Block size (in words) in which to randomly flip one bit
*/
public void corrupt(int blockSize) {
Random rand = new Random();
public void corrupt(int blockSize) {
Random rand = new Random();
// Increment offset by blockSize
for (int offset = 0; offset < words.length; offset += blockSize) {
// Increment offset by blockSize
for (int offset = 0; offset < words.length; offset += blockSize) {
// Calculate address to corrupt by adding a random value between 0 to (blocksize-1) to offset
int address = rand.nextInt(blockSize) + offset;
// Calculate address to corrupt by adding a random value between 0 to (blocksize-1) to offset
int address = rand.nextInt(blockSize) + offset;
// Checking here avoids having a protected area at the end of the address space
if(address < words.length) {
// Checking here avoids having a protected area at the end of the address space
if (address < words.length) {
// Calculate bitmask by left-shifting 1 by a random value between 0 and 15
int bitmask = 1 << rand.nextInt(16);
// Calculate bitmask by left-shifting 1 by a random value between 0 and 15
int bitmask = 1 << rand.nextInt(16);
// Flip the bit with XOR
words[address] ^= bitmask;
}
}
// Flip the bit with XOR
words[address] ^= bitmask;
}
}
}
/**
@ -186,4 +187,12 @@ public class Memory implements Target, MongoSerializable {
public char[] getWords() {
return words;
}
@Override
public Memory clone() {
Memory memory = new Memory(words.length);
memory.words = new char[words.length];
System.arraycopy(memory.words, 0, words, 0, words.length);
return memory;
}
}

View File

@ -3,7 +3,7 @@ package net.simon987.mar.server.assembly;
/**
* Represents a register in a cpu
*/
public class Register {
public class Register implements Cloneable {
/**
* Name of the register
@ -47,4 +47,14 @@ public class Register {
return name + "=" + value;
}
@Override
public Register clone() {
try {
return (Register) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -2,29 +2,23 @@ package net.simon987.mar.server.assembly;
import net.simon987.mar.server.io.MongoSerializable;
import net.simon987.mar.server.logging.LogManager;
import org.bson.Document;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* A set of registers for a CPU
*/
public class RegisterSet implements Target, MongoSerializable {
public class RegisterSet implements Target, MongoSerializable, Cloneable {
/**
* List of registers
*/
private final HashMap<Integer, Register> registers = new HashMap<>(8);
// TODO configurable number of registers
private static final int REG_COUNT = 8 + 1;
private final Register[] registers = new Register[REG_COUNT];
/**
* Create an empty Register set
*/
public RegisterSet() {
}
@ -40,9 +34,8 @@ public class RegisterSet implements Target, MongoSerializable {
name = name.toUpperCase();
for (Integer i : registers.keySet()) {
if (registers.get(i).getName().equals(name)) {
for (int i = 1; i < REG_COUNT; i++) {
if (registers[i].getName().equals(name)) {
return i;
}
}
@ -56,7 +49,7 @@ public class RegisterSet implements Target, MongoSerializable {
* @param index index of the register
*/
public Register getRegister(int index) {
return registers.get(index);
return registers[index];
}
/**
@ -68,7 +61,8 @@ public class RegisterSet implements Target, MongoSerializable {
name = name.toUpperCase();
for (Register r : registers.values()) {
for (Register r : registers) {
if (r == null) continue;
if (r.getName().equals(name)) {
return r;
}
@ -86,15 +80,11 @@ public class RegisterSet implements Target, MongoSerializable {
*/
@Override
public int get(int address) {
Register register = registers.get(address);
if (register != null) {
return register.getValue();
} else {
if (address <= 0 || address >= REG_COUNT) {
return 0;
}
return registers[address].getValue();
}
/**
@ -105,23 +95,17 @@ public class RegisterSet implements Target, MongoSerializable {
*/
@Override
public void set(int address, int value) {
Register register = registers.get(address);
if (register != null) {
register.setValue(value);
} else {
LogManager.LOGGER.info("DEBUG: trying to set unknown reg index : " + address);
if (address <= 0 || address >= REG_COUNT) {
return;
}
}
public void put(int index, Register register) {
registers.put(index, register);
registers[address].setValue(value);
}
public void clear() {
for (Register register : registers.values()) {
register.setValue(0);
for (Register r : registers) {
if (r == null) continue;
r.setValue(0);
}
}
@ -134,24 +118,24 @@ public class RegisterSet implements Target, MongoSerializable {
* @param index Index of the register
* @param reg Register to add
*/
void addRegister(int index, Register reg) {
registers.put(index, reg);
public void put(int index, Register reg) {
registers[index] = reg;
}
int size() {
return registers.size();
return REG_COUNT - 1;
}
@Override
public Document mongoSerialise() {
List<Document> registers = new ArrayList<>();
for (Integer index : this.registers.keySet()) {
for (int i = 1; i < REG_COUNT; i++) {
Document register = new Document();
register.put("index", index);
register.put("name", getRegister(index).getName());
register.put("value", (int) getRegister(index).getValue());
register.put("index", i);
register.put("name", getRegister(i).getName());
register.put("value", (int) getRegister(i).getValue());
registers.add(register);
}
@ -169,11 +153,12 @@ public class RegisterSet implements Target, MongoSerializable {
List registers = (ArrayList) obj.get("registers");
for (Object sRegister : registers) {
if (sRegister == null) continue;
Register register = new Register((String) ((Document) sRegister).get("name"));
register.setValue(((Document) sRegister).getInteger("value"));
registerSet.registers.put(((Document) sRegister).getInteger("index"), register);
registerSet.put(((Document) sRegister).getInteger("index"), register);
}
@ -191,7 +176,7 @@ public class RegisterSet implements Target, MongoSerializable {
Register register = new Register((String) jsonRegister.get("name"));
register.setValue((int) (long) jsonRegister.get("value"));
registerSet.registers.put((int) (long) jsonRegister.get("index"), register);
registerSet.put((int) (long) jsonRegister.get("index"), register);
}
@ -202,10 +187,20 @@ public class RegisterSet implements Target, MongoSerializable {
public String toString() {
String str = "";
for (Integer index : registers.keySet()) {
str += index + " " + registers.get(index).getName() + "=" + Util.toHex(registers.get(index).getValue()) + "\n";
for (int i = 1; i < REG_COUNT; i++) {
str += i + " " + getRegister(i).getName() + "=" + Util.toHex(getRegister(i).getValue()) + "\n";
}
return str;
}
@Override
public RegisterSet clone() {
RegisterSet rs = new RegisterSet();
for (int i = 1; i < REG_COUNT; i++) {
rs.put(i, getRegister(i).clone());
}
return rs;
}
}

View File

@ -3,7 +3,7 @@ package net.simon987.mar.server.assembly;
/**
* Represents the state of the processor
*/
public class Status {
public class Status implements Cloneable {
/**
* Set to true when the result of
@ -148,4 +148,14 @@ public class Status {
setCarryFlag((stat & (1 << 1)) != 0);
setOverflowFlag((stat & 1) != 0);
}
@Override
public Status clone() {
try {
return (Status) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -23,17 +23,12 @@ public class HwiInstruction extends Instruction {
@Override
public Status execute(Target src, int srcIndex, Status status) {
status.setErrorFlag(cpu.getHardwareHost().hardwareInterrupt(src.get(srcIndex), cpu.getStatus()));
return status;
}
@Override
public Status execute(int src, Status status) {
status.setErrorFlag(cpu.getHardwareHost().hardwareInterrupt(src, cpu.getStatus()));
return status;
}
}

View File

@ -12,5 +12,4 @@ public interface HardwareHost {
boolean hardwareInterrupt(int address, Status status);
int hardwareQuery(int address);
}

View File

@ -0,0 +1,53 @@
package net.simon987.mar.server;
import net.simon987.mar.server.assembly.CPU;
import net.simon987.mar.server.assembly.CpuState;
import net.simon987.mar.server.assembly.HardwareModule;
import net.simon987.mar.server.assembly.Status;
import net.simon987.mar.server.game.objects.HardwareHost;
import java.util.ArrayList;
import java.util.List;
public class FakeHardwareHost implements HardwareHost {
public final List<HwiCall> callHistory = new ArrayList<>();
private final CPU cpu;
public FakeHardwareHost(CPU cpu) {
this.cpu = cpu;
}
@Override
public void attachHardware(HardwareModule hardware, int address) {
// noop
}
@Override
public void detachHardware(int address) {
// noop
}
@Override
public boolean hardwareInterrupt(int address, Status status) {
callHistory.add(new HwiCall(address, cpu.getState()));
return true;
}
@Override
public int hardwareQuery(int address) {
return -1;
}
public static class HwiCall {
public HwiCall(int address, CpuState state) {
this.address = address;
this.state = state;
}
public int address;
public CpuState state;
}
}

View File

@ -0,0 +1,35 @@
package net.simon987.mar.server;
import net.simon987.mar.server.assembly.AssemblyResult;
import net.simon987.mar.server.assembly.CpuState;
import java.util.List;
public class TestExecutionResult {
public CpuState state;
public List<FakeHardwareHost.HwiCall> hwiHistory;
private AssemblyResult ar;
public TestExecutionResult(CpuState state, List<FakeHardwareHost.HwiCall> hwiHistory, AssemblyResult ar) {
this.state = state;
this.hwiHistory = hwiHistory;
this.ar = ar;
}
public int regValue(String register) {
return state.registers.getRegister(register).getValue();
}
public int labelOffset(String label) {
return ar.labels.get(label);
}
public int memValue(int offset) {
return state.memory.get(offset);
}
// Add more utility methods here
}

View File

@ -74,5 +74,4 @@ public class RegisterSetTest {
//Test unknown indexes
registerSet.set(3, 10);
}
}

View File

@ -1,18 +1,57 @@
package net.simon987.mar.server.assembly;
import net.simon987.mar.server.FakeConfiguration;
import net.simon987.mar.server.FakeHardwareHost;
import net.simon987.mar.server.IServerConfiguration;
import net.simon987.mar.server.TestExecutionResult;
import net.simon987.mar.server.assembly.exception.CancelledException;
class TestHelper {
public class TestHelper {
static Assembler getTestAsm() {
private static final int TIMEOUT = 100;
IServerConfiguration configuration = new FakeConfiguration();
configuration.setInt("memory_size", 1000);
configuration.setInt("org_offset", 400);
return new Assembler(new DefaultInstructionSet(), new DefaultRegisterSet(), configuration);
public static Assembler getTestAsm() {
IServerConfiguration config = getTestConfig();
CPU cpu = getTestCpu();
return new Assembler(cpu.getInstructionSet(), cpu.getRegisterSet(), config);
}
public static IServerConfiguration getTestConfig() {
IServerConfiguration configuration = new FakeConfiguration();
configuration.setInt("memory_size", 65536);
configuration.setInt("org_offset", 400);
return configuration;
}
public static CPU getTestCpu() {
CPU cpu = null;
try {
cpu = new CPU(getTestConfig());
} catch (CancelledException e) {
e.printStackTrace();
}
return cpu;
}
public static TestExecutionResult executeCode(String code) {
AssemblyResult ar = getTestAsm().parse(code);
CPU cpu = TestHelper.getTestCpu();
FakeHardwareHost host = new FakeHardwareHost(cpu);
cpu.setHardwareHost(host);
cpu.getMemory().clear();
char[] assembledCode = ar.getWords();
cpu.getMemory().write((char) ar.origin, assembledCode, 0, assembledCode.length);
cpu.setCodeSectionOffset(ar.getCodeSectionOffset());
cpu.reset();
cpu.execute(TIMEOUT);
return new TestExecutionResult(cpu.getState(), host.callHistory, ar);
}
}

View File

@ -1,6 +1,30 @@
package net.simon987.mar.server.assembly.instruction;
import net.simon987.mar.server.FakeConfiguration;
import net.simon987.mar.server.IServerConfiguration;
import net.simon987.mar.server.TestExecutionResult;
import net.simon987.mar.server.assembly.*;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CallInstructionTest {
@Test
public void callSimple1() {
String code = "" +
"my_routine: \n" +
" MOV X, 0x1234 \n" +
" RET \n" +
".text \n" +
"CALL my_routine \n" +
"MOV Y, 0x4567 \n" +
"brk \n";
TestExecutionResult res = TestHelper.executeCode(code);
assertEquals(0x1234, res.regValue("X"));
assertEquals(0x4567, res.regValue("Y"));
}
}

View File

@ -0,0 +1,26 @@
package net.simon987.mar.server.assembly.instruction;
import net.simon987.mar.server.FakeHardwareHost;
import net.simon987.mar.server.TestExecutionResult;
import net.simon987.mar.server.assembly.TestHelper;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class HwiInstructionTest {
@Test
public void hwiSimple1() {
String code = "" +
"MOV A, 0x123 \n" +
"HWI 0x4567 \n" +
"MOV A, 0x789 \n" +
"brk \n";
TestExecutionResult res = TestHelper.executeCode(code);
FakeHardwareHost.HwiCall call = res.hwiHistory.get(0);
assertEquals(0x4567, call.address);
assertEquals(0x123, call.state.registers.getRegister("A").getValue());
}
}