| 1 | import v3.ssa |
| 2 | import v3.ssa.optimize |
| 3 | |
| 4 | // --- helpers --------------------------------------------------------------- |
| 5 | |
| 6 | // count_op supports count op handling for v3 tests. |
| 7 | fn count_op(m &ssa.Module, func_id int, op ssa.OpCode) int { |
| 8 | mut n := 0 |
| 9 | for blk_id in m.funcs[func_id].blocks { |
| 10 | for val_id in m.blocks[blk_id].instrs { |
| 11 | val := m.values[val_id] |
| 12 | if val.kind == .instruction && m.instrs[val.index].op == op { |
| 13 | n++ |
| 14 | } |
| 15 | } |
| 16 | } |
| 17 | return n |
| 18 | } |
| 19 | |
| 20 | // --- TypeStore: arrays, tuples, unsigned ---------------------------------- |
| 21 | |
| 22 | // test_type_store_arrays_tuples_unsigned validates this v3 regression case. |
| 23 | fn test_type_store_arrays_tuples_unsigned() { |
| 24 | mut ts := ssa.TypeStore.new() |
| 25 | s32 := ts.get_int(32) |
| 26 | un32 := ts.get_uint(32) |
| 27 | // Signed and unsigned of the same width must be distinct types. |
| 28 | assert s32 != un32 |
| 29 | assert !ts.types[s32].is_unsigned |
| 30 | assert ts.types[un32].is_unsigned |
| 31 | |
| 32 | arr := ts.get_array(s32, 4) |
| 33 | arr2 := ts.get_array(s32, 4) |
| 34 | assert arr == arr2 // cached |
| 35 | assert ts.types[arr].kind == .array_t |
| 36 | assert ts.types[arr].len == 4 |
| 37 | assert ts.types[arr].elem_type == s32 |
| 38 | |
| 39 | tup := ts.get_tuple([s32, un32]) |
| 40 | assert ts.types[tup].kind == .struct_t |
| 41 | assert ts.types[tup].fields == [s32, un32] |
| 42 | } |
| 43 | |
| 44 | // test_array_type_sizing validates array type sizing behavior in v3 tests. |
| 45 | fn test_array_type_sizing() { |
| 46 | mut m := ssa.Module.new() |
| 47 | s32 := m.type_store.get_int(32) |
| 48 | arr := m.type_store.get_array(s32, 5) |
| 49 | // Fixed array of 5 x i32 = 20 bytes, aligned to element alignment (4). |
| 50 | assert m.type_size(arr) == 20 |
| 51 | assert m.type_align(arr) == 4 |
| 52 | } |
| 53 | |
| 54 | // --- algebraic simplification --------------------------------------------- |
| 55 | |
| 56 | // test_algebraic_add_zero validates algebraic add zero behavior in v3 tests. |
| 57 | fn test_algebraic_add_zero() { |
| 58 | mut m := ssa.Module.new() |
| 59 | i64t := m.type_store.get_int(64) |
| 60 | func_id := m.new_function('add_zero', i64t) |
| 61 | x := m.add_value(.argument, i64t, 'x', 0) |
| 62 | m.func_add_param(func_id, x) |
| 63 | entry := m.add_block(func_id, 'entry') |
| 64 | zero := m.get_or_add_const(i64t, '0') |
| 65 | r := m.add_instr(.add, entry, i64t, [x, zero]) |
| 66 | m.add_instr(.ret, entry, ssa.TypeID(0), [r]) |
| 67 | |
| 68 | optimize.optimize(mut m) |
| 69 | |
| 70 | // x + 0 -> x, so the add is gone and ret returns the argument directly. |
| 71 | assert count_op(m, func_id, .add) == 0 |
| 72 | ret_id := m.blocks[m.funcs[func_id].blocks[0]].instrs.last() |
| 73 | ret := m.instrs[m.values[ret_id].index] |
| 74 | assert m.values[ret.operands[0]].kind == .argument |
| 75 | } |
| 76 | |
| 77 | // test_algebraic_mul_two_to_shift validates algebraic mul two to shift behavior in v3 tests. |
| 78 | fn test_algebraic_mul_two_to_shift() { |
| 79 | mut m := ssa.Module.new() |
| 80 | i64t := m.type_store.get_int(64) |
| 81 | func_id := m.new_function('mul2', i64t) |
| 82 | x := m.add_value(.argument, i64t, 'x', 0) |
| 83 | m.func_add_param(func_id, x) |
| 84 | entry := m.add_block(func_id, 'entry') |
| 85 | two := m.get_or_add_const(i64t, '2') |
| 86 | r := m.add_instr(.mul, entry, i64t, [x, two]) |
| 87 | m.add_instr(.ret, entry, ssa.TypeID(0), [r]) |
| 88 | |
| 89 | optimize.optimize(mut m) |
| 90 | |
| 91 | // x * 2 -> x << 1 |
| 92 | assert count_op(m, func_id, .mul) == 0 |
| 93 | assert count_op(m, func_id, .shl) == 1 |
| 94 | } |
| 95 | |
| 96 | // test_algebraic_sub_self_is_zero validates algebraic sub self is zero behavior in v3 tests. |
| 97 | fn test_algebraic_sub_self_is_zero() { |
| 98 | mut m := ssa.Module.new() |
| 99 | i64t := m.type_store.get_int(64) |
| 100 | func_id := m.new_function('sub_self', i64t) |
| 101 | x := m.add_value(.argument, i64t, 'x', 0) |
| 102 | m.func_add_param(func_id, x) |
| 103 | entry := m.add_block(func_id, 'entry') |
| 104 | r := m.add_instr(.sub, entry, i64t, [x, x]) |
| 105 | m.add_instr(.ret, entry, ssa.TypeID(0), [r]) |
| 106 | |
| 107 | optimize.optimize(mut m) |
| 108 | |
| 109 | assert count_op(m, func_id, .sub) == 0 |
| 110 | ret_id := m.blocks[m.funcs[func_id].blocks[0]].instrs.last() |
| 111 | ret := m.instrs[m.values[ret_id].index] |
| 112 | ret_operand := m.values[ret.operands[0]] |
| 113 | assert ret_operand.kind == .constant |
| 114 | assert ret_operand.name == '0' |
| 115 | } |
| 116 | |
| 117 | // --- DCE side-effect preservation ----------------------------------------- |
| 118 | |
| 119 | // test_dce_preserves_side_effecting_ops validates this v3 regression case. |
| 120 | fn test_dce_preserves_side_effecting_ops() { |
| 121 | mut m := ssa.Module.new() |
| 122 | i64t := m.type_store.get_int(64) |
| 123 | func_id := m.new_function('side_fx', i64t) |
| 124 | entry := m.add_block(func_id, 'entry') |
| 125 | // A pure dead add that should be removed. |
| 126 | a := m.get_or_add_const(i64t, '7') |
| 127 | b := m.get_or_add_const(i64t, '9') |
| 128 | m.add_instr(.add, entry, i64t, [a, b]) |
| 129 | // Side-effecting ops with no uses that must be preserved. |
| 130 | m.add_instr(.fence, entry, ssa.TypeID(0), []) |
| 131 | m.add_instr(.cmpxchg, entry, i64t, [a]) |
| 132 | m.add_instr(.ret, entry, ssa.TypeID(0), []) |
| 133 | |
| 134 | optimize.optimize(mut m) |
| 135 | |
| 136 | assert count_op(m, func_id, .add) == 0 // dead pure op removed |
| 137 | assert count_op(m, func_id, .fence) == 1 // preserved |
| 138 | assert count_op(m, func_id, .cmpxchg) == 1 // preserved |
| 139 | } |
| 140 | |
| 141 | // --- structured verifier --------------------------------------------------- |
| 142 | |
| 143 | // test_structured_verifier_accepts_valid_module validates this v3 regression case. |
| 144 | fn test_structured_verifier_accepts_valid_module() { |
| 145 | mut m := ssa.Module.new() |
| 146 | i64t := m.type_store.get_int(64) |
| 147 | func_id := m.new_function('valid', i64t) |
| 148 | entry := m.add_block(func_id, 'entry') |
| 149 | c := m.get_or_add_const(i64t, '1') |
| 150 | m.add_instr(.ret, entry, ssa.TypeID(0), [c]) |
| 151 | |
| 152 | optimize.optimize(mut m) |
| 153 | errors := optimize.verify(m) |
| 154 | // No structural errors on a well-formed module. |
| 155 | for e in errors { |
| 156 | assert !e.msg.contains('invalid') |
| 157 | assert !e.msg.contains('not in its') |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // test_structured_verifier_accepts_prototype_declaration validates this v3 regression case. |
| 162 | fn test_structured_verifier_accepts_prototype_declaration() { |
| 163 | mut m := ssa.Module.new() |
| 164 | i64t := m.type_store.get_int(64) |
| 165 | // A prototype/extern function with no body must be accepted (not fatal). |
| 166 | proto := m.new_function('extern_decl', i64t) |
| 167 | m.func_set_prototype(proto, true) |
| 168 | m.func_set_c_extern(proto, true) |
| 169 | // Should not panic. |
| 170 | optimize.verify_and_panic(m, 'prototype test') |
| 171 | assert true |
| 172 | } |
| 173 | |
| 174 | // --- mem2reg + phi elimination (IR level) --------------------------------- |
| 175 | |
| 176 | // test_mem2reg_promotes_diamond_slot validates this v3 regression case. |
| 177 | fn test_mem2reg_promotes_diamond_slot() { |
| 178 | mut m := ssa.Module.new() |
| 179 | i64t := m.type_store.get_int(64) |
| 180 | i1 := m.type_store.get_int(1) |
| 181 | ptr_i64 := m.type_store.get_ptr(i64t) |
| 182 | func_id := m.new_function('diamond', i64t) |
| 183 | cond := m.add_value(.argument, i1, 'cond', 0) |
| 184 | m.func_add_param(func_id, cond) |
| 185 | |
| 186 | entry := m.add_block(func_id, 'entry') |
| 187 | then_blk := m.add_block(func_id, 'then') |
| 188 | else_blk := m.add_block(func_id, 'else') |
| 189 | merge := m.add_block(func_id, 'merge') |
| 190 | |
| 191 | slot := m.add_instr(.alloca, entry, ptr_i64, []) |
| 192 | m.add_instr(.br, entry, ssa.TypeID(0), [cond, ssa.ValueID(then_blk), ssa.ValueID(else_blk)]) |
| 193 | |
| 194 | ten := m.get_or_add_const(i64t, '10') |
| 195 | m.add_instr(.store, then_blk, ssa.TypeID(0), [ten, slot]) |
| 196 | m.add_instr(.jmp, then_blk, ssa.TypeID(0), [ssa.ValueID(merge)]) |
| 197 | |
| 198 | twenty := m.get_or_add_const(i64t, '20') |
| 199 | m.add_instr(.store, else_blk, ssa.TypeID(0), [twenty, slot]) |
| 200 | m.add_instr(.jmp, else_blk, ssa.TypeID(0), [ssa.ValueID(merge)]) |
| 201 | |
| 202 | loaded := m.add_instr(.load, merge, i64t, [slot]) |
| 203 | m.add_instr(.ret, merge, ssa.TypeID(0), [loaded]) |
| 204 | |
| 205 | optimize.optimize_with_options(mut m, optimize.OptimizeOptions{ |
| 206 | mem2reg: true |
| 207 | eliminate_phis: true |
| 208 | }) |
| 209 | |
| 210 | // The scalar slot is fully promoted: no alloca/load/store remain. |
| 211 | assert count_op(m, func_id, .alloca) == 0 |
| 212 | assert count_op(m, func_id, .load) == 0 |
| 213 | assert count_op(m, func_id, .store) == 0 |
| 214 | // Phi was lowered to assign copies in the predecessor blocks. |
| 215 | assert count_op(m, func_id, .assign) >= 1 |
| 216 | // And no phi survives after elimination. |
| 217 | assert count_op(m, func_id, .phi) == 0 |
| 218 | } |
| 219 | |