diff --git a/Java/src/main/java/org/ciyam/at/FunctionCode.java b/Java/src/main/java/org/ciyam/at/FunctionCode.java
index 3a139ef..dfb9a9e 100644
--- a/Java/src/main/java/org/ciyam/at/FunctionCode.java
+++ b/Java/src/main/java/org/ciyam/at/FunctionCode.java
@@ -653,8 +653,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* 0x0140
* B = B + A
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256.
+ *
+ * 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)));
@@ -665,8 +667,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* 0x0141
* A = A + B
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256.
+ *
+ * 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)));
@@ -677,8 +681,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* 0x0142
* B = B - A
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256.
+ *
+ * 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)));
@@ -689,8 +695,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* 0x0143
* A = A - B
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256.
+ *
+ * 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)));
@@ -702,8 +710,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* B = A * B
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256,
* i.e. only the low 256 bits of the product are kept.
+ *
+ * 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)));
@@ -715,8 +725,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* A = A * B
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Result wraps modulo 2256,
* i.e. only the low 256 bits of the product are kept.
+ *
+ * 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)));
@@ -728,8 +740,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* B = A / B
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Division is unsigned and truncating.
* Can also throw IllegalOperationException if divide-by-zero attempted.
+ *
+ * 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);
@@ -746,8 +760,10 @@ protected void postCheckExecute(FunctionData functionData, MachineState state, s
* A = B / A
* A1..A4 and B1..B4 are treated as 256-bit unsigned integers, with A1/B1 least significant. Division is unsigned and truncating.
* Can also throw IllegalOperationException if divide-by-zero attempted.
+ *
+ * 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);
@@ -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;
@@ -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 value is within the platform-specific function code range dispatched to the API. */
@@ -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);
diff --git a/Java/src/main/java/org/ciyam/at/MachineState.java b/Java/src/main/java/org/ciyam/at/MachineState.java
index c826f7b..7fb48bd 100644
--- a/Java/src/main/java/org/ciyam/at/MachineState.java
+++ b/Java/src/main/java/org/ciyam/at/MachineState.java
@@ -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
diff --git a/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java b/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java
index 66c3f78..58042ec 100644
--- a/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java
+++ b/Java/src/test/java/org/ciyam/at/ABArithmeticFunctionCodeTests.java
@@ -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).
*
* A1..A4 / B1..B4 are treated as 256-bit unsigned integers, least significant register first. + *
+ * 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 { @@ -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 @@ -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 diff --git a/Java/src/test/java/org/ciyam/at/SerializationTests.java b/Java/src/test/java/org/ciyam/at/SerializationTests.java index 7aef33c..fd3291e 100644 --- a/Java/src/test/java/org/ciyam/at/SerializationTests.java +++ b/Java/src/test/java/org/ciyam/at/SerializationTests.java @@ -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(); diff --git a/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java b/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java index c1cba48..ea5b16a 100644 --- a/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java +++ b/Java/src/test/java/org/ciyam/at/test/ExecutableTest.java @@ -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() { @@ -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(); diff --git a/Java/src/test/java/org/ciyam/at/test/TestUtils.java b/Java/src/test/java/org/ciyam/at/test/TestUtils.java index cb093f8..d63cee8 100644 --- a/Java/src/test/java/org/ciyam/at/test/TestUtils.java +++ b/Java/src/test/java/org/ciyam/at/test/TestUtils.java @@ -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];