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];