diff --git a/tests/assembly_params.json b/tests/assembly_params.json new file mode 100644 index 00000000..06047197 --- /dev/null +++ b/tests/assembly_params.json @@ -0,0 +1,175 @@ +{ + "_comment": "Synthetic MSP430 instruction costs. Every value is derived from the cost model below and chosen to produce the outcomes the tests assert on, so treat them as fixed inputs rather than as an estimate of any real device.", + "_cost_model": "cost = 1.5 * units. A register-only instruction is 1 unit. An immediate source adds 1, an indirect/autoincrement source adds 1, and an indexed/symbolic/absolute source adds 2. A memory destination adds the same, plus 1 more when the instruction reads it back (everything except mov/cmp/bit). Jumps are 2 units. Outside that model: push/pop/br are 3, 2 and 3 units; nop/dint/clrc/ret/reti are 1 unit; adjcallstackdown/adjcallstackup are 0 because they expand to no code when every argument is register-passed, which holds for the snippets in tests/. Library calls are listed individually, scaled so __mspabi_divi and the other division helpers exceed any E_safe a test config can produce and a block containing one cannot fit a single charge.", + "_adding_a_key": "Derive the value from the model above. A key absent here costs defaultEnergy_ (1.0) and only warns, so tests/test_rockclimb.py asserts the pass reports zero missing keys.", + "ignored_call_targets": [ + "_timing_delay_cycles", + "bench_commit_done", + "bench_halt", + "debug_exit", + "debug_exit_commit", + "debug_init", + "timing_gpio_init", + "timing_gpio_start", + "timing_gpio_stop" + ], + "parameters": { + "adc_indexed": 6.0, + "add_absolute_register": 4.5, + "add_autoincrement_register": 3.0, + "add_immediate_indexed": 7.5, + "add_immediate_register": 3.0, + "add_indexed_register": 4.5, + "add_indirect_register": 3.0, + "add_register_absolute": 6.0, + "add_register_indexed": 6.0, + "add_register_register": 1.5, + "addc_immediate_register": 3.0, + "addc_register_register": 1.5, + "adjcallstackdown": 0.0, + "adjcallstackup": 0.0, + "and_absolute_register": 4.5, + "and_immediate_absolute": 7.5, + "and_immediate_register": 3.0, + "and_indexed_register": 4.5, + "and_register_register": 1.5, + "and_symbolic_absolute": 9.0, + "and_symbolic_register": 4.5, + "bic_immediate_absolute": 7.5, + "bic_immediate_register": 3.0, + "bic_register_register": 1.5, + "bic_symbolic_register": 4.5, + "bis_immediate_absolute": 7.5, + "bis_immediate_register": 3.0, + "bis_indexed_indexed": 9.0, + "bis_indexed_register": 4.5, + "bis_register_indexed": 6.0, + "bis_register_register": 1.5, + "bis_symbolic_absolute": 9.0, + "bis_symbolic_register": 4.5, + "bit_immediate_absolute": 6.0, + "bit_immediate_indexed": 6.0, + "bit_immediate_register": 3.0, + "bit_symbolic_absolute": 7.5, + "bit_symbolic_indexed": 7.5, + "bit_symbolic_register": 4.5, + "br_immediate": 4.5, + "br_indexed": 4.5, + "call___mspabi_addd": 3000.0, + "call___mspabi_addf": 600.0, + "call___mspabi_cvtdf": 1200.0, + "call___mspabi_cvtfd": 300.0, + "call___mspabi_divd": 6000.0, + "call___mspabi_divf": 750.0, + "call___mspabi_divi": 750.0, + "call___mspabi_divli": 3000.0, + "call___mspabi_divu": 750.0, + "call___mspabi_fixfli": 150.0, + "call___mspabi_fltuld": 300.0, + "call___mspabi_fltulf": 300.0, + "call___mspabi_mpyd": 12000.0, + "call___mspabi_mpyf": 1500.0, + "call___mspabi_mpyi": 30.0, + "call___mspabi_mpyl": 45.0, + "call___mspabi_mpyll": 45.0, + "call___mspabi_remu": 750.0, + "call___mspabi_remul": 3000.0, + "call___mspabi_subd": 3000.0, + "call_cos": 600000.0, + "call_immediate": 18.0, + "call_memcpy": 18.0, + "call_memcpy_bytes": 12.0, + "call_memset": 18.0, + "call_memset_bytes": 12.0, + "call_sin": 600000.0, + "clr_register": 1.5, + "clrc": 1.5, + "cmp_absolute_indexed": 7.5, + "cmp_immediate_absolute": 6.0, + "cmp_immediate_indexed": 6.0, + "cmp_immediate_register": 3.0, + "cmp_indexed_indexed": 7.5, + "cmp_indexed_register": 4.5, + "cmp_register_indexed": 4.5, + "cmp_register_register": 1.5, + "cmp_symbolic_indexed": 7.5, + "cmp_symbolic_register": 4.5, + "dint": 1.5, + "inc_indexed": 6.0, + "inc_register": 1.5, + "inc_symbolic": 6.0, + "incd_register": 1.5, + "incd_symbolic": 6.0, + "inv_register": 1.5, + "jc_symbolic": 3.0, + "jcc_symbolic": 3.0, + "jge_symbolic": 3.0, + "jl_symbolic": 3.0, + "jmp_symbolic": 3.0, + "jnc_symbolic": 3.0, + "jnz_symbolic": 3.0, + "jz_symbolic": 3.0, + "mov_RESHI_register": 1.5, + "mov_RESLO_register": 1.5, + "mov_absolute_absolute": 7.5, + "mov_absolute_indexed": 7.5, + "mov_absolute_register": 4.5, + "mov_autoincrement_indexed": 6.0, + "mov_autoincrement_register": 3.0, + "mov_immediate_absolute": 6.0, + "mov_immediate_indexed": 6.0, + "mov_immediate_register": 3.0, + "mov_immediate_symbolic": 6.0, + "mov_indexed_absolute": 7.5, + "mov_indexed_indexed": 7.5, + "mov_indexed_register": 4.5, + "mov_indirect_absolute": 6.0, + "mov_indirect_indexed": 6.0, + "mov_indirect_register": 3.0, + "mov_register_MPY": 1.5, + "mov_register_OP2": 1.5, + "mov_register_absolute": 4.5, + "mov_register_indexed": 4.5, + "mov_register_register": 1.5, + "mov_symbolic_absolute": 7.5, + "mov_symbolic_indexed": 7.5, + "mov_symbolic_register": 4.5, + "nop": 1.5, + "pop_register": 3.0, + "push_register": 4.5, + "ret": 1.5, + "reti": 1.5, + "rla_indexed": 6.0, + "rla_register": 1.5, + "rla_symbolic": 6.0, + "rlc_register": 1.5, + "rra_register": 1.5, + "rra_symbolic": 6.0, + "rrc_indexed": 6.0, + "rrc_register": 1.5, + "rrc_symbolic": 6.0, + "sub_absolute_register": 4.5, + "sub_immediate_register": 3.0, + "sub_indexed_indexed": 9.0, + "sub_indexed_register": 4.5, + "sub_register_indexed": 6.0, + "sub_register_register": 1.5, + "sub_symbolic_register": 4.5, + "subc_register_register": 1.5, + "swpb_register": 1.5, + "sxt_register": 1.5, + "xor_absolute_indexed": 9.0, + "xor_absolute_register": 4.5, + "xor_autoincrement_register": 3.0, + "xor_immediate_indexed": 7.5, + "xor_immediate_register": 3.0, + "xor_indexed_indexed": 9.0, + "xor_indexed_register": 4.5, + "xor_indirect_register": 3.0, + "xor_register_indexed": 6.0, + "xor_register_register": 1.5, + "xor_symbolic_indexed": 9.0, + "xor_symbolic_register": 4.5, + "zext_register": 1.5 + } +} diff --git a/tests/programs/aes.c b/tests/programs/aes.c new file mode 100644 index 00000000..b506f8e7 --- /dev/null +++ b/tests/programs/aes.c @@ -0,0 +1,429 @@ +/* + * AES-128 CBC - Preprocessed benchmark for intermittent computing. + * Based on tiny-AES-c (public domain). + * Simplified: AES128 CBC only, uint32_t instead of size_t for MSP430. + */ +#include "benchmark.h" +#include "loop_tripcount.h" +#include + +#define FORCE_INLINE static inline __attribute__((always_inline)) + +/* --- AES Configuration (AES-128, CBC only) --- */ + +#define Nb 4 +#define Nk 4 +#define Nr 10 +#define AES_BLOCKLEN 16 +#define AES_KEYLEN 16 +#define AES_keyExpSize 176 + +/* VERIFY_BUILD (defined by `ckpt verify`) shrinks the workload: BOR halt + * mode makes the full 16-block workload impractically slow. Four blocks + * still exercise the multi-block CBC chaining paths in both directions. */ +#ifdef VERIFY_BUILD +#define AES_BUFFER_BLOCKS 4 +#else +#define AES_BUFFER_BLOCKS 16 +#endif +#define AES_BUFFER_LEN (AES_BLOCKLEN * AES_BUFFER_BLOCKS) + +/* --- Types --- */ + +typedef uint8_t state_t[4][4]; + +struct AES_ctx { + uint8_t RoundKey[AES_keyExpSize]; + uint8_t Iv[AES_BLOCKLEN]; +}; + +/* --- Mutable globals --- */ + +struct AES_ctx g_ctx; + +static uint8_t g_key[16] __attribute__((used, section(".fram"))) = { + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; + +static uint8_t g_iv[16] __attribute__((used, section(".fram"))) = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + +uint8_t g_buf[AES_BUFFER_LEN] __attribute__((section(".fram"))); + +/* --- Const data (no annotation) --- */ + +static const uint8_t sbox[256] = { + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16}; + +static const uint8_t rsbox[256] = { + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d}; + +static const uint8_t Rcon[11] = {0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36}; + +/* Repeat the standard 4-block CBC test vector to lengthen the workload + * without changing the benchmark structure. */ +#define AES_TEST_VECTOR_64 \ + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, \ + 0x2a, 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, \ + 0x8e, 0x51, 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, \ + 0x0a, 0x52, 0xef, 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, \ + 0xe6, 0x6c, 0x37, 0x10 + +static const uint8_t test_data[AES_BUFFER_LEN] = { + AES_TEST_VECTOR_64, +#ifndef VERIFY_BUILD + AES_TEST_VECTOR_64, + AES_TEST_VECTOR_64, + AES_TEST_VECTOR_64, +#endif +}; + +/* --- Macros --- */ + +#define getSBoxValue(num) (sbox[(num)]) +#define getSBoxInvert(num) (rsbox[(num)]) + +#define Multiply(x, y) \ + (((y & 1) * x) ^ ((y >> 1 & 1) * xtime(x)) ^ ((y >> 2 & 1) * xtime(xtime(x))) ^ \ + ((y >> 3 & 1) * xtime(xtime(xtime(x)))) ^ ((y >> 4 & 1) * xtime(xtime(xtime(xtime(x)))))) + +/* --- Helper Functions --- */ + +FORCE_INLINE uint8_t xtime(uint8_t x) { + return ((x << 1) ^ (((x >> 7) & 1) * 0x1b)); +} + +FORCE_INLINE void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key) { + unsigned i, j, k; + uint8_t tempa[4]; + + /* The first round key is the key itself. */ + for (i = 0; i < Nk; ++i) { + __loop_tripcount(4); + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + } + + /* All other round keys are found from the previous round keys. */ + for (i = Nk; i < Nb * (Nr + 1); ++i) { + __loop_tripcount(40); + { + k = (i - 1) * 4; + tempa[0] = RoundKey[k + 0]; + tempa[1] = RoundKey[k + 1]; + tempa[2] = RoundKey[k + 2]; + tempa[3] = RoundKey[k + 3]; + } + + if (i % Nk == 0) { + /* RotWord() */ + { + const uint8_t u8tmp = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = u8tmp; + } + + /* SubWord() */ + { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + + tempa[0] = tempa[0] ^ Rcon[i / Nk]; + } + + j = i * 4; + k = (i - Nk) * 4; + RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; + RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; + RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; + RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; + } +} + +FORCE_INLINE void AddRoundKey(uint8_t round, state_t *state, const uint8_t *RoundKey) { + uint8_t i, j; + for (i = 0; i < 4; ++i) { + __loop_tripcount(4); + for (j = 0; j < 4; ++j) { + __loop_tripcount(4); + (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; + } + } +} + +FORCE_INLINE void SubBytes(state_t *state) { + uint8_t i, j; + for (i = 0; i < 4; ++i) { + __loop_tripcount(4); + for (j = 0; j < 4; ++j) { + __loop_tripcount(4); + (*state)[j][i] = getSBoxValue((*state)[j][i]); + } + } +} + +FORCE_INLINE void InvSubBytes(state_t *state) { + uint8_t i, j; + for (i = 0; i < 4; ++i) { + __loop_tripcount(4); + for (j = 0; j < 4; ++j) { + __loop_tripcount(4); + (*state)[j][i] = getSBoxInvert((*state)[j][i]); + } + } +} + +FORCE_INLINE void ShiftRows(state_t *state) { + uint8_t temp; + + /* Rotate first row 1 columns to left */ + temp = (*state)[0][1]; + (*state)[0][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[3][1]; + (*state)[3][1] = temp; + + /* Rotate second row 2 columns to left */ + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + /* Rotate third row 3 columns to left */ + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[3][3]; + (*state)[3][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[1][3]; + (*state)[1][3] = temp; +} + +FORCE_INLINE void InvShiftRows(state_t *state) { + uint8_t temp; + + /* Rotate first row 1 columns to right */ + temp = (*state)[3][1]; + (*state)[3][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[0][1]; + (*state)[0][1] = temp; + + /* Rotate second row 2 columns to right */ + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + /* Rotate third row 3 columns to right */ + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[1][3]; + (*state)[1][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[3][3]; + (*state)[3][3] = temp; +} + +FORCE_INLINE void MixColumns(state_t *state) { + uint8_t i; + uint8_t Tmp, Tm, t; + for (i = 0; i < 4; ++i) { + __loop_tripcount(4); + t = (*state)[i][0]; + Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3]; + Tm = (*state)[i][0] ^ (*state)[i][1]; + Tm = xtime(Tm); + (*state)[i][0] ^= Tm ^ Tmp; + Tm = (*state)[i][1] ^ (*state)[i][2]; + Tm = xtime(Tm); + (*state)[i][1] ^= Tm ^ Tmp; + Tm = (*state)[i][2] ^ (*state)[i][3]; + Tm = xtime(Tm); + (*state)[i][2] ^= Tm ^ Tmp; + Tm = (*state)[i][3] ^ t; + Tm = xtime(Tm); + (*state)[i][3] ^= Tm ^ Tmp; + } +} + +FORCE_INLINE void InvMixColumns(state_t *state) { + int i; + uint8_t a, b, c, d; + for (i = 0; i < 4; ++i) { + __loop_tripcount(4); + a = (*state)[i][0]; + b = (*state)[i][1]; + c = (*state)[i][2]; + d = (*state)[i][3]; + + (*state)[i][0] = + Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09); + (*state)[i][1] = + Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d); + (*state)[i][2] = + Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b); + (*state)[i][3] = + Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e); + } +} + +FORCE_INLINE void Cipher(state_t *state, const uint8_t *RoundKey) { + uint8_t round = 0; + + AddRoundKey(0, state, RoundKey); + + for (round = 1;; ++round) { + __loop_tripcount(10); + SubBytes(state); + ShiftRows(state); + if (round == Nr) { + break; + } + MixColumns(state); + AddRoundKey(round, state, RoundKey); + } + AddRoundKey(Nr, state, RoundKey); +} + +FORCE_INLINE void InvCipher(state_t *state, const uint8_t *RoundKey) { + uint8_t round = 0; + + AddRoundKey(Nr, state, RoundKey); + + for (round = (Nr - 1);; --round) { + __loop_tripcount(10); + InvShiftRows(state); + InvSubBytes(state); + AddRoundKey(round, state, RoundKey); + if (round == 0) { + break; + } + InvMixColumns(state); + } +} + +FORCE_INLINE void AES_init_ctx_iv(struct AES_ctx *ctx, const uint8_t *aes_key, const uint8_t *iv) { + KeyExpansion(ctx->RoundKey, aes_key); + __builtin_memcpy(ctx->Iv, iv, AES_BLOCKLEN); +} + +FORCE_INLINE void AES_ctx_set_iv(struct AES_ctx *ctx, const uint8_t *iv) { + __builtin_memcpy(ctx->Iv, iv, AES_BLOCKLEN); +} + +FORCE_INLINE void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint32_t length) { + uint32_t i; + uint8_t j; + + /* First block: XOR with IV from ctx */ + for (j = 0; j < AES_BLOCKLEN; ++j) { + __loop_tripcount(16); + g_buf[j] ^= ctx->Iv[j]; + } + Cipher((state_t *)&g_buf[0], ctx->RoundKey); + + /* Remaining blocks: XOR with previous ciphertext block */ + for (i = AES_BLOCKLEN; i < length; i += AES_BLOCKLEN) { + __loop_tripcount(AES_BUFFER_BLOCKS - 1); + for (j = 0; j < AES_BLOCKLEN; ++j) { + __loop_tripcount(16); + g_buf[i + j] ^= g_buf[i - AES_BLOCKLEN + j]; + } + Cipher((state_t *)&g_buf[i], ctx->RoundKey); + } + + /* Store last block as IV for next call */ + for (j = 0; j < AES_BLOCKLEN; ++j) { + __loop_tripcount(16); + ctx->Iv[j] = g_buf[length - AES_BLOCKLEN + j]; + } +} + +FORCE_INLINE void AES_CBC_decrypt_buffer(struct AES_ctx *ctx, uint32_t length) { + uint32_t i; + uint8_t j; + uint8_t storeNextIv[AES_BLOCKLEN]; + + for (i = 0; i < length; i += AES_BLOCKLEN) { + __loop_tripcount(AES_BUFFER_BLOCKS); + /* Save ciphertext for next IV */ + for (j = 0; j < AES_BLOCKLEN; ++j) { + __loop_tripcount(16); + storeNextIv[j] = g_buf[i + j]; + } + InvCipher((state_t *)&g_buf[i], ctx->RoundKey); + /* XOR with current IV */ + for (j = 0; j < AES_BLOCKLEN; ++j) { + __loop_tripcount(16); + g_buf[i + j] ^= ctx->Iv[j]; + } + /* Update IV */ + for (j = 0; j < AES_BLOCKLEN; ++j) { + __loop_tripcount(16); + ctx->Iv[j] = storeNextIv[j]; + } + } +} + +/* --- Main --- */ + +__attribute__((noinline)) int main(void) { + BENCH_INIT(); + int i; + + /* Copy test data to working buffer */ + for (i = 0; i < AES_BUFFER_LEN; i++) { + __loop_tripcount(AES_BUFFER_LEN); + g_buf[i] = test_data[i]; + } + + /* Encrypt */ + AES_init_ctx_iv(&g_ctx, g_key, g_iv); + AES_CBC_encrypt_buffer(&g_ctx, AES_BUFFER_LEN); + + /* Decrypt */ + AES_ctx_set_iv(&g_ctx, g_iv); + AES_CBC_decrypt_buffer(&g_ctx, AES_BUFFER_LEN); + + BENCH_EXIT((int)g_buf[0]); + return (int)g_buf[0]; +} diff --git a/tests/programs/crc.c b/tests/programs/crc.c new file mode 100644 index 00000000..9b5d7b83 --- /dev/null +++ b/tests/programs/crc.c @@ -0,0 +1,133 @@ +/********************************************************************** + * CRC-32 benchmark (preprocessed, self-contained) + * + * Based on Michael Barr's public-domain CRC implementation. + * Adapted for intermittent-computing checkpoint-insertion analysis. + **********************************************************************/ + +#include "benchmark.h" +#include "loop_tripcount.h" +#include + +#define FORCE_INLINE static inline __attribute__((always_inline)) + +/* CRC-32 parameters */ +typedef uint32_t crc; + +#define POLYNOMIAL 0x04C11DB7 +#define INITIAL_REMAINDER 0xFFFFFFFF +#define FINAL_XOR_VALUE 0xFFFFFFFF +#define WIDTH (8 * sizeof(crc)) +#define TOPBIT ((crc)1 << (WIDTH - 1)) + +/* Repeat the original 256-byte input to lengthen the timed workload + * without changing the benchmark structure. */ +#define CRC_TEST_DATA_REPEATS 4 +#define TEST_DATA_LEN (256 * CRC_TEST_DATA_REPEATS) + +#define CRC_TEST_DATA_256 \ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, \ + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, \ + 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, \ + 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, \ + 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, \ + 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, \ + 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, \ + 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, \ + 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, \ + 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, \ + 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, \ + 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, \ + 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, \ + 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, \ + 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, \ + 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, \ + 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, \ + 0x00 + +static const uint8_t test_data[TEST_DATA_LEN] = { + CRC_TEST_DATA_256, + CRC_TEST_DATA_256, + CRC_TEST_DATA_256, + CRC_TEST_DATA_256, +}; + +/* Mutable global: CRC lookup table */ +static crc crcTable[256] __attribute__((used)); + +/********************************************************************* + * reflect() - Reorder the bits of a binary sequence by reflecting + * them about the middle position. + *********************************************************************/ +FORCE_INLINE uint32_t reflect(uint32_t data, uint8_t nBits) { + uint32_t reflection = 0x00000000; + uint8_t bit; + + for (bit = 0; bit < nBits; ++bit) { + __loop_tripcount(32); + + if (data & 0x01) { + reflection |= (1 << ((nBits - 1) - bit)); + } + + data = (data >> 1); + } + + return reflection; +} + +/********************************************************************* + * crcInit() - Populate the partial CRC lookup table. + *********************************************************************/ +FORCE_INLINE void crcInit(void) { + crc remainder; + uint32_t dividend; + uint8_t bit; + + for (dividend = 0; dividend < 256; ++dividend) { + __loop_tripcount(256); + + remainder = dividend << (WIDTH - 8); + + for (bit = 8; bit > 0; --bit) { + __loop_tripcount(8); + + if (remainder & TOPBIT) { + remainder = (remainder << 1) ^ POLYNOMIAL; + } else { + remainder = (remainder << 1); + } + } + + crcTable[dividend] = remainder; + } +} + +/********************************************************************* + * crcFast() - Compute the CRC of a given message using the lookup + * table. crcInit() must be called first. + *********************************************************************/ +FORCE_INLINE crc crcFast(const uint8_t message[], uint32_t nBytes) { + crc remainder = INITIAL_REMAINDER; + uint8_t data; + uint32_t byte; + + for (byte = 0; byte < nBytes; ++byte) { + __loop_tripcount(TEST_DATA_LEN); + + data = (uint8_t)reflect(message[byte], 8) ^ (remainder >> (WIDTH - 8)); + remainder = crcTable[data] ^ (remainder << 8); + } + + return (uint32_t)reflect(remainder, WIDTH) ^ FINAL_XOR_VALUE; +} + +/* --- Main --- */ + +__attribute__((noinline)) int main(void) { + BENCH_INIT(); + crcInit(); + volatile crc result = crcFast((const uint8_t *)test_data, TEST_DATA_LEN); + BENCH_EXIT((int)result); + return (int)result; +} diff --git a/tests/test_capacitor_support.py b/tests/test_capacitor_support.py index 4fb1fa2f..da37133a 100644 --- a/tests/test_capacitor_support.py +++ b/tests/test_capacitor_support.py @@ -6,33 +6,38 @@ import pytest from ckpt.analysis.plot import _sort_key -from ckpt.analysis.strip_mining import parse_strip_mining_log -from ckpt.bench.config import discover_capacitors +from ckpt.analysis.strip_mining import CAPACITY_MAP, parse_strip_mining_log +from ckpt.bench.config import _DEFAULT_CAPS, discover_capacitors from ckpt.env import ProjectEnv from conftest import PROJECT_DIR pytestmark = pytest.mark.unit -def test_discover_capacitors_includes_50uf_by_default() -> None: +def test_discover_capacitors_resolves_every_default_cap() -> None: + """Discovery silently drops a default cap whose config file is missing.""" env = ProjectEnv.from_environ(PROJECT_DIR) caps = discover_capacitors(env, "milp", None) - assert [cap.label for cap in caps] == ["1uF", "5uF", "10uF", "50uF", "100uF"] + assert [cap.label for cap in caps] == _DEFAULT_CAPS + # Comparing against _DEFAULT_CAPS alone still passes if 50uF is dropped + # from both. + assert "50uF" in _DEFAULT_CAPS -def test_discover_capacitors_resolves_50uf_explicitly() -> None: +def test_discover_capacitors_resolves_explicit_cap() -> None: env = ProjectEnv.from_environ(PROJECT_DIR) + label = _DEFAULT_CAPS[-1] - caps = discover_capacitors(env, "milp", ["50uF"]) + caps = discover_capacitors(env, "milp", [label]) assert len(caps) == 1 - assert caps[0].label == "50uF" - assert caps[0].config_path == PROJECT_DIR / "benchmarks" / "config_50uF.json" + assert caps[0].label == label + assert caps[0].config_path == PROJECT_DIR / "benchmarks" / f"config_{label}.json" -def test_parse_strip_mining_log_uses_50uf_capacity(tmp_path: Path) -> None: +def test_parse_strip_mining_log_resolves_capacitor_capacity(tmp_path: Path) -> None: log_path = tmp_path / "strip_mining.log" log_path.write_text("[1/1] Running crc-50uF ...\n") @@ -40,7 +45,7 @@ def test_parse_strip_mining_log_uses_50uf_capacity(tmp_path: Path) -> None: assert len(runs) == 1 assert runs[0]["capacitor"] == "50uF" - assert runs[0]["capacity"] == 243000.0 + assert runs[0]["capacity"] == CAPACITY_MAP["50uF"] def test_plot_sort_key_orders_capacitors_numerically() -> None: diff --git a/tests/test_rockclimb.py b/tests/test_rockclimb.py index 254baceb..76ef0def 100644 --- a/tests/test_rockclimb.py +++ b/tests/test_rockclimb.py @@ -20,7 +20,7 @@ PROJECT_DIR / "passes" / "build" / "rockclimb-backend" / "RockClimbMachinePass.so" ) ROCKCLIMB_PARAMS = TESTS_DIR / "rockclimb_params.json" -ASSEMBLY_ENERGY_CONFIG = PROJECT_DIR / "benchmarks" / "assembly_params.json" +ASSEMBLY_ENERGY_CONFIG = TESTS_DIR / "assembly_params.json" # --------------------------------------------------------------------------- # Module-wide mark @@ -141,6 +141,12 @@ def _run( check=False, ) + if "Missing (" in result.stderr: + assert "Missing (0 keys):" in result.stderr, ( + f"{energy_config} does not price every instruction reached by " + f"this snippet, so the pass fell back to 1.0:\n{result.stderr}" + ) + output_mir = out_mir.read_text() if out_mir.exists() else "" output_asm = "" @@ -763,9 +769,10 @@ def test_external_call_costed_as_expensive_instruction( self, run_rockclimb_machine, tmp_path ): """An external library call with no entry/exit boundaries of its own is - costed as a single expensive instruction. Integer division - (__mspabi_divi ~752) exceeds E_safe (~472) for the default config, so a - block containing it cannot fit one charge.""" + costed as a single expensive instruction. tests/assembly_params.json + prices __mspabi_divi at 750, above the E_safe of 471.87 that + tests/rockclimb_params.json yields, so a block containing an integer + division cannot fit one charge.""" src = write_src(tmp_path, "int divfn(int a, int b) { return a / b; }") result = run_rockclimb_machine( src, diff --git a/tests/test_rockclimb_preprocess.py b/tests/test_rockclimb_preprocess.py index ae193c9b..9f46969e 100644 --- a/tests/test_rockclimb_preprocess.py +++ b/tests/test_rockclimb_preprocess.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import re import subprocess from pathlib import Path @@ -13,14 +12,16 @@ from ckpt.env import ProjectEnv from ckpt.runner import StepResult from ckpt.toolchain import Toolchain -from conftest import PROJECT_DIR, _run, write_src +from conftest import PROJECT_DIR, TESTS_DIR, _run, write_src -IR_ENERGY_CONFIG = PROJECT_DIR / "benchmarks" / "sample_energy_config_ir.json" -ASSEMBLY_ENERGY_CONFIG = PROJECT_DIR / "benchmarks" / "assembly_params.json" -CRC_BENCHMARK = PROJECT_DIR / "benchmarks" / "intermittent" / "crc.c" -CRC_5UF_CONFIG = PROJECT_DIR / "benchmarks" / "config_5uF.json" -AES_BENCHMARK = PROJECT_DIR / "benchmarks" / "intermittent" / "aes.c" -AES_1UF_CONFIG = PROJECT_DIR / "benchmarks" / "config_1uF.json" +IR_ENERGY_CONFIG = TESTS_DIR / "estimator_ir_weighted.json" +ASSEMBLY_ENERGY_CONFIG = TESTS_DIR / "assembly_params.json" +# Cost of one CONSTANT_LOOP iteration under IR_ENERGY_CONFIG. +CONSTANT_LOOP_ITER_ENERGY = 12.0 +CRC_BENCHMARK = TESTS_DIR / "programs" / "crc.c" +AES_BENCHMARK = TESTS_DIR / "programs" / "aes.c" +SMALL_CAP_CAPACITY = 3640.0 +LARGE_CAP_CAPACITY = 18200.0 pytestmark = pytest.mark.rockclimb @@ -89,6 +90,26 @@ def _write_rockclimb_config(tmp_path: Path, *, capacity: float) -> Path: return config_path +def _write_board_like_config(tmp_path: Path, *, capacity: float) -> Path: + """Unlike _write_rockclimb_config, this leaves E_safe below capacity, as a + real capacitor does.""" + config_path = tmp_path / "rockclimb_board.json" + config_path.write_text( + json.dumps( + { + "capacity": capacity, + "E_pro": 45.0, + "E_epi": 35.0, + "N_reg": 16, + "reg_store_energy": 0.0, + "reg_restore_energy": 2.0, + "rockclimb": {"distributed_checkpointing": True}, + } + ) + ) + return config_path + + def _prepare_ir_for_preprocess(tools, compile_to_ir, src: Path, tmp_path: Path) -> Path: input_ll = tmp_path / "input.ll" compile_to_ir(src, input_ll) @@ -174,7 +195,11 @@ def test_preprocess_partially_unrolls_constant_trip_loop( src = write_src(tmp_path, CONSTANT_LOOP) optimized_ll = _prepare_ir_for_preprocess(tools, compile_to_ir, src, tmp_path) output_ll = tmp_path / "preprocessed.ll" - config_path = _write_rockclimb_config(tmp_path, capacity=20.0) + # Room for a few iterations but not all 8, so the budget picks K, not the + # max-unroll cap. + config_path = _write_rockclimb_config( + tmp_path, capacity=3 * CONSTANT_LOOP_ITER_ENERGY + 4.0 + ) before_ir = optimized_ll.read_text() result = _run_rockclimb_preprocess( @@ -199,7 +224,10 @@ def test_preprocess_caps_unroll_for_loop_that_fits_budget( src = write_src(tmp_path, CONSTANT_LOOP) optimized_ll = _prepare_ir_for_preprocess(tools, compile_to_ir, src, tmp_path) output_ll = tmp_path / "preprocessed.ll" - config_path = _write_rockclimb_config(tmp_path, capacity=200.0) + # Room for the whole loop twice over, so only the max-unroll cap can limit K. + config_path = _write_rockclimb_config( + tmp_path, capacity=16 * CONSTANT_LOOP_ITER_ENERGY + ) before_ir = optimized_ll.read_text() result = _run_rockclimb_preprocess( @@ -223,7 +251,11 @@ def test_preprocess_honors_cli_max_unroll_factor(tools, compile_to_ir, tmp_path) src = write_src(tmp_path, CONSTANT_LOOP) optimized_ll = _prepare_ir_for_preprocess(tools, compile_to_ir, src, tmp_path) output_ll = tmp_path / "preprocessed.ll" - config_path = _write_rockclimb_config(tmp_path, capacity=200.0) + # 3 must differ from RockClimbMaxUnrollFactorOpt's default of 4, or + # ignoring the flag would produce the same K. + config_path = _write_rockclimb_config( + tmp_path, capacity=16 * CONSTANT_LOOP_ITER_ENERGY + ) result = _run_rockclimb_preprocess( tools, @@ -231,12 +263,12 @@ def test_preprocess_honors_cli_max_unroll_factor(tools, compile_to_ir, tmp_path) IR_ENERGY_CONFIG, config_path, output_ll, - 4, + 3, ) assert result.returncode == 0, result.stderr assert "RockClimbLoopUnrollPass: unrolled sum8::" in result.stdout + result.stderr - assert "K=4" in result.stdout + result.stderr + assert "K=3" in result.stdout + result.stderr def test_preprocess_skips_unknown_trip_count_loop(tools, compile_to_ir, tmp_path): @@ -478,6 +510,7 @@ def wrapped_preprocess(tc, env, opts, tmp, input_ll): def test_crc_unroll_does_not_explode_distributed_checkpoints(tmp_path): env = ProjectEnv.from_environ(PROJECT_DIR) tc = Toolchain.resolve(env) + crc_config = _write_board_like_config(tmp_path, capacity=LARGE_CAP_CAPACITY) out4 = compile_rockclimb( tc, @@ -485,7 +518,7 @@ def test_crc_unroll_does_not_explode_distributed_checkpoints(tmp_path): RockClimbCompileOptions( input_c=CRC_BENCHMARK, energy_config=ASSEMBLY_ENERGY_CONFIG, - rockclimb_config=CRC_5UF_CONFIG, + rockclimb_config=crc_config, output=tmp_path / "crc_u4", pass_log_level="info", precomputed_energy=True, @@ -506,7 +539,7 @@ def test_crc_unroll_does_not_explode_distributed_checkpoints(tmp_path): RockClimbCompileOptions( input_c=CRC_BENCHMARK, energy_config=ASSEMBLY_ENERGY_CONFIG, - rockclimb_config=CRC_5UF_CONFIG, + rockclimb_config=crc_config, output=tmp_path / "crc_u16", pass_log_level="info", precomputed_energy=True, @@ -534,6 +567,7 @@ def test_crc_unroll_does_not_explode_distributed_checkpoints(tmp_path): def test_compile_rockclimb_handles_full_unroll_in_nested_loops(tmp_path): env = ProjectEnv.from_environ(PROJECT_DIR) tc = Toolchain.resolve(env) + aes_config = _write_board_like_config(tmp_path, capacity=SMALL_CAP_CAPACITY) result = compile_rockclimb( tc, @@ -541,7 +575,7 @@ def test_compile_rockclimb_handles_full_unroll_in_nested_loops(tmp_path): RockClimbCompileOptions( input_c=AES_BENCHMARK, energy_config=ASSEMBLY_ENERGY_CONFIG, - rockclimb_config=AES_1UF_CONFIG, + rockclimb_config=aes_config, output=tmp_path / "aes_u5", pass_log_level="info", precomputed_energy=True, @@ -557,7 +591,6 @@ def test_compile_rockclimb_handles_full_unroll_in_nested_loops(tmp_path): ), ) + # Fully unrolling a nested loop deletes it, which the pass must survive. assert "Stack dump:" not in result.pass_output - assert re.search( - r"RockClimbLoopUnrollPass: fully unrolled main::.* N=4 K=4", result.pass_output - ) + assert "RockClimbLoopUnrollPass: fully unrolled main::" in result.pass_output diff --git a/tests/test_schematic.py b/tests/test_schematic.py index b71e82d8..d3a9e5e3 100644 --- a/tests/test_schematic.py +++ b/tests/test_schematic.py @@ -8,6 +8,7 @@ import json import re import subprocess +from typing import NamedTuple import pytest from conftest import ( @@ -166,21 +167,37 @@ def _run_ckpt_compile_schematic_o3( return result, result.stdout + result.stderr +class LoopBudget(NamedTuple): + # E_loop is logged before refineLoopBudgetWithConvergence and numIt after, + # so the two are not a single snapshot. + e_loop: float + num_it: int + max_trip_count: int + + +_NUMBER = r"[0-9.eE+-]+" + + def _extract_loop_budget( log_text, loop_header, ): e_loop_match = re.search( - rf"\[LoopAnalyzer\]\s+loop={re.escape(loop_header)}\s+E_loop=([0-9.]+)", + rf"\[LoopAnalyzer\]\s+loop={re.escape(loop_header)}\s+E_loop=({_NUMBER})", log_text, ) num_it_match = re.search( - rf"\[LoopAnalyzer\]\s+loop={re.escape(loop_header)}\s+numIt=(\d+)", + rf"\[LoopAnalyzer\]\s+loop={re.escape(loop_header)}\s+numIt=(\d+)" + rf"\s+maxTripCount=(\d+)", log_text, ) assert e_loop_match, f"Missing E_loop log for {loop_header}:\n{log_text[-4000:]}" assert num_it_match, f"Missing numIt log for {loop_header}:\n{log_text[-4000:]}" - return float(e_loop_match.group(1)), int(num_it_match.group(1)) + return LoopBudget( + e_loop=float(e_loop_match.group(1)), + num_it=int(num_it_match.group(1)), + max_trip_count=int(num_it_match.group(2)), + ) def _write_trace(trace, trace_json): @@ -293,12 +310,19 @@ def test_schematic_o3_dijkstra_loop_budget_uses_rare_inner_branch( assert result.returncode == 0, log_text[-4000:] - inner_e_loop, _inner_num_it = _extract_loop_budget(log_text, "for.body18.i") - outer_e_loop, outer_num_it = _extract_loop_budget(log_text, "while.body.i") + inner = _extract_loop_budget(log_text, "for.body18.i") + outer = _extract_loop_budget(log_text, "while.body.i") + + # The inner loop is entered rarely in the trace, but a single outer + # iteration may still run it to its full trip count. The outer budget must + # therefore cover the whole inner loop, not just the hot path through it. + inner_worst_case = inner.e_loop * inner.max_trip_count + assert outer.e_loop >= inner_worst_case, ( + f"outer E_loop {outer.e_loop} ignores the rare inner branch " + f"(worst-case inner cost {inner_worst_case})\n{log_text[-4000:]}" + ) - assert inner_e_loop > 70.0, log_text - assert outer_e_loop > 2400.0, log_text - assert outer_num_it < 25, log_text + assert outer.num_it < outer.max_trip_count, log_text[-4000:] def test_schematic_synthesizes_missing_top_level_loop_trace(