Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions Java/src/main/java/org/ciyam/at/FunctionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* <code>0x0140</code><br>
* B = B + A<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2<sup>256</sup>.
* <p>
* Requires AT creation version 3 or later.
*/
ADD_A_TO_B(0x0140, 0, false) {
ADD_A_TO_B(0x0140, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
storeUnsigned256IntoB(state, unsigned256FromB(state).add(unsigned256FromA(state)));
Expand All @@ -665,8 +667,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* <code>0x0141</code><br>
* A = A + B<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2<sup>256</sup>.
* <p>
* Requires AT creation version 3 or later.
*/
ADD_B_TO_A(0x0141, 0, false) {
ADD_B_TO_A(0x0141, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
storeUnsigned256IntoA(state, unsigned256FromA(state).add(unsigned256FromB(state)));
Expand All @@ -677,8 +681,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* <code>0x0142</code><br>
* B = B - A<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2<sup>256</sup>.
* <p>
* Requires AT creation version 3 or later.
*/
SUB_A_FROM_B(0x0142, 0, false) {
SUB_A_FROM_B(0x0142, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
storeUnsigned256IntoB(state, unsigned256FromB(state).subtract(unsigned256FromA(state)));
Expand All @@ -689,8 +695,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* <code>0x0143</code><br>
* A = A - B<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2<sup>256</sup>.
* <p>
* Requires AT creation version 3 or later.
*/
SUB_B_FROM_A(0x0143, 0, false) {
SUB_B_FROM_A(0x0143, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
storeUnsigned256IntoA(state, unsigned256FromA(state).subtract(unsigned256FromB(state)));
Expand All @@ -702,8 +710,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* B = A * B<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2<sup>256</sup>,
* i.e. only the low 256 bits of the product are kept.
* <p>
* Requires AT creation version 3 or later.
*/
MUL_A_BY_B(0x0144, 0, false) {
MUL_A_BY_B(0x0144, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
storeUnsigned256IntoB(state, unsigned256FromA(state).multiply(unsigned256FromB(state)));
Expand All @@ -715,8 +725,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* A = A * B<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2<sup>256</sup>,
* i.e. only the low 256 bits of the product are kept.
* <p>
* Requires AT creation version 3 or later.
*/
MUL_B_BY_A(0x0145, 0, false) {
MUL_B_BY_A(0x0145, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
storeUnsigned256IntoA(state, unsigned256FromA(state).multiply(unsigned256FromB(state)));
Expand All @@ -728,8 +740,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* B = A / B<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Division is unsigned and truncating.<br>
* Can also throw <code>IllegalOperationException</code> if divide-by-zero attempted.
* <p>
* Requires AT creation version 3 or later.
*/
DIV_A_BY_B(0x0146, 0, false) {
DIV_A_BY_B(0x0146, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
BigInteger divisor = unsigned256FromB(state);
Expand All @@ -746,8 +760,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* A = B / A<br>
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Division is unsigned and truncating.<br>
* Can also throw <code>IllegalOperationException</code> if divide-by-zero attempted.
* <p>
* Requires AT creation version 3 or later.
*/
DIV_B_BY_A(0x0147, 0, false) {
DIV_B_BY_A(0x0147, 0, false, 3) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
BigInteger divisor = unsigned256FromA(state);
Expand Down Expand Up @@ -1264,6 +1280,8 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
public final short value;
public final int paramCount;
public final boolean returnsValue;
/** Minimum AT creation version required for this function code to be available - see {@link #execute(FunctionData, MachineState, short)}. */
public final short minVersion;

/** First function code (inclusive) of the platform-specific range dispatched to the API - see {@link #API_PASSTHROUGH}. */
public static final int PLATFORM_CODE_START = 0x0500;
Expand All @@ -1274,9 +1292,14 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));

private FunctionCode(int value, int paramCount, boolean returnsValue) {
this(value, paramCount, returnsValue, 1);
}

private FunctionCode(int value, int paramCount, boolean returnsValue, int minVersion) {
this.value = (short) value;
this.paramCount = paramCount;
this.returnsValue = returnsValue;
this.minVersion = (short) minVersion;
}

/** Returns whether <code>value</code> is within the platform-specific function code range dispatched to the API. */
Expand Down Expand Up @@ -1330,6 +1353,12 @@ public void preExecuteCheck(int paramCount, boolean returnValueExpected) throws
* @throws ExecutionException
*/
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Function codes introduced after this AT's creation version must behave as if they don't exist,
// i.e. throw IllegalFunctionCodeException just like an unknown function code would.
if (state.version < this.minVersion)
throw new IllegalFunctionCodeException(String.format("Unknown function code 0x%04x for AT version %d (function requires version %d+)",
this.value, state.version, this.minVersion));

// Check passed functionData against requirements of this function
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected);

Expand Down
2 changes: 2 additions & 0 deletions Java/src/main/java/org/ciyam/at/MachineState.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public VersionedConstants(int codePageSize, int dataPageSize, int callStackPageS
static {
VERSIONED_CONSTANTS.put((short) 1, new VersionedConstants(256, 256, 256, 256));
VERSIONED_CONSTANTS.put((short) 2, new VersionedConstants(OPCODE_SIZE, VALUE_SIZE, ADDRESS_SIZE, VALUE_SIZE));
// Version 3 uses version 2's page-size semantics; the only difference is function code availability - see FunctionCode.minVersion
VERSIONED_CONSTANTS.put((short) 3, new VersionedConstants(OPCODE_SIZE, VALUE_SIZE, ADDRESS_SIZE, VALUE_SIZE));
}

// Set during construction
Expand Down
66 changes: 66 additions & 0 deletions Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
import static org.junit.Assert.*;

import org.ciyam.at.test.ExecutableTest;
import org.ciyam.at.test.TestUtils;
import org.junit.Before;
import org.junit.Test;

/**
* Tests for the 256-bit A/B arithmetic function codes (0x0140 - 0x0147).
* <p>
* A1..A4 / B1..B4 are treated as 256-bit unsigned integers, least significant register first.
* <p>
* These function codes require AT creation version 3, so tests here run under a version 3 header,
* apart from the explicit version 2 fault tests.
*/
public class ABArithmeticFunctionCodeTests extends ExecutableTest {

Expand All @@ -20,6 +25,11 @@ public class ABArithmeticFunctionCodeTests extends ExecutableTest {
private static final long ALL_ONES = 0xffffffffffffffffL;
private static final long TOP_BIT = 0x8000000000000000L;

@Before
public void useVersion3Header() {
headerBytes = TestUtils.V3_HEADER_BYTES;
}

@Test
public void testAddAToB() throws ExecutionException {
// B = B + A
Expand Down Expand Up @@ -208,6 +218,62 @@ public void testDivBByZeroAIsFatalError() throws ExecutionException {
assertTrue(state.hadFatalError());
}

@Test
public void testAddAToBIsFatalErrorForVersion2() throws ExecutionException {
// Version 2 ATs must treat 0x0140 like an unknown function code
headerBytes = TestUtils.HEADER_BYTES;

executeFunction(FunctionCode.ADD_A_TO_B,
new long[] { 3L, 0L, 0L, 0L },
new long[] { 5L, 0L, 0L, 0L });

assertTrue(state.isFinished());
assertTrue(state.hadFatalError());

// Execution must have faulted before reaching GET_A_DAT / GET_B_DAT, so result addresses remain zero
assertEquals(0L, getData(A_RESULT_ADDRESS));
assertEquals(0L, getData(B_RESULT_ADDRESS));
}

@Test
public void testAddBToAIsFatalErrorForVersion2() throws ExecutionException {
// Version 2 ATs must treat 0x0141 like an unknown function code
headerBytes = TestUtils.HEADER_BYTES;

executeFunction(FunctionCode.ADD_B_TO_A,
new long[] { 3L, 0L, 0L, 0L },
new long[] { 5L, 0L, 0L, 0L });

assertTrue(state.isFinished());
assertTrue(state.hadFatalError());

// Execution must have faulted before reaching GET_A_DAT / GET_B_DAT, so result addresses remain zero
assertEquals(0L, getData(A_RESULT_ADDRESS));
assertEquals(0L, getData(B_RESULT_ADDRESS));
}

@Test
public void testAllArithmeticFunctionCodesAreFatalErrorForVersion2() throws ExecutionException {
FunctionCode[] functionCodes = new FunctionCode[] {
FunctionCode.ADD_A_TO_B, FunctionCode.ADD_B_TO_A,
FunctionCode.SUB_A_FROM_B, FunctionCode.SUB_B_FROM_A,
FunctionCode.MUL_A_BY_B, FunctionCode.MUL_B_BY_A,
FunctionCode.DIV_A_BY_B, FunctionCode.DIV_B_BY_A
};

for (FunctionCode functionCode : functionCodes) {
// Fresh buffers/API for each function code
beforeTest();
headerBytes = TestUtils.HEADER_BYTES;

executeFunction(functionCode,
new long[] { 3L, 0L, 0L, 0L },
new long[] { 5L, 0L, 0L, 0L });

assertTrue(functionCode.name() + " should be fatal error under version 2", state.hadFatalError());
}
}

/** Loads A and B registers with passed values, executes passed function, then saves A and B back into the data segment. */
private void executeFunction(FunctionCode functionCode, long[] aValues, long[] bValues) {
// A register source values
Expand Down
23 changes: 23 additions & 0 deletions Java/src/test/java/org/ciyam/at/SerializationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@ public void testCreationBytes() {
restoredState = MachineState.fromBytes(api, loggerFactory, packedState, codeBytes);
}

/** Test serialization to/from creation bytes for version 3 ATs. */
@Test
public void testVersion3CreationBytes() {
byte[] headerBytes = TestUtils.V3_HEADER_BYTES;
byte[] codeBytes = codeByteBuffer.array();
byte[] dataBytes = dataByteBuffer.array();

state = new MachineState(api, loggerFactory, headerBytes, codeBytes, dataBytes);
assertEquals(TestUtils.V3_VERSION, state.version);
packedState = state.toBytes();

byte[] creationBytes = MachineState.toCreationBytes(TestUtils.V3_VERSION, codeBytes, dataBytes, TestUtils.NUM_CALL_STACK_PAGES, TestUtils.NUM_USER_STACK_PAGES, TestUtils.MIN_ACTIVATION_AMOUNT);

MachineState restoredState = new MachineState(api, loggerFactory, creationBytes);
assertEquals(TestUtils.V3_VERSION, restoredState.version);
byte[] packedRestoredSate = restoredState.toBytes();

assertTrue(Arrays.equals(packedState, packedRestoredSate));

restoredState = MachineState.fromBytes(api, loggerFactory, packedState, codeBytes);
assertEquals(TestUtils.V3_VERSION, restoredState.version);
}

private byte[] simulate() {
byte[] headerBytes = TestUtils.HEADER_BYTES;
byte[] codeBytes = codeByteBuffer.array();
Expand Down
3 changes: 2 additions & 1 deletion Java/src/test/java/org/ciyam/at/test/ExecutableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ExecutableTest {
public int userStackSize;
public byte[] packedState;
public byte[] codeBytes;
/** Header bytes used when deploying - tests can override to use a different AT version */
public byte[] headerBytes = TestUtils.HEADER_BYTES;

@BeforeClass
public static void beforeClass() {
Expand Down Expand Up @@ -56,7 +58,6 @@ public void execute(boolean onceOnly) {
if (packedState == null) {
// First time
System.out.println("First execution - deploying...");
byte[] headerBytes = TestUtils.HEADER_BYTES;
codeBytes = codeByteBuffer.array();
byte[] dataBytes = dataByteBuffer.array();

Expand Down
2 changes: 2 additions & 0 deletions Java/src/test/java/org/ciyam/at/test/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
public class TestUtils {

public static final short VERSION = 2;
public static final short V3_VERSION = 3;
public static final short NUM_CODE_PAGES = 0x0200;
public static final short NUM_DATA_PAGES = 0x0200;
public static final short NUM_CALL_STACK_PAGES = 0x0010;
public static final short NUM_USER_STACK_PAGES = 0x0010;
public static final long MIN_ACTIVATION_AMOUNT = 0L;
public static final byte[] HEADER_BYTES = toHeaderBytes(VERSION, NUM_CODE_PAGES, NUM_DATA_PAGES, NUM_CALL_STACK_PAGES, NUM_USER_STACK_PAGES, MIN_ACTIVATION_AMOUNT);
public static final byte[] V3_HEADER_BYTES = toHeaderBytes(V3_VERSION, NUM_CODE_PAGES, NUM_DATA_PAGES, NUM_CALL_STACK_PAGES, NUM_USER_STACK_PAGES, MIN_ACTIVATION_AMOUNT);

public static byte[] hexToBytes(String hex) {
byte[] output = new byte[hex.length() / 2];
Expand Down
Loading