zig/src/codegen/arm.zig

895 lines
26 KiB
Zig

const std = @import("std");
const DW = std.dwarf;
const testing = std.testing;
/// The condition field specifies the flags neccessary for an
/// Instruction to be executed
pub const Condition = enum(u4) {
/// equal
eq,
/// not equal
ne,
/// unsigned higher or same
cs,
/// unsigned lower
cc,
/// negative
mi,
/// positive or zero
pl,
/// overflow
vs,
/// no overflow
vc,
/// unsigned higer
hi,
/// unsigned lower or same
ls,
/// greater or equal
ge,
/// less than
lt,
/// greater than
gt,
/// less than or equal
le,
/// always
al,
};
/// Represents a register in the ARM instruction set architecture
pub const Register = enum(u5) {
r0,
r1,
r2,
r3,
r4,
r5,
r6,
r7,
r8,
r9,
r10,
r11,
r12,
r13,
r14,
r15,
/// Argument / result / scratch register 1
a1,
/// Argument / result / scratch register 2
a2,
/// Argument / scratch register 3
a3,
/// Argument / scratch register 4
a4,
/// Variable-register 1
v1,
/// Variable-register 2
v2,
/// Variable-register 3
v3,
/// Variable-register 4
v4,
/// Variable-register 5
v5,
/// Platform register
v6,
/// Variable-register 7
v7,
/// Frame pointer or Variable-register 8
fp,
/// Intra-Procedure-call scratch register
ip,
/// Stack pointer
sp,
/// Link register
lr,
/// Program counter
pc,
/// Returns the unique 4-bit ID of this register which is used in
/// the machine code
pub fn id(self: Register) u4 {
return @truncate(u4, @enumToInt(self));
}
/// Returns the index into `callee_preserved_regs`.
pub fn allocIndex(self: Register) ?u4 {
inline for (callee_preserved_regs) |cpreg, i| {
if (self.id() == cpreg.id()) return i;
}
return null;
}
pub fn dwarfLocOp(self: Register) u8 {
return @as(u8, self.id()) + DW.OP_reg0;
}
};
test "Register.id" {
testing.expectEqual(@as(u4, 15), Register.r15.id());
testing.expectEqual(@as(u4, 15), Register.pc.id());
}
/// Program status registers containing flags, mode bits and other
/// vital information
pub const Psr = enum {
cpsr,
spsr,
};
pub const callee_preserved_regs = [_]Register{ .r0, .r1, .r2, .r3, .r4, .r5, .r6, .r7, .r8, .r10 };
pub const c_abi_int_param_regs = [_]Register{ .r0, .r1, .r2, .r3 };
pub const c_abi_int_return_regs = [_]Register{ .r0, .r1 };
/// Represents an instruction in the ARM instruction set architecture
pub const Instruction = union(enum) {
DataProcessing: packed struct {
// Note to self: The order of the fields top-to-bottom is
// right-to-left in the actual 32-bit int representation
op2: u12,
rd: u4,
rn: u4,
s: u1,
opcode: u4,
i: u1,
fixed: u2 = 0b00,
cond: u4,
},
SingleDataTransfer: packed struct {
offset: u12,
rd: u4,
rn: u4,
load_store: u1,
write_back: u1,
byte_word: u1,
up_down: u1,
pre_post: u1,
imm: u1,
fixed: u2 = 0b01,
cond: u4,
},
BlockDataTransfer: packed struct {
register_list: u16,
rn: u4,
load_store: u1,
write_back: u1,
psr_or_user: u1,
up_down: u1,
pre_post: u1,
fixed: u3 = 0b100,
cond: u4,
},
Branch: packed struct {
offset: u24,
link: u1,
fixed: u3 = 0b101,
cond: u4,
},
BranchExchange: packed struct {
rn: u4,
fixed_1: u1 = 0b1,
link: u1,
fixed_2: u22 = 0b0001_0010_1111_1111_1111_00,
cond: u4,
},
SupervisorCall: packed struct {
comment: u24,
fixed: u4 = 0b1111,
cond: u4,
},
Breakpoint: packed struct {
imm4: u4,
fixed_1: u4 = 0b0111,
imm12: u12,
fixed_2_and_cond: u12 = 0b1110_0001_0010,
},
/// Represents the possible operations which can be performed by a
/// DataProcessing instruction
const Opcode = enum(u4) {
// Rd := Op1 AND Op2
@"and",
// Rd := Op1 EOR Op2
eor,
// Rd := Op1 - Op2
sub,
// Rd := Op2 - Op1
rsb,
// Rd := Op1 + Op2
add,
// Rd := Op1 + Op2 + C
adc,
// Rd := Op1 - Op2 + C - 1
sbc,
// Rd := Op2 - Op1 + C - 1
rsc,
// set condition codes on Op1 AND Op2
tst,
// set condition codes on Op1 EOR Op2
teq,
// set condition codes on Op1 - Op2
cmp,
// set condition codes on Op1 + Op2
cmn,
// Rd := Op1 OR Op2
orr,
// Rd := Op2
mov,
// Rd := Op1 AND NOT Op2
bic,
// Rd := NOT Op2
mvn,
};
/// Represents the second operand to a data processing instruction
/// which can either be content from a register or an immediate
/// value
pub const Operand = union(enum) {
Register: packed struct {
rm: u4,
shift: u8,
},
Immediate: packed struct {
imm: u8,
rotate: u4,
},
/// Represents multiple ways a register can be shifted. A
/// register can be shifted by a specific immediate value or
/// by the contents of another register
pub const Shift = union(enum) {
Immediate: packed struct {
fixed: u1 = 0b0,
typ: u2,
amount: u5,
},
Register: packed struct {
fixed_1: u1 = 0b1,
typ: u2,
fixed_2: u1 = 0b0,
rs: u4,
},
pub const Type = enum(u2) {
logical_left,
logical_right,
arithmetic_right,
rotate_right,
};
pub const none = Shift{
.Immediate = .{
.amount = 0,
.typ = 0,
},
};
pub fn toU8(self: Shift) u8 {
return switch (self) {
.Register => |v| @bitCast(u8, v),
.Immediate => |v| @bitCast(u8, v),
};
}
pub fn reg(rs: Register, typ: Type) Shift {
return Shift{
.Register = .{
.rs = rs.id(),
.typ = @enumToInt(typ),
},
};
}
pub fn imm(amount: u5, typ: Type) Shift {
return Shift{
.Immediate = .{
.amount = amount,
.typ = @enumToInt(typ),
},
};
}
};
pub fn toU12(self: Operand) u12 {
return switch (self) {
.Register => |v| @bitCast(u12, v),
.Immediate => |v| @bitCast(u12, v),
};
}
pub fn reg(rm: Register, shift: Shift) Operand {
return Operand{
.Register = .{
.rm = rm.id(),
.shift = shift.toU8(),
},
};
}
pub fn imm(immediate: u8, rotate: u4) Operand {
return Operand{
.Immediate = .{
.imm = immediate,
.rotate = rotate,
},
};
}
};
/// Represents the offset operand of a load or store
/// instruction. Data can be loaded from memory with either an
/// immediate offset or an offset that is stored in some register.
pub const Offset = union(enum) {
Immediate: u12,
Register: packed struct {
rm: u4,
shift: u8,
},
pub const none = Offset{
.Immediate = 0,
};
pub fn toU12(self: Offset) u12 {
return switch (self) {
.Register => |v| @bitCast(u12, v),
.Immediate => |v| v,
};
}
pub fn reg(rm: Register, shift: u8) Offset {
return Offset{
.Register = .{
.rm = rm.id(),
.shift = shift,
},
};
}
pub fn imm(immediate: u8) Offset {
return Offset{
.Immediate = immediate,
};
}
};
/// Represents the register list operand to a block data transfer
/// instruction
pub const RegisterList = packed struct {
r0: bool = false,
r1: bool = false,
r2: bool = false,
r3: bool = false,
r4: bool = false,
r5: bool = false,
r6: bool = false,
r7: bool = false,
r8: bool = false,
r9: bool = false,
r10: bool = false,
r11: bool = false,
r12: bool = false,
r13: bool = false,
r14: bool = false,
r15: bool = false,
};
pub fn toU32(self: Instruction) u32 {
return switch (self) {
.DataProcessing => |v| @bitCast(u32, v),
.SingleDataTransfer => |v| @bitCast(u32, v),
.BlockDataTransfer => |v| @bitCast(u32, v),
.Branch => |v| @bitCast(u32, v),
.BranchExchange => |v| @bitCast(u32, v),
.SupervisorCall => |v| @bitCast(u32, v),
.Breakpoint => |v| @intCast(u32, v.imm4) | (@intCast(u32, v.fixed_1) << 4) | (@intCast(u32, v.imm12) << 8) | (@intCast(u32, v.fixed_2_and_cond) << 20),
};
}
// Helper functions for the "real" functions below
fn dataProcessing(
cond: Condition,
opcode: Opcode,
s: u1,
rd: Register,
rn: Register,
op2: Operand,
) Instruction {
return Instruction{
.DataProcessing = .{
.cond = @enumToInt(cond),
.i = @boolToInt(op2 == .Immediate),
.opcode = @enumToInt(opcode),
.s = s,
.rn = rn.id(),
.rd = rd.id(),
.op2 = op2.toU12(),
},
};
}
fn singleDataTransfer(
cond: Condition,
rd: Register,
rn: Register,
offset: Offset,
pre_index: bool,
positive: bool,
byte_word: u1,
write_back: bool,
load_store: u1,
) Instruction {
return Instruction{
.SingleDataTransfer = .{
.cond = @enumToInt(cond),
.rn = rn.id(),
.rd = rd.id(),
.offset = offset.toU12(),
.load_store = load_store,
.write_back = @boolToInt(write_back),
.byte_word = byte_word,
.up_down = @boolToInt(positive),
.pre_post = @boolToInt(pre_index),
.imm = @boolToInt(offset != .Immediate),
},
};
}
fn blockDataTransfer(
cond: Condition,
rn: Register,
reg_list: RegisterList,
pre_post: u1,
up_down: u1,
psr_or_user: u1,
write_back: bool,
load_store: u1,
) Instruction {
return Instruction{
.BlockDataTransfer = .{
.register_list = @bitCast(u16, reg_list),
.rn = rn.id(),
.load_store = load_store,
.write_back = @boolToInt(write_back),
.psr_or_user = psr_or_user,
.up_down = up_down,
.pre_post = pre_post,
.cond = @enumToInt(cond),
},
};
}
fn branch(cond: Condition, offset: i24, link: u1) Instruction {
return Instruction{
.Branch = .{
.cond = @enumToInt(cond),
.link = link,
.offset = @bitCast(u24, offset),
},
};
}
fn branchExchange(cond: Condition, rn: Register, link: u1) Instruction {
return Instruction{
.BranchExchange = .{
.cond = @enumToInt(cond),
.link = link,
.rn = rn.id(),
},
};
}
fn supervisorCall(cond: Condition, comment: u24) Instruction {
return Instruction{
.SupervisorCall = .{
.cond = @enumToInt(cond),
.comment = comment,
},
};
}
fn breakpoint(imm: u16) Instruction {
return Instruction{
.Breakpoint = .{
.imm12 = @truncate(u12, imm >> 4),
.imm4 = @truncate(u4, imm),
},
};
}
// Public functions replicating assembler syntax as closely as
// possible
// Data processing
pub fn @"and"(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .@"and", 0, rd, rn, op2);
}
pub fn ands(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .@"and", 1, rd, rn, op2);
}
pub fn eor(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .eor, 0, rd, rn, op2);
}
pub fn eors(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .eor, 1, rd, rn, op2);
}
pub fn sub(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .sub, 0, rd, rn, op2);
}
pub fn subs(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .sub, 1, rd, rn, op2);
}
pub fn rsb(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .rsb, 0, rd, rn, op2);
}
pub fn rsbs(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .rsb, 1, rd, rn, op2);
}
pub fn add(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .add, 0, rd, rn, op2);
}
pub fn adds(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .add, 1, rd, rn, op2);
}
pub fn adc(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .adc, 0, rd, rn, op2);
}
pub fn adcs(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .adc, 1, rd, rn, op2);
}
pub fn sbc(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .sbc, 0, rd, rn, op2);
}
pub fn sbcs(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .sbc, 1, rd, rn, op2);
}
pub fn rsc(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .rsc, 0, rd, rn, op2);
}
pub fn rscs(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .rsc, 1, rd, rn, op2);
}
pub fn tst(cond: Condition, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .tst, 1, .r0, rn, op2);
}
pub fn teq(cond: Condition, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .teq, 1, .r0, rn, op2);
}
pub fn cmp(cond: Condition, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .cmp, 1, .r0, rn, op2);
}
pub fn cmn(cond: Condition, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .cmn, 1, .r0, rn, op2);
}
pub fn orr(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .orr, 0, rd, rn, op2);
}
pub fn orrs(cond: Condition, rd: Register, rn: Register, op2: Operand) Instruction {
return dataProcessing(cond, .orr, 1, rd, rn, op2);
}
pub fn mov(cond: Condition, rd: Register, op2: Operand) Instruction {
return dataProcessing(cond, .mov, 0, rd, .r0, op2);
}
pub fn movs(cond: Condition, rd: Register, op2: Operand) Instruction {
return dataProcessing(cond, .mov, 1, rd, .r0, op2);
}
pub fn bic(cond: Condition, rd: Register, op2: Operand) Instruction {
return dataProcessing(cond, .bic, 0, rd, rn, op2);
}
pub fn bics(cond: Condition, rd: Register, op2: Operand) Instruction {
return dataProcessing(cond, .bic, 1, rd, rn, op2);
}
pub fn mvn(cond: Condition, rd: Register, op2: Operand) Instruction {
return dataProcessing(cond, .mvn, 0, rd, .r0, op2);
}
pub fn mvns(cond: Condition, rd: Register, op2: Operand) Instruction {
return dataProcessing(cond, .mvn, 1, rd, .r0, op2);
}
// PSR transfer
pub fn mrs(cond: Condition, rd: Register, psr: Psr) Instruction {
return dataProcessing(cond, if (psr == .cpsr) .tst else .cmp, 0, rd, .r15, Operand.reg(.r0, Operand.Shift.none));
}
// Single data transfer
pub const OffsetArgs = struct {
pre_index: bool = true,
positive: bool = true,
offset: Offset,
write_back: bool = false,
};
pub fn ldr(cond: Condition, rd: Register, rn: Register, args: OffsetArgs) Instruction {
return singleDataTransfer(cond, rd, rn, args.offset, args.pre_index, args.positive, 0, args.write_back, 1);
}
pub fn ldrb(cond: Condition, rd: Register, rn: Register, args: OffsetArgs) Instruction {
return singleDataTransfer(cond, rd, rn, args.offset, args.pre_index, args.positive, 1, args.write_back, 1);
}
pub fn str(cond: Condition, rd: Register, rn: Register, args: OffsetArgs) Instruction {
return singleDataTransfer(cond, rd, rn, args.offset, args.pre_index, args.positive, 0, args.write_back, 0);
}
pub fn strb(cond: Condition, rd: Register, rn: Register, args: OffsetArgs) Instruction {
return singleDataTransfer(cond, rd, rn, args.offset, args.pre_index, args.positive, 1, args.write_back, 0);
}
// Block data transfer
pub fn ldmda(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 0, 0, 0, write_back, 1);
}
pub fn ldmdb(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 1, 0, 0, write_back, 1);
}
pub fn ldmib(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 1, 1, 0, write_back, 1);
}
pub fn ldmia(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 0, 1, 0, write_back, 1);
}
pub const ldmfa = ldmda;
pub const ldmea = ldmdb;
pub const ldmed = ldmib;
pub const ldmfd = ldmia;
pub const ldm = ldmia;
pub fn stmda(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 0, 0, 0, write_back, 0);
}
pub fn stmdb(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 1, 0, 0, write_back, 0);
}
pub fn stmib(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 1, 1, 0, write_back, 0);
}
pub fn stmia(cond: Condition, rn: Register, write_back: bool, reg_list: RegisterList) Instruction {
return blockDataTransfer(cond, rn, reg_list, 0, 1, 0, write_back, 0);
}
pub const stmed = stmda;
pub const stmfd = stmdb;
pub const stmfa = stmib;
pub const stmea = stmia;
pub const stm = stmia;
// Branch
pub fn b(cond: Condition, offset: i24) Instruction {
return branch(cond, offset, 0);
}
pub fn bl(cond: Condition, offset: i24) Instruction {
return branch(cond, offset, 1);
}
// Branch and exchange
pub fn bx(cond: Condition, rn: Register) Instruction {
return branchExchange(cond, rn, 0);
}
pub fn blx(cond: Condition, rn: Register) Instruction {
return branchExchange(cond, rn, 1);
}
// Supervisor Call
pub const swi = svc;
pub fn svc(cond: Condition, comment: u24) Instruction {
return supervisorCall(cond, comment);
}
// Breakpoint
pub fn bkpt(imm: u16) Instruction {
return breakpoint(imm);
}
// Aliases
pub fn pop(cond: Condition, args: anytype) Instruction {
if (@typeInfo(@TypeOf(args)) != .Struct) {
@compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args)));
}
if (args.len < 1) {
@compileError("Expected at least one register");
} else if (args.len == 1) {
const reg = args[0];
return ldr(cond, reg, .sp, .{
.pre_index = false,
.positive = true,
.offset = Offset.imm(4),
.write_back = false,
});
} else {
var register_list: u16 = 0;
inline for (args) |arg| {
const reg = @as(Register, arg);
register_list |= @as(u16, 1) << reg.id();
}
return ldm(cond, .sp, true, @bitCast(RegisterList, register_list));
}
}
pub fn push(cond: Condition, args: anytype) Instruction {
if (@typeInfo(@TypeOf(args)) != .Struct) {
@compileError("Expected tuple or struct argument, found " ++ @typeName(@TypeOf(args)));
}
if (args.len < 1) {
@compileError("Expected at least one register");
} else if (args.len == 1) {
const reg = args[0];
return str(cond, reg, .sp, .{
.pre_index = true,
.positive = false,
.offset = Offset.imm(4),
.write_back = true,
});
} else {
var register_list: u16 = 0;
inline for (args) |arg| {
const reg = @as(Register, arg);
register_list |= @as(u16, 1) << reg.id();
}
return stmdb(cond, .sp, true, @bitCast(RegisterList, register_list));
}
}
};
test "serialize instructions" {
const Testcase = struct {
inst: Instruction,
expected: u32,
};
const testcases = [_]Testcase{
.{ // add r0, r0, r0
.inst = Instruction.add(.al, .r0, .r0, Instruction.Operand.reg(.r0, Instruction.Operand.Shift.none)),
.expected = 0b1110_00_0_0100_0_0000_0000_00000000_0000,
},
.{ // mov r4, r2
.inst = Instruction.mov(.al, .r4, Instruction.Operand.reg(.r2, Instruction.Operand.Shift.none)),
.expected = 0b1110_00_0_1101_0_0000_0100_00000000_0010,
},
.{ // mov r0, #42
.inst = Instruction.mov(.al, .r0, Instruction.Operand.imm(42, 0)),
.expected = 0b1110_00_1_1101_0_0000_0000_0000_00101010,
},
.{ // mrs r5, cpsr
.inst = Instruction.mrs(.al, .r5, .cpsr),
.expected = 0b1110_00010_0_001111_0101_000000000000,
},
.{ // ldr r0, [r2, #42]
.inst = Instruction.ldr(.al, .r0, .r2, .{
.offset = Instruction.Offset.imm(42),
}),
.expected = 0b1110_01_0_1_1_0_0_1_0010_0000_000000101010,
},
.{ // str r0, [r3]
.inst = Instruction.str(.al, .r0, .r3, .{
.offset = Instruction.Offset.none,
}),
.expected = 0b1110_01_0_1_1_0_0_0_0011_0000_000000000000,
},
.{ // b #12
.inst = Instruction.b(.al, 12),
.expected = 0b1110_101_0_0000_0000_0000_0000_0000_1100,
},
.{ // bl #-4
.inst = Instruction.bl(.al, -4),
.expected = 0b1110_101_1_1111_1111_1111_1111_1111_1100,
},
.{ // bx lr
.inst = Instruction.bx(.al, .lr),
.expected = 0b1110_0001_0010_1111_1111_1111_0001_1110,
},
.{ // svc #0
.inst = Instruction.svc(.al, 0),
.expected = 0b1110_1111_0000_0000_0000_0000_0000_0000,
},
.{ // bkpt #42
.inst = Instruction.bkpt(42),
.expected = 0b1110_0001_0010_000000000010_0111_1010,
},
.{ // stmdb r9, {r0}
.inst = Instruction.stmdb(.al, .r9, false, .{ .r0 = true }),
.expected = 0b1110_100_1_0_0_0_0_1001_0000000000000001,
},
.{ // ldmea r4!, {r2, r5}
.inst = Instruction.ldmea(.al, .r4, true, .{ .r2 = true, .r5 = true }),
.expected = 0b1110_100_1_0_0_1_1_0100_0000000000100100,
},
};
for (testcases) |case| {
const actual = case.inst.toU32();
testing.expectEqual(case.expected, actual);
}
}
test "aliases" {
const Testcase = struct {
expected: Instruction,
actual: Instruction,
};
const testcases = [_]Testcase{
.{ // pop { r6 }
.actual = Instruction.pop(.al, .{.r6}),
.expected = Instruction.ldr(.al, .r6, .sp, .{
.pre_index = false,
.positive = true,
.offset = Instruction.Offset.imm(4),
.write_back = false,
}),
},
.{ // pop { r1, r5 }
.actual = Instruction.pop(.al, .{ .r1, .r5 }),
.expected = Instruction.ldm(.al, .sp, true, .{ .r1 = true, .r5 = true }),
},
.{ // push { r3 }
.actual = Instruction.push(.al, .{.r3}),
.expected = Instruction.str(.al, .r3, .sp, .{
.pre_index = true,
.positive = false,
.offset = Instruction.Offset.imm(4),
.write_back = true,
}),
},
.{ // push { r0, r2 }
.actual = Instruction.push(.al, .{ .r0, .r2 }),
.expected = Instruction.stmdb(.al, .sp, true, .{ .r0 = true, .r2 = true }),
},
};
for (testcases) |case| {
testing.expectEqual(case.expected.toU32(), case.actual.toU32());
}
}