/*
 * IR - Lightweight JIT Compilation Framework
 * (Aarch64 native code generator based on DynAsm)
 * Copyright (C) 2022 Zend by Perforce.
 * Authors: Dmitry Stogov <dmitry@php.net>
 */

|.arch arm64

|.actionlist dasm_actions
|.globals ir_lb
|.section code, cold_code, rodata, jmp_table

|.define IR_LOOP_ALIGNMENT, 8

#ifdef IR_DEBUG
typedef struct _ir_mem {uint64_t v;} ir_mem;

# define IR_MEM_VAL(loc)            ((loc).v)
#else
typedef uint64_t ir_mem;

# define IR_MEM_VAL(loc)            (loc)
#endif

#define IR_MEM_OFFSET(loc)          ((int32_t)(IR_MEM_VAL(loc) & 0xffffffff))
#define IR_MEM_BASE(loc)            ((ir_reg)((IR_MEM_VAL(loc) >> 32) & 0xff))
#define IR_MEM_INDEX(loc)           ((ir_reg)((IR_MEM_VAL(loc) >> 40) & 0xff))
#define IR_MEM_SHIFT(loc)           ((int32_t)((IR_MEM_VAL(loc) >> 48) & 0xff))

#define IR_MEM_O(addr)            IR_MEM(IR_REG_NONE, addr, IR_REG_NONE, 0)
#define IR_MEM_B(base)            IR_MEM(base, 0, IR_REG_NONE, 0)
#define IR_MEM_BO(base, offset)   IR_MEM(base, offset, IR_REG_NONE, 0)

IR_ALWAYS_INLINE ir_mem IR_MEM(ir_reg base, int32_t offset, ir_reg index, int32_t shift)
{
	ir_mem mem;
	IR_ASSERT(base == IR_REG_NONE || (base >= IR_REG_GP_FIRST && base <= IR_REG_GP_LAST));
	IR_ASSERT(index == IR_REG_NONE || (index >= IR_REG_GP_FIRST && index <= IR_REG_GP_LAST));
	IR_ASSERT(index == IR_REG_NONE || offset == 0);
	IR_ASSERT(shift == 0); // TODO: ???
#ifdef IR_DEBUG
	mem.v =
#else
	mem =
#endif
		((uint64_t)(uint32_t)offset |
		((uint64_t)(uint8_t)base << 32) |
		((uint64_t)(uint8_t)index << 40) |
		((uint64_t)(uint8_t)shift << 48));
	return mem;
}

#define IR_SPILL_POS_TO_OFFSET(offset) \
	((ctx->flags & IR_USE_FRAME_POINTER) ? \
		((offset) + (int32_t)sizeof(void*) * 2) : \
		((offset) + ctx->call_stack_size))

#define B_IMM           (1<<27)        // signed imm26 * 4
#define ADR_IMM         (1<<20)        // signed imm21
#define ADRP_IMM        (1LL<<32)      // signed imm21 * 4096

static bool aarch64_may_use_b(const ir_code_buffer *code_buffer, const void *addr)
{
	if (code_buffer) {
		if (addr >= code_buffer->start && (char*)addr < (char*)code_buffer->end) {
			return (((char*)code_buffer->end - (char*)code_buffer->start) < B_IMM);
		} else if ((char*)addr >= (char*)code_buffer->end) {
			return (((char*)addr - (char*)code_buffer->start) < B_IMM);
		} else if (addr < code_buffer->start) {
			return (((char*)code_buffer->end - (char*)addr) < B_IMM);
		}
	}
	return 0;
}

#if 0
static bool aarch64_may_use_adr(ir_code_buffer *code_buffer, const void *addr)
{
	if (code_buffer) {
		if (addr >= code_buffer->start && (char*)addr < (char*)code_buffer->end) {
			return (((char*)code_buffer->end - (char*)code_buffer->start) < ADR_IMM);
		} else if ((char*)addr >= (char*)code_buffer->end) {
			return (((char*)addr - (char*)code_buffer->start) < ADR_IMM);
		} else if (addr < code_buffer->start) {
			return (((char*)code_buffer->end - (char*)addr) < ADR_IMM);
		}
	}
	return 0;
}

static bool aarch64_may_use_adrp(ir_code_buffer *code_buffer, const void *addr)
{
	if (code_buffer) {
		if (addr >= code_buffer->start && (char*)addr < (char*)code_buffer->end) {
			return (((char*)code_buffer->end - (char*)code_buffer->start) < ADRP_IMM);
		} else if ((char*)addr >= (char*)code_buffer->end) {
			return (((char*)addr - (char*)code_buffer->start) < ADRP_IMM);
		} else if (addr < code_buffer->start) {
			return (((char*)code_buffer->end - (char*)addr) < ADRP_IMM);
		}
	}
	return 0;
}
#endif

/* Determine whether "val" falls into two allowed ranges:
 *   Range 1: [0, 0xfff]
 *   Range 2: LSL #12 to Range 1
 * Used to guard the immediate encoding for add/adds/sub/subs/cmp/cmn instructions. */
static bool aarch64_may_encode_imm12(const int64_t val)
{
	return (val >= 0 && (val <= 0xfff || !(val & 0xffffffffff000fff)));
}

/* Determine whether an immediate value can be encoded as the immediate operand of logical instructions. */
static bool aarch64_may_encode_logical_imm(uint64_t value, uint32_t type_size)
{
	/* fast path: power of two */
	if (value > 0 && !(value & (value - 1))) {
		return 1;
	}

	if (type_size == 8) {
		if (dasm_imm13((uint32_t)value, (uint32_t)(value >> 32)) != -1) {
			return 1;
		}
	} else {
		if (dasm_imm13((uint32_t)value, (uint32_t)value) != -1) {
			return 1;
		}
	}

	return 0;
}

static bool aarch64_may_encode_imm7_addr_offset(const int64_t offset, uint32_t type_size)
{
	return (uintptr_t)(offset) % type_size == 0
		&& offset < 63 * (int32_t)type_size
		&& offset >= -64 * (int32_t)type_size;
}

static bool aarch64_may_encode_addr_offset(int64_t offset, uint32_t type_size)
{
	return (uintptr_t)(offset) % type_size == 0 && (uintptr_t)(offset) < 0xfff * type_size;
}

|.macro ASM_REG_REG_OP, op, type, dst, src
||	if (ir_type_size[type] == 8) {
|		op Rx(dst), Rx(src)
||	} else {
|		op Rw(dst), Rw(src)
||	}
|.endmacro

|.macro ASM_REG_REG_REG_OP, op, type, dst, src1, src2
||	if (ir_type_size[type] == 8) {
|		op Rx(dst), Rx(src1), Rx(src2)
||	} else {
|		op Rw(dst), Rw(src1), Rw(src2)
||	}
|.endmacro

|.macro ASM_REG_REG_REG_TXT_OP, op, type, dst, src1, src2, txt
||	if (ir_type_size[type] == 8) {
|		op Rx(dst), Rx(src1), Rx(src2), txt
||	} else {
|		op Rw(dst), Rw(src1), Rw(src2), txt
||	}
|.endmacro

|.macro ASM_REG_REG_REG_REG_OP, op, type, dst, src1, src2, src3
||	if (ir_type_size[type] == 8) {
|		op Rx(dst), Rx(src1), Rx(src2), Rx(src3)
||	} else {
|		op Rw(dst), Rw(src1), Rw(src2), Rw(src3);
||	}
|.endmacro

|.macro ASM_REG_REG_IMM_OP, op, type, dst, src1, val
||	if (ir_type_size[type] == 8) {
|		op Rx(dst), Rx(src1), #val
||	} else {
|		op Rw(dst), Rw(src1), #val
||	}
|.endmacro

|.macro ASM_REG_IMM_OP, op, type, reg, val
||	if (ir_type_size[type] == 8) {
|		op Rx(reg), #val
||	} else {
|		op Rw(reg), #val
||	}
|.endmacro

|.macro ASM_FP_REG_IMM_OP, op, type, reg, val
||	if (type == IR_DOUBLE) {
|		op Rd(reg-IR_REG_FP_FIRST), #val
||	} else {
||		IR_ASSERT(type == IR_FLOAT);
|		op Rs(reg-IR_REG_FP_FIRST), #val
||	}
|.endmacro

|.macro ASM_FP_REG_REG_REG_OP, op, type, dst, src1, src2
||	if (type == IR_DOUBLE) {
|		op Rd(dst-IR_REG_FP_FIRST), Rd(src1-IR_REG_FP_FIRST), Rd(src2-IR_REG_FP_FIRST)
||	} else {
||		IR_ASSERT(type == IR_FLOAT);
|		op Rs(dst-IR_REG_FP_FIRST), Rs(src1-IR_REG_FP_FIRST), Rs(src2-IR_REG_FP_FIRST)
||	}
|.endmacro

typedef struct _ir_backend_data {
	ir_reg_alloc_data  ra_data;
	dasm_State        *dasm_state;
	ir_bitset          emit_constants;
	int                rodata_label, jmp_table_label;
	bool               resolved_label_syms;
} ir_backend_data;

typedef struct _ir_aarch64_sysv_va_list {
	void    *stack;
	void    *gr_top;
	void    *vr_top;
	int32_t  gr_offset;
	int32_t  vr_offset;
} ir_aarch64_sysv_va_list;

#define IR_GP_REG_NAME(code, name64, name32) \
	#name64,
#define IR_GP_REG_NAME32(code, name64, name32) \
	#name32,
#define IR_FP_REG_NAME(code, name64, name32, name16, name8) \
	#name64,
#define IR_FP_REG_NAME32(code, name64, name32, name16, name8) \
	#name32,

static const char *_ir_reg_name[] = {
	IR_GP_REGS(IR_GP_REG_NAME)
	IR_FP_REGS(IR_FP_REG_NAME)
	"ALL",
	"SCRATCH",
};

static const char *_ir_reg_name32[IR_REG_NUM] = {
	IR_GP_REGS(IR_GP_REG_NAME32)
	IR_FP_REGS(IR_FP_REG_NAME32)
};

const char *ir_reg_name(int8_t reg, ir_type type)
{
	if (reg >= IR_REG_NUM) {
		IR_ASSERT((uint8_t)reg < sizeof(_ir_reg_name) / sizeof(_ir_reg_name[0]));
		return _ir_reg_name[reg];
	}
	IR_ASSERT(reg >= 0 && reg < IR_REG_NUM);
	if (type == IR_VOID) {
		type = (reg < IR_REG_FP_FIRST) ? IR_ADDR : IR_DOUBLE;
	}
	if (ir_type_size[type] == 8) {
		return _ir_reg_name[reg];
	} else {
		return _ir_reg_name32[reg];
	}
}

/* Calling Conventions */
#define IR_REG_SCRATCH_AARCH64         IR_REG_SET_1

#define IR_REGSET_SCRATCH_AARCH64 \
	(IR_REGSET_INTERVAL(IR_REG_X0, IR_REG_X18) | \
	 IR_REGSET_INTERVAL(IR_REG_V0, IR_REG_V7) | \
	 IR_REGSET_INTERVAL(IR_REG_V16, IR_REG_V31))

const ir_regset ir_scratch_regset[] = {
	IR_REGSET_GP | IR_REGSET_FP,
	IR_REGSET_SCRATCH_AARCH64,
};

const ir_call_conv_dsc ir_call_conv_aarch64_sysv = {
	0,           /* cleanup_stack_by_callee */
	0,           /* pass_struct_by_val      */
	1,           /* sysv_varargs            */
	0,           /* shadow_param_regs       */
	0,           /* shadow_store_size       */
	8,           /* int_param_regs_count    */
	8,           /* fp_param_regs_count     */
	IR_REG_X0 ,  /* int_ret_reg             */
	IR_REG_V0,   /* fp_ret_reg              */
	IR_REG_NONE, /* fp_varargs_reg          */
	IR_REG_SCRATCH_AARCH64,
	(const int8_t[8]){IR_REG_X0, IR_REG_X1, IR_REG_X2, IR_REG_X3, IR_REG_X4, IR_REG_X5, IR_REG_X6, IR_REG_X7},
	(const int8_t[8]){IR_REG_V0, IR_REG_V1, IR_REG_V2, IR_REG_V3, IR_REG_V4, IR_REG_V5, IR_REG_V6, IR_REG_V7},
	IR_REGSET_INTERVAL(IR_REG_X19, IR_REG_X30) | IR_REGSET_INTERVAL(IR_REG_V8, IR_REG_V15),

};

const ir_call_conv_dsc ir_call_conv_aarch64_darwin = {
	0,           /* cleanup_stack_by_callee */
	0,           /* pass_struct_by_val      */
	0,           /* sysv_varargs            */
	0,           /* shadow_param_regs       */
	0,           /* shadow_store_size       */
	8,           /* int_param_regs_count    */
	8,           /* fp_param_regs_count     */
	IR_REG_X0 ,  /* int_ret_reg             */
	IR_REG_V0,   /* fp_ret_reg              */
	IR_REG_NONE, /* fp_varargs_reg          */
	IR_REG_SCRATCH_AARCH64,
	(const int8_t[8]){IR_REG_X0, IR_REG_X1, IR_REG_X2, IR_REG_X3, IR_REG_X4, IR_REG_X5, IR_REG_X6, IR_REG_X7},
	(const int8_t[8]){IR_REG_V0, IR_REG_V1, IR_REG_V2, IR_REG_V3, IR_REG_V4, IR_REG_V5, IR_REG_V6, IR_REG_V7},
	IR_REGSET_INTERVAL(IR_REG_X19, IR_REG_X30) | IR_REGSET_INTERVAL(IR_REG_V8, IR_REG_V15),

};

const ir_call_conv_dsc ir_call_conv_aarch64_preserve_none = {
	0,           /* cleanup_stack_by_callee */
	0,           /* pass_struct_by_val      */
	1,           /* sysv_varargs            */
	0,           /* shadow_param_regs       */
	0,           /* shadow_store_size       */
	23,          /* int_param_regs_count    */
	8,           /* fp_param_regs_count     */
	IR_REG_X0 ,  /* int_ret_reg             */
	IR_REG_V0,   /* fp_ret_reg              */
	IR_REG_NONE, /* fp_varargs_reg          */
	IR_REG_ALL,
	(const int8_t[23]){IR_REG_X20, IR_REG_X21, IR_REG_X22, IR_REG_X23, IR_REG_X24, IR_REG_X25, IR_REG_X26, IR_REG_X27,
	                   IR_REG_X28,
	                   IR_REG_X0, IR_REG_X1, IR_REG_X2, IR_REG_X3, IR_REG_X4, IR_REG_X5, IR_REG_X6, IR_REG_X7,
	                   IR_REG_X10, IR_REG_X11, IR_REG_X12, IR_REG_X13, IR_REG_X14, IR_REG_X9},
	(const int8_t[8]){IR_REG_V0, IR_REG_V1, IR_REG_V2, IR_REG_V3, IR_REG_V4, IR_REG_V5, IR_REG_V6, IR_REG_V7},
	IR_REGSET_EMPTY,

};

#ifdef __APPLE__
# define ir_call_conv_default ir_call_conv_aarch64_darwin
#else
# define ir_call_conv_default ir_call_conv_aarch64_sysv
#endif

#define IR_RULES(_)        \
	_(CMP_INT)             \
	_(CMP_FP)              \
	_(MUL_PWR2)            \
	_(DIV_PWR2)            \
	_(MOD_PWR2)            \
	_(SDIV_PWR2)           \
	_(SMOD_PWR2)           \
	_(OP_INT)              \
	_(OP_FP)               \
	_(BINOP_INT)           \
	_(BINOP_FP)            \
	_(SHIFT)               \
	_(SHIFT_CONST)         \
	_(COPY_INT)            \
	_(COPY_FP)             \
	_(CMP_AND_BRANCH_INT)  \
	_(CMP_AND_BRANCH_FP)   \
	_(GUARD_CMP_INT)       \
	_(GUARD_CMP_FP)        \
	_(GUARD_OVERFLOW)      \
	_(OVERFLOW_AND_BRANCH) \
	_(MIN_MAX_INT)         \
	_(REG_BINOP_INT)       \
	_(LOAD_INT)            \
	_(LOAD_FP)             \
	_(STORE_INT)           \
	_(STORE_FP)            \
	_(IF_INT)              \
	_(RETURN_VOID)         \
	_(RETURN_INT)          \
	_(RETURN_FP)           \
	_(IGOTO_DUP)           \

#define IR_RULE_ENUM(name) IR_ ## name,

#define IR_STATIC_ALLOCA   (IR_SKIPPED | IR_FUSED | IR_SIMPLE | IR_ALLOCA)

enum _ir_rule {
	IR_FIRST_RULE = IR_LAST_OP,
	IR_RULES(IR_RULE_ENUM)
	IR_LAST_RULE
};

#define IR_RULE_NAME(name)  #name,
const char *ir_rule_name[IR_LAST_OP] = {
	NULL,
	IR_RULES(IR_RULE_NAME)
	NULL
};

/* register allocation */
int ir_get_target_constraints(ir_ctx *ctx, ir_ref ref, ir_target_constraints *constraints)
{
	uint32_t rule = ir_rule(ctx, ref);
	const ir_insn *insn;
	int n = 0;
	int flags = IR_USE_MUST_BE_IN_REG | IR_OP1_MUST_BE_IN_REG | IR_OP2_MUST_BE_IN_REG | IR_OP3_MUST_BE_IN_REG;
	const ir_proto_t *proto;
	const ir_call_conv_dsc *cc;
	ir_ref next;

	constraints->def_reg = IR_REG_NONE;
	constraints->hints_count = 0;
	switch (rule & IR_RULE_MASK) {
		case IR_BINOP_INT:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else if (ir_rule(ctx, insn->op1) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op2) && insn->op1 != insn->op2) {
				const ir_insn *val_insn = &ctx->ir_base[insn->op2];
				switch (insn->op) {
					case IR_ADD:
					case IR_ADD_OV:
					case IR_SUB:
					case IR_SUB_OV:
						if (IR_IS_SYM_CONST(val_insn->op) || !aarch64_may_encode_imm12(val_insn->val.u64)) {
							constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
							n++;
						}
						break;
					case IR_MUL_OV:
						constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_SAVE_SUB_REF);
						n++;
						break;
					case IR_AND:
					case IR_OR:
					case IR_XOR:
						if (IR_IS_SYM_CONST(val_insn->op) || !aarch64_may_encode_logical_imm(val_insn->val.u64, ir_type_size[insn->type])) {
							constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
							n++;
						}
						break;
					case IR_MUL:
					case IR_DIV:
					case IR_MOD:
						constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
						n++;
						break;
				}
			} else if (!IR_IS_CONST_REF(insn->op2) && ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (insn->op == IR_MOD) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else if (insn->op == IR_MUL_OV && (ir_type_size[insn->type] == 8 || IR_IS_TYPE_SIGNED(insn->type))) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_SAVE_SUB_REF);
				n++;
			}
			break;
		case IR_INT2FP:
		case IR_FP2INT:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, ctx->ir_base[insn->op1].type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_MUL_PWR2:
		case IR_DIV_PWR2:
		case IR_MOD_PWR2:
		case IR_SHIFT:
		case IR_SHIFT_CONST:
		case IR_OP_INT:
		case IR_OP_FP:
		case IR_FP2FP:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (rule == IR_SHIFT_CONST
			 && (insn->op == IR_ROL || insn->op == IR_ROR)
			 && ir_type_size[insn->type] < 4) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else if (rule == IR_SHIFT
			 && (insn->op == IR_ROL || insn->op == IR_ROR)
			 && ir_type_size[insn->type] < 4) {
				if (insn->op == IR_ROL) {
					flags |= IR_DEF_CONFLICTS_WITH_INPUT_REGS;
				}
				constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_SAVE_SUB_REF);
				n++;
			} else if (rule == IR_SHIFT && insn->op == IR_ROL) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_SDIV_PWR2:
			flags = IR_DEF_CONFLICTS_WITH_INPUT_REGS | IR_USE_MUST_BE_IN_REG | IR_OP1_MUST_BE_IN_REG;
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op2)) {
				int64_t offset = ctx->ir_base[insn->op2].val.u64 - 1;
				if (!aarch64_may_encode_imm12(offset)) {
					constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
					n++;
				}
			}
			break;
		case IR_SMOD_PWR2:
			flags = IR_USE_MUST_BE_IN_REG | IR_OP1_SHOULD_BE_IN_REG;
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_USE_SUB_REF, IR_SAVE_SUB_REF);
			n++;
			break;
		case IR_CTPOP:
			flags = IR_USE_MUST_BE_IN_REG | IR_OP1_MUST_BE_IN_REG;
			insn = &ctx->ir_base[ref];
			constraints->tmp_regs[0] = IR_TMP_REG(2, IR_DOUBLE, IR_USE_SUB_REF, IR_SAVE_SUB_REF);
			n = 1;
			break;
		case IR_BINOP_FP:
		case IR_MIN_MAX_INT:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else if (ir_rule(ctx, insn->op1) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op2) && insn->op1 != insn->op2) {
				constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else if (!IR_IS_CONST_REF(insn->op2) && ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_CMP_INT:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else if (ir_rule(ctx, insn->op1) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[0] = IR_TMP_REG(1, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n = 1;
			}
			if (IR_IS_CONST_REF(insn->op2)) {
				if (insn->op1 != insn->op2) {
					insn = &ctx->ir_base[insn->op2];
					if (IR_IS_SYM_CONST(insn->op) || !aarch64_may_encode_imm12(insn->val.u64)) {
						constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
						n++;
					}
				}
			} else if (ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_CMP_FP:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				const ir_insn *val_insn = &ctx->ir_base[insn->op1];
				constraints->tmp_regs[n] = IR_TMP_REG(1, val_insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op2) && insn->op1 != insn->op2) {
				const ir_insn *val_insn = &ctx->ir_base[insn->op2];
				constraints->tmp_regs[n] = IR_TMP_REG(2, val_insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_VSTORE:
		case IR_VSTORE_v:
			insn = &ctx->ir_base[ref];
			if (IR_IS_CONST_REF(insn->op3)) {
				insn = &ctx->ir_base[insn->op3];
				constraints->tmp_regs[0] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n = 1;
			} else if (ir_rule(ctx, insn->op3) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[0] = IR_TMP_REG(3, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n = 1;
			}
			break;
		case IR_LOAD_FP:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op2)) {
				IR_ASSERT(ctx->ir_base[insn->op2].type == IR_ADDR);
				constraints->tmp_regs[n] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_STORE_INT:
		case IR_STORE_FP:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op2)) {
				IR_ASSERT(ctx->ir_base[insn->op2].type == IR_ADDR);
				constraints->tmp_regs[n] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op3)) {
				insn = &ctx->ir_base[insn->op3];
				if (!IR_IS_TYPE_INT(insn->type) || IR_IS_SYM_CONST(insn->op) || insn->val.i64 != 0) {
					constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
					n++;
				}
			} else if (ir_rule(ctx, insn->op3) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_SWITCH:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op2)) {
				insn = &ctx->ir_base[insn->op2];
				constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			} else {
				insn = &ctx->ir_base[insn->op2];
				constraints->tmp_regs[n] = IR_TMP_REG(1, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			constraints->tmp_regs[n] = IR_TMP_REG(3, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
			n++;
			break;
		case IR_ARGVAL:
			/* memcpy() clobbers all scratch registers */
			constraints->tmp_regs[0] = IR_SCRATCH_REG(IR_REG_SCRATCH_AARCH64, IR_DEF_SUB_REF - IR_SUB_REFS_COUNT, IR_USE_SUB_REF);
			n = 1;
			break;
		case IR_CALL:
			insn = &ctx->ir_base[ref];
			proto = ir_call_proto(ctx, insn);
			cc = ir_get_call_conv_dsc(proto ? proto->flags : IR_CC_DEFAULT);
			if (insn->type != IR_VOID) {
				constraints->def_reg = (IR_IS_TYPE_INT(insn->type)) ?
					cc->int_ret_reg : cc->fp_ret_reg;
			}
			constraints->tmp_regs[0] = IR_SCRATCH_REG(cc->scratch_reg, IR_USE_SUB_REF, IR_DEF_SUB_REF);
			n = 1;
			if (insn->inputs_count > 2) {
				goto get_arg_hints;
			}
			flags = IR_USE_SHOULD_BE_IN_REG | IR_OP2_MUST_BE_IN_REG | IR_OP3_SHOULD_BE_IN_REG;
			break;
		case IR_TAILCALL:
			insn = &ctx->ir_base[ref];
			if (insn->inputs_count > 2) {
				proto = ir_call_proto(ctx, insn);
				cc = ir_get_call_conv_dsc(proto ? proto->flags : IR_CC_DEFAULT);
get_arg_hints:
				constraints->hints[2] = IR_REG_NONE;
				constraints->hints_count = ir_get_args_regs(ctx, insn, cc, constraints->hints);
				if (!IR_IS_CONST_REF(insn->op2)) {
					constraints->tmp_regs[n] = IR_TMP_REG(1, IR_ADDR, IR_LOAD_SUB_REF, IR_USE_SUB_REF);
					n++;
				}
			}
			flags = IR_USE_SHOULD_BE_IN_REG | IR_OP2_MUST_BE_IN_REG | IR_OP3_SHOULD_BE_IN_REG;
			break;
		case IR_IGOTO:
			insn = &ctx->ir_base[ref];
			if (ctx->ir_base[insn->op1].op == IR_MERGE || ctx->ir_base[insn->op1].op == IR_LOOP_BEGIN) {
				ir_insn *merge = &ctx->ir_base[insn->op1];
				ir_ref *p, n = merge->inputs_count;

				for (p = merge->ops + 1; n > 0; p++, n--) {
					ir_ref input = *p;
					IR_ASSERT(ctx->ir_base[input].op == IR_END || ctx->ir_base[input].op == IR_LOOP_END);
					ctx->rules[input] = IR_IGOTO_DUP;
				}
			}
			return insn->op;
		case IR_COND:
			insn = &ctx->ir_base[ref];
			n = 0;
			if (IR_IS_CONST_REF(insn->op1)) {
				constraints->tmp_regs[n] = IR_TMP_REG(1, ctx->ir_base[insn->op1].type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op2) || ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(2, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op3) || ir_rule(ctx, insn->op3) == IR_STATIC_ALLOCA) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, insn->type, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
		case IR_COPY_INT:
		case IR_COPY_FP:
		case IR_TRUNC:
		case IR_BITCAST:
		case IR_PROTO:
			flags = IR_USE_MUST_BE_IN_REG | IR_OP1_SHOULD_BE_IN_REG;
			break;
		case IR_ZEXT:
		case IR_SEXT:
			flags = IR_DEF_REUSES_OP1_REG | IR_USE_MUST_BE_IN_REG;
			break;
		case IR_PARAM:
			constraints->def_reg = ir_get_param_reg(ctx, ref);
			flags = (constraints->def_reg != IR_REG_NONE) ? IR_USE_SHOULD_BE_IN_REG : 0;
			break;
		case IR_PI:
		case IR_PHI:
			flags = IR_USE_SHOULD_BE_IN_REG;
			break;
		case IR_RLOAD:
			constraints->def_reg = ctx->ir_base[ref].op2;
			flags = IR_USE_SHOULD_BE_IN_REG;
			break;
		case IR_EXITCALL:
			cc = ir_get_call_conv_dsc(ctx->flags);
			constraints->def_reg = cc->int_ret_reg;
			break;
		case IR_RSTORE:
			flags = IR_OP3_SHOULD_BE_IN_REG;
			break;
		case IR_RETURN_INT:
			cc = ir_get_call_conv_dsc(ctx->flags);
			flags = IR_OP2_SHOULD_BE_IN_REG;
			constraints->hints[2] = cc->int_ret_reg;
			constraints->hints_count = 3;
			break;
		case IR_RETURN_FP:
			cc = ir_get_call_conv_dsc(ctx->flags);
			flags = IR_OP2_SHOULD_BE_IN_REG;
			constraints->hints[2] = cc->fp_ret_reg;
			constraints->hints_count = 3;
			break;
		case IR_SNAPSHOT:
			flags = 0;
			next = ir_next_control(ctx, ref);
			if (ctx->ir_base[next].op == IR_GUARD || ctx->ir_base[next].op == IR_GUARD_NOT) {
				flags = IR_EXTEND_INPUTS_TO_NEXT;
			}
			break;
		case IR_VA_START:
			flags = IR_OP2_MUST_BE_IN_REG;
			constraints->tmp_regs[0] = IR_TMP_REG(3, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
			n = 1;
			insn = &ctx->ir_base[ref];
			if (IR_IS_CONST_REF(insn->op2)) {
				constraints->tmp_regs[1] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n = 2;
			}
			break;
		case IR_VA_ARG:
			flags = IR_USE_MUST_BE_IN_REG | IR_OP2_MUST_BE_IN_REG | IR_DEF_CONFLICTS_WITH_INPUT_REGS;
			constraints->tmp_regs[0] = IR_TMP_REG(3, IR_ADDR, IR_LOAD_SUB_REF, IR_SAVE_SUB_REF);
			n = 1;
			insn = &ctx->ir_base[ref];
			if (IR_IS_CONST_REF(insn->op2)) {
				constraints->tmp_regs[1] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n = 2;
			}
			break;
		case IR_VA_COPY:
			flags = IR_OP2_MUST_BE_IN_REG | IR_OP3_MUST_BE_IN_REG;
			constraints->tmp_regs[0] = IR_TMP_REG(1, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
			n = 1;
			insn = &ctx->ir_base[ref];
			if (IR_IS_CONST_REF(insn->op2)) {
				constraints->tmp_regs[n] = IR_TMP_REG(2, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			if (IR_IS_CONST_REF(insn->op3)) {
				constraints->tmp_regs[n] = IR_TMP_REG(3, IR_ADDR, IR_LOAD_SUB_REF, IR_DEF_SUB_REF);
				n++;
			}
			break;
	}
	constraints->tmps_count = n;

	return flags;
}

/* instruction selection */
static void ir_match_fuse_addr(ir_ctx *ctx, ir_ref addr_ref, ir_type type)
{
	if (!IR_IS_CONST_REF(addr_ref)) {
		ir_insn *addr_insn = &ctx->ir_base[addr_ref];

		if (addr_insn->op == IR_ADD
		 && !IR_IS_CONST_REF(addr_insn->op1)
		 && IR_IS_CONST_REF(addr_insn->op2)  // TODO: temporary workaround
		 && !IR_IS_SYM_CONST(ctx->ir_base[addr_insn->op2].op)
		 && aarch64_may_encode_addr_offset(ctx->ir_base[addr_insn->op2].val.i64, ir_type_size[type])) {
			ir_use_list *use_list = &ctx->use_lists[addr_ref];
			ir_ref j = use_list->count;

			if (j > 1) {
				/* check if address is used only in LOAD and STORE */
				ir_ref *p = &ctx->use_edges[use_list->refs];

				do {
					ir_insn *insn = &ctx->ir_base[*p];
					if (insn->op != IR_LOAD && insn->op != IR_LOAD_v
					 && ((insn->op != IR_STORE && insn->op != IR_STORE_v) || insn->op3 == addr_ref)) {
						return;
					}
					p++;
				} while (--j);
			}
			ctx->rules[addr_ref] = IR_FUSED | IR_SIMPLE | addr_insn->op;
		}
	}
}

static bool all_usages_are_fusable(ir_ctx *ctx, ir_ref ref)
{
	ir_insn *insn = &ctx->ir_base[ref];

	if (insn->op >= IR_EQ && insn->op <= IR_UNORDERED) {
		ir_use_list *use_list = &ctx->use_lists[ref];
		ir_ref n = use_list->count;

		if (n > 0) {
			ir_ref *p = ctx->use_edges + use_list->refs;

			do {
				insn = &ctx->ir_base[*p];
				if (insn->op != IR_IF
				 && insn->op != IR_GUARD
				 && insn->op != IR_GUARD_NOT) {
					return 0;
				}
				p++;
				n--;
			} while (n);
			return 1;
		}
	}
	return 0;
}


static uint32_t ir_match_insn(ir_ctx *ctx, ir_ref ref)
{
	ir_insn *op2_insn;
	ir_insn *insn = &ctx->ir_base[ref];

	switch (insn->op) {
		case IR_EQ:
		case IR_NE:
		case IR_LT:
		case IR_GE:
		case IR_LE:
		case IR_GT:
		case IR_ULT:
		case IR_UGE:
		case IR_ULE:
		case IR_UGT:
			if (IR_IS_TYPE_INT(ctx->ir_base[insn->op1].type)) {
				return IR_CMP_INT;
			} else {
				return IR_CMP_FP;
			}
			break;
		case IR_ORDERED:
		case IR_UNORDERED:
			return IR_CMP_FP;
		case IR_ADD:
		case IR_SUB:
			if (IR_IS_TYPE_INT(insn->type)) {
				if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
					op2_insn = &ctx->ir_base[insn->op2];
					if (IR_IS_SYM_CONST(op2_insn->op)) {
						/* pass */
					} else if (IR_IS_CONST_REF(insn->op1)) {
						// const
					} else if (op2_insn->val.i64 == 0) {
						// return IR_COPY_INT;
					}
				}
binop_int:
				return IR_BINOP_INT;
			} else {
binop_fp:
				return IR_BINOP_FP;
			}
			break;
		case IR_MUL:
			if (IR_IS_TYPE_INT(insn->type)) {
				if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
					op2_insn = &ctx->ir_base[insn->op2];
					if (IR_IS_SYM_CONST(op2_insn->op)) {
						/* pass */
					} else if (IR_IS_CONST_REF(insn->op1)) {
						// const
					} else if (op2_insn->val.u64 == 0) {
						// 0
					} else if (op2_insn->val.u64 == 1) {
						// return IR_COPY_INT;
					} else if (IR_IS_POWER_OF_TWO(op2_insn->val.u64)) {
						return IR_MUL_PWR2;
					}
				}
				return IR_BINOP_INT;
			} else {
				goto binop_fp;
			}
			break;
		case IR_ADD_OV:
		case IR_SUB_OV:
			IR_ASSERT(IR_IS_TYPE_INT(insn->type));
			goto binop_int;
		case IR_MUL_OV:
			IR_ASSERT(IR_IS_TYPE_INT(insn->type));
			goto binop_int;
		case IR_DIV:
			if (IR_IS_TYPE_INT(insn->type)) {
				if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
					op2_insn = &ctx->ir_base[insn->op2];
					if (IR_IS_SYM_CONST(op2_insn->op)) {
						/* pass */
					} else if (IR_IS_CONST_REF(insn->op1)) {
						// const
					} else if (op2_insn->val.u64 == 1) {
						// return IR_COPY_INT;
					} else if (IR_IS_POWER_OF_TWO(op2_insn->val.u64)) {
						if (IR_IS_TYPE_UNSIGNED(insn->type)) {
							return IR_DIV_PWR2;
						} else {
							return IR_SDIV_PWR2;
						}
					}
				}
				return IR_BINOP_INT;
			} else {
				goto binop_fp;
			}
			break;
		case IR_MOD:
			if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
				op2_insn = &ctx->ir_base[insn->op2];
				if (IR_IS_SYM_CONST(op2_insn->op)) {
					/* pass */
				} else if (IR_IS_CONST_REF(insn->op1)) {
					// const
				} else if (IR_IS_POWER_OF_TWO(op2_insn->val.u64)) {
					if (IR_IS_TYPE_UNSIGNED(insn->type)) {
						return IR_MOD_PWR2;
					} else {
						return IR_SMOD_PWR2;
					}
				}
			}
			return IR_BINOP_INT;
		case IR_BSWAP:
		case IR_NOT:
		case IR_CTLZ:
		case IR_CTTZ:
			IR_ASSERT(IR_IS_TYPE_INT(insn->type));
			return IR_OP_INT;
		case IR_NEG:
		case IR_ABS:
			if (IR_IS_TYPE_INT(insn->type)) {
				return IR_OP_INT;
			} else {
				return IR_OP_FP;
			}
		case IR_OR:
			if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
				op2_insn = &ctx->ir_base[insn->op2];
				if (IR_IS_SYM_CONST(op2_insn->op)) {
					/* pass */
				} else if (IR_IS_CONST_REF(insn->op1)) {
					// const
				} else if (op2_insn->val.i64 == 0) {
					// return IR_COPY_INT;
				} else if (op2_insn->val.i64 == -1) {
					// -1
				}
			}
			goto binop_int;
		case IR_AND:
			if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
				op2_insn = &ctx->ir_base[insn->op2];
				if (IR_IS_SYM_CONST(op2_insn->op)) {
					/* pass */
				} else if (IR_IS_CONST_REF(insn->op1)) {
					// const
				} else if (op2_insn->val.i64 == 0) {
					// 0
				} else if (op2_insn->val.i64 == -1) {
					// return IR_COPY_INT;
				}
			}
			goto binop_int;
		case IR_XOR:
			if ((ctx->flags & IR_OPT_CODEGEN) && IR_IS_CONST_REF(insn->op2)) {
				op2_insn = &ctx->ir_base[insn->op2];
				if (IR_IS_SYM_CONST(op2_insn->op)) {
					/* pass */
				} else if (IR_IS_CONST_REF(insn->op1)) {
					// const
				}
			}
			goto binop_int;
		case IR_SHL:
			if (IR_IS_CONST_REF(insn->op2)) {
				if (ctx->flags & IR_OPT_CODEGEN) {
					op2_insn = &ctx->ir_base[insn->op2];
					if (IR_IS_SYM_CONST(op2_insn->op)) {
						/* pass */
					} else if (IR_IS_CONST_REF(insn->op1)) {
						// const
					} else if (op2_insn->val.u64 == 0) {
						// return IR_COPY_INT;
					} else if (ir_type_size[insn->type] >= 4) {
						if (op2_insn->val.u64 == 1) {
							// lea [op1*2]
						} else if (op2_insn->val.u64 == 2) {
							// lea [op1*4]
						} else if (op2_insn->val.u64 == 3) {
							// lea [op1*8]
						}
					}
				}
				return IR_SHIFT_CONST;
			}
			return IR_SHIFT;
		case IR_SHR:
		case IR_SAR:
		case IR_ROL:
		case IR_ROR:
			if (IR_IS_CONST_REF(insn->op2)) {
				if (ctx->flags & IR_OPT_CODEGEN) {
					op2_insn = &ctx->ir_base[insn->op2];
					if (IR_IS_SYM_CONST(op2_insn->op)) {
						/* pass */
					} else if (IR_IS_CONST_REF(insn->op1)) {
						// const
					} else if (op2_insn->val.u64 == 0) {
						// return IR_COPY_INT;
					}
				}
				return IR_SHIFT_CONST;
			}
			return IR_SHIFT;
		case IR_MIN:
		case IR_MAX:
			if (IR_IS_TYPE_INT(insn->type)) {
				return IR_MIN_MAX_INT;
			} else {
				goto binop_fp;
			}
			break;
//		case IR_COND:
		case IR_COPY:
			if (IR_IS_TYPE_INT(insn->type)) {
				return IR_COPY_INT | IR_MAY_REUSE;
			} else {
				return IR_COPY_FP | IR_MAY_REUSE;
			}
			break;
//		case IR_TRUNC:
		case IR_PROTO:
			return insn->op | IR_MAY_REUSE;
		case IR_BITCAST:
			if (IR_IS_TYPE_INT(insn->type) && IR_IS_TYPE_INT(ctx->ir_base[insn->op1].type)) {
				return insn->op | IR_MAY_REUSE;
			} else {
				return insn->op;
			}
		case IR_CALL:
			if (ctx->flags & IR_FUNCTION) {
				ctx->flags |= IR_USE_FRAME_POINTER;
			}
			ctx->flags2 |= IR_HAS_CALLS;
			return IR_CALL;
		case IR_VAR:
			return IR_STATIC_ALLOCA;
		case IR_PARAM:
			return ctx->use_lists[ref].count > 0 ? IR_PARAM : IR_SKIPPED | IR_PARAM;
		case IR_ALLOCA:
			if (ctx->flags & IR_FUNCTION) {
				if (IR_IS_CONST_REF(insn->op2) && ctx->cfg_map[ref] == 1) {
					ir_insn *val = &ctx->ir_base[insn->op2];

					if (!IR_IS_SYM_CONST(val->op)) {
						return IR_STATIC_ALLOCA;
					}
				}
				ctx->flags |= IR_USE_FRAME_POINTER;
				ctx->flags2 |= IR_HAS_ALLOCA;
			}
			return IR_ALLOCA;
		case IR_LOAD:
		case IR_LOAD_v:
			ir_match_fuse_addr(ctx, insn->op2, insn->type);
			if (IR_IS_TYPE_INT(insn->type)) {
				return IR_LOAD_INT;
			} else {
				return IR_LOAD_FP;
			}
			break;
		case IR_STORE:
		case IR_STORE_v:
			ir_match_fuse_addr(ctx, insn->op2, ctx->ir_base[insn->op3].type);
			if (IR_IS_TYPE_INT(ctx->ir_base[insn->op3].type)) {
				return IR_STORE_INT;
			} else {
				return IR_STORE_FP;
			}
			break;
		case IR_RLOAD:
			if (IR_REGSET_IN(IR_REGSET_UNION((ir_regset)ctx->fixed_regset, IR_REGSET_FIXED), insn->op2)) {
				return IR_SKIPPED | IR_RLOAD;
			}
			return IR_RLOAD;
		case IR_RSTORE:
			if (IR_IS_TYPE_INT(ctx->ir_base[insn->op2].type)) {
				if ((ctx->flags & IR_OPT_CODEGEN) && ir_in_same_block(ctx, insn->op2) && ctx->use_lists[insn->op2].count == 1) {
					ir_insn *op_insn = &ctx->ir_base[insn->op2];

					if (!ctx->rules[insn->op2]) {
						ctx->rules[insn->op2] = ir_match_insn(ctx, insn->op2);
					}
					if (ctx->rules[insn->op2] == IR_BINOP_INT) {
						if (ctx->ir_base[op_insn->op1].op == IR_RLOAD
						 && ctx->ir_base[op_insn->op1].op2 == insn->op3) {
							ctx->rules[insn->op2] = IR_FUSED | IR_BINOP_INT;
							ctx->rules[op_insn->op1] = IR_SKIPPED | IR_RLOAD;
							return IR_REG_BINOP_INT;
						} else if ((ir_op_flags[op_insn->op] & IR_OP_FLAG_COMMUTATIVE)
						 && ctx->ir_base[op_insn->op2].op == IR_RLOAD
						 && ctx->ir_base[op_insn->op2].op2 == insn->op3) {
							SWAP_REFS(op_insn->op1, op_insn->op2);
							ctx->rules[insn->op2] = IR_FUSED | IR_BINOP_INT;
							ctx->rules[op_insn->op1] = IR_SKIPPED | IR_RLOAD;
							return IR_REG_BINOP_INT;
						}
					}
				}
			}
			return IR_RSTORE;
		case IR_START:
		case IR_BEGIN:
		case IR_IF_TRUE:
		case IR_IF_FALSE:
		case IR_CASE_VAL:
		case IR_CASE_RANGE:
		case IR_CASE_DEFAULT:
		case IR_MERGE:
		case IR_LOOP_BEGIN:
		case IR_UNREACHABLE:
			return IR_SKIPPED | insn->op;
		case IR_RETURN:
			if (!insn->op2) {
				return IR_RETURN_VOID;
			} else if (IR_IS_TYPE_INT(ctx->ir_base[insn->op2].type)) {
				return IR_RETURN_INT;
			} else {
				return IR_RETURN_FP;
			}
		case IR_IF:
			if (!IR_IS_CONST_REF(insn->op2) && (ctx->use_lists[insn->op2].count == 1 || all_usages_are_fusable(ctx, insn->op2))) {
				op2_insn = &ctx->ir_base[insn->op2];
				if (op2_insn->op >= IR_EQ && op2_insn->op <= IR_UNORDERED) {
					if (IR_IS_TYPE_INT(ctx->ir_base[op2_insn->op1].type)) {
						ctx->rules[insn->op2] = IR_FUSED | IR_CMP_INT;
						return IR_CMP_AND_BRANCH_INT;
					} else {
						ctx->rules[insn->op2] = IR_FUSED | IR_CMP_FP;
						return IR_CMP_AND_BRANCH_FP;
					}
				} else if (op2_insn->op == IR_OVERFLOW && ir_in_same_block(ctx, insn->op2)) {
					ctx->rules[insn->op2] = IR_FUSED | IR_SIMPLE | IR_OVERFLOW;
					return IR_OVERFLOW_AND_BRANCH;
				}
			}
			if (IR_IS_TYPE_INT(ctx->ir_base[insn->op2].type)) {
				return IR_IF_INT;
			} else {
				IR_ASSERT(0 && "NIY IR_IF_FP");
				break;
			}
		case IR_GUARD:
		case IR_GUARD_NOT:
			if (!IR_IS_CONST_REF(insn->op2) && (ctx->use_lists[insn->op2].count == 1 || all_usages_are_fusable(ctx, insn->op2))) {
				op2_insn = &ctx->ir_base[insn->op2];
				if (op2_insn->op >= IR_EQ && op2_insn->op <= IR_UNORDERED) {
					if (IR_IS_TYPE_INT(ctx->ir_base[op2_insn->op1].type)) {
						ctx->rules[insn->op2] = IR_FUSED | IR_CMP_INT;
						return IR_GUARD_CMP_INT;
					} else {
						ctx->rules[insn->op2] = IR_FUSED | IR_CMP_FP;
						return IR_GUARD_CMP_FP;
					}
				} else if (op2_insn->op == IR_OVERFLOW && ir_in_same_block(ctx, insn->op2)) {
					ctx->rules[insn->op2] = IR_FUSED | IR_SIMPLE | IR_OVERFLOW;
					return IR_GUARD_OVERFLOW;
				}
			}
			return insn->op;
		case IR_VA_START:
			ctx->flags2 |= IR_HAS_VA_START;
			if ((ctx->ir_base[insn->op2].op == IR_ALLOCA) || (ctx->ir_base[insn->op2].op == IR_VADDR)) {
				ir_use_list *use_list = &ctx->use_lists[insn->op2];
				ir_ref *p, n = use_list->count;
				for (p = &ctx->use_edges[use_list->refs]; n > 0; p++, n--) {
					ir_insn *use_insn = &ctx->ir_base[*p];
					if (use_insn->op == IR_VA_START || use_insn->op == IR_VA_END) {
					} else if (use_insn->op == IR_VA_COPY) {
						if (use_insn->op3 == insn->op2) {
							ctx->flags2 |= IR_HAS_VA_COPY;
						}
					} else if (use_insn->op == IR_VA_ARG) {
						if (use_insn->op2 == insn->op2) {
							if (IR_IS_TYPE_INT(use_insn->type)) {
								ctx->flags2 |= IR_HAS_VA_ARG_GP;
							} else {
								IR_ASSERT(IR_IS_TYPE_FP(use_insn->type));
								ctx->flags2 |= IR_HAS_VA_ARG_FP;
							}
						}
					} else if (*p > ref) {
						/* diriect va_list access */
						ctx->flags2 |= IR_HAS_VA_ARG_GP|IR_HAS_VA_ARG_FP;
					}
				}
			} else {
				/* va_list may escape */
				ctx->flags2 |= IR_HAS_VA_ARG_GP|IR_HAS_VA_ARG_FP;
			}
			return IR_VA_START;
		case IR_VA_END:
			return IR_SKIPPED | IR_NOP;
		case IR_VADDR:
			if (ctx->use_lists[ref].count > 0) {
				ir_use_list *use_list = &ctx->use_lists[ref];
				ir_ref *p, n = use_list->count;

				for (p = &ctx->use_edges[use_list->refs]; n > 0; p++, n--) {
					if (ctx->ir_base[*p].op != IR_VA_END) {
						return IR_STATIC_ALLOCA;
					}
				}
			}
			return IR_SKIPPED | IR_NOP;
		case IR_ARGVAL:
			return IR_FUSED | IR_ARGVAL;
		case IR_NOP:
			return IR_SKIPPED | IR_NOP;
		case IR_ASM:
		case IR_ASM_OUT:
		case IR_ASM_GOTO:
			fprintf(stderr, "ERROR: IR_ASM is not implemented yet\n");
			exit(1);
			return IR_SKIPPED | IR_NOP;
		default:
			break;
	}

	return insn->op;
}

static void ir_match_insn2(ir_ctx *ctx, ir_ref ref, uint32_t rule)
{
}

/* code generation */
static int32_t ir_ref_spill_slot_offset(ir_ctx *ctx, ir_ref ref, ir_reg *reg)
{
	int32_t offset;

	IR_ASSERT(ref >= 0);
	offset = ctx->live_intervals[ctx->vregs[ref]]->stack_spill_pos;
	IR_ASSERT(offset != -1);
	if (ctx->live_intervals[ctx->vregs[ref]]->flags & IR_LIVE_INTERVAL_SPILL_SPECIAL) {
		IR_ASSERT(ctx->spill_base != IR_REG_NONE);
		*reg = ctx->spill_base;
		return offset;
	}
	*reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
	return IR_SPILL_POS_TO_OFFSET(offset);
}

static ir_mem ir_vreg_spill_slot(ir_ctx *ctx, ir_ref v)
{
	int32_t offset;
	ir_reg base;

	IR_ASSERT(v > 0 && v <= ctx->vregs_count && ctx->live_intervals[v]);
	offset = ctx->live_intervals[v]->stack_spill_pos;
	IR_ASSERT(offset != -1);
	if (ctx->live_intervals[v]->flags & IR_LIVE_INTERVAL_SPILL_SPECIAL) {
		IR_ASSERT(ctx->spill_base != IR_REG_NONE);
		return IR_MEM_BO(ctx->spill_base, offset);
	}
	base = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
	offset = IR_SPILL_POS_TO_OFFSET(offset);
	return IR_MEM_BO(base, offset);
}

static ir_mem ir_ref_spill_slot(ir_ctx *ctx, ir_ref ref)
{
	IR_ASSERT(!IR_IS_CONST_REF(ref));
	return ir_vreg_spill_slot(ctx, ctx->vregs[ref]);
}

static bool ir_is_same_spill_slot(ir_ctx *ctx, ir_ref ref, ir_mem mem)
{
	return IR_MEM_VAL(ir_ref_spill_slot(ctx, ref)) == IR_MEM_VAL(mem);
}

static int32_t ir_var_spill_slot(ir_ctx *ctx, ir_ref ref, ir_reg *reg)
{
	ir_insn *var_insn = &ctx->ir_base[ref];

	IR_ASSERT(var_insn->op == IR_VAR);
	*reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
	return IR_SPILL_POS_TO_OFFSET(var_insn->op3);
}

static bool ir_may_avoid_spill_load(ir_ctx *ctx, ir_ref ref, ir_ref use)
{
	ir_live_interval *ival;

	IR_ASSERT(ctx->vregs[ref] && ctx->live_intervals[ctx->vregs[ref]]);
	ival = ctx->live_intervals[ctx->vregs[ref]];
	while (ival) {
		ir_use_pos *use_pos = ival->use_pos;
		while (use_pos) {
			if (IR_LIVE_POS_TO_REF(use_pos->pos) == use) {
				return !use_pos->next || use_pos->next->op_num == 0;
			}
			use_pos = use_pos->next;
		}
		ival = ival->next;
	}
	return 0;
}

static void ir_emit_load_imm_int(ir_ctx *ctx, ir_type type, ir_reg reg, int64_t val)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	IR_ASSERT(IR_IS_TYPE_INT(type));
	if (ir_type_size[type] == 8) {
		if (val == 0) {
			if (reg != IR_REG_ZR) {
				|	mov Rx(reg), xzr
			}
		} else if (((uint64_t)(val)) <= 0xffff) {
			|	movz Rx(reg), #((uint64_t)(val))
		} else if (~((uint64_t)(val)) <= 0xffff) {
			|	movn Rx(reg), #(~((uint64_t)(val)))
		} else if ((uint64_t)(val) & 0xffff) {
			|	movz Rx(reg), #((uint64_t)(val) & 0xffff)
			if (((uint64_t)(val) >> 16) & 0xffff) {
				|	movk Rx(reg), #(((uint64_t)(val) >> 16) & 0xffff), lsl #16
			}
			if (((uint64_t)(val) >> 32) & 0xffff) {
				|	movk Rx(reg), #(((uint64_t)(val) >> 32) & 0xffff), lsl #32
			}
			if ((((uint64_t)(val) >> 48) & 0xffff)) {
				|	movk Rx(reg), #(((uint64_t)(val) >> 48) & 0xffff), lsl #48
			}
		} else if (((uint64_t)(val) >> 16) & 0xffff) {
			|	movz Rx(reg), #(((uint64_t)(val) >> 16) & 0xffff), lsl #16
			if (((uint64_t)(val) >> 32) & 0xffff) {
				|	movk Rx(reg), #(((uint64_t)(val) >> 32) & 0xffff), lsl #32
			}
			if ((((uint64_t)(val) >> 48) & 0xffff)) {
				|	movk Rx(reg), #(((uint64_t)(val) >> 48) & 0xffff), lsl #48
			}
		} else if (((uint64_t)(val) >> 32) & 0xffff) {
				|	movz Rx(reg), #(((uint64_t)(val) >> 32) & 0xffff), lsl #32
			if ((((uint64_t)(val) >> 48) & 0xffff)) {
				|	movk Rx(reg), #(((uint64_t)(val) >> 48) & 0xffff), lsl #48
			}
		} else {
			|	movz Rx(reg), #(((uint64_t)(val) >> 48) & 0xffff), lsl #48
		}
	} else {
		if (val == 0) {
			if (reg != IR_REG_ZR) {
				|	mov Rw(reg), wzr
			}
		} else if (((uint64_t)(val)) <= 0xffff) {
			|	movz Rw(reg), #((uint64_t)(val))
		} else if (~((uint64_t)(val)) <= 0xffff) {
			|	movn Rw(reg), #(~((uint64_t)(val)))
		} else if ((uint64_t)(val) & 0xffff) {
			|	movz Rw(reg), #((uint64_t)(val) & 0xffff)
			if (((uint64_t)(val) >> 16) & 0xffff) {
				|	movk Rw(reg), #(((uint64_t)(val) >> 16) & 0xffff), lsl #16
			}
		} else if (((uint64_t)(val) >> 16) & 0xffff) {
			|	movz Rw(reg), #(((uint64_t)(val) >> 16) & 0xffff), lsl #16
		}
	}
}

static void ir_emit_load_mem_int(ir_ctx *ctx, ir_type type, ir_reg reg, ir_mem mem)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg base_reg = IR_MEM_BASE(mem);
	ir_reg index_reg = IR_MEM_INDEX(mem);
	int32_t offset = IR_MEM_OFFSET(mem);

	if (index_reg == IR_REG_NONE) {
		if (aarch64_may_encode_addr_offset(offset, ir_type_size[type])) {
			switch (ir_type_size[type]) {
				default:
					IR_ASSERT(0);
				case 8:
					|	ldr Rx(reg), [Rx(base_reg), #offset]
					break;
				case 4:
					|	ldr Rw(reg), [Rx(base_reg), #offset]
					break;
				case 2:
					if (IR_IS_TYPE_SIGNED(type)) {
						|	ldrsh Rw(reg), [Rx(base_reg), #offset]
					} else {
						|	ldrh Rw(reg), [Rx(base_reg), #offset]
					}
					break;
				case 1:
					if (IR_IS_TYPE_SIGNED(type)) {
						|	ldrsb Rw(reg), [Rx(base_reg), #offset]
					} else {
						|	ldrb Rw(reg), [Rx(base_reg), #offset]
					}
					break;
			}
			return;
		} else {
			index_reg = IR_REG_INT_TMP; /* reserved temporary register */

			ir_emit_load_imm_int(ctx, IR_ADDR, index_reg, offset);
		}
	} else {
		IR_ASSERT(offset == 0);
	}

	switch (ir_type_size[type]) {
		default:
			IR_ASSERT(0);
		case 8:
			|	ldr Rx(reg), [Rx(base_reg), Rx(index_reg)]
			break;
		case 4:
			|	ldr Rw(reg), [Rx(base_reg), Rx(index_reg)]
			break;
		case 2:
			if (IR_IS_TYPE_SIGNED(type)) {
				|	ldrsh Rw(reg), [Rx(base_reg), Rx(index_reg)]
			} else {
				|	ldrh Rw(reg), [Rx(base_reg), Rx(index_reg)]
			}
			break;
		case 1:
			if (IR_IS_TYPE_SIGNED(type)) {
				|	ldrsb Rw(reg), [Rx(base_reg), Rx(index_reg)]
			} else {
				|	ldrb Rw(reg), [Rx(base_reg), Rx(index_reg)]
			}
			break;
	}
}

static void ir_emit_load_imm_fp(ir_ctx *ctx, ir_type type, ir_reg reg, ir_ref src)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_insn *insn = &ctx->ir_base[src];
	int label;

	if (type == IR_FLOAT && insn->val.u32 == 0) {
		|	fmov Rs(reg-IR_REG_FP_FIRST), wzr
	} else if (type == IR_DOUBLE && insn->val.u64 == 0) {
		|	fmov Rd(reg-IR_REG_FP_FIRST), xzr
	} else {
		label = ir_get_const_label(ctx, src);
		if (type == IR_DOUBLE) {
			|	ldr Rd(reg-IR_REG_FP_FIRST), =>label
		} else {
			IR_ASSERT(type == IR_FLOAT);
			|	ldr Rs(reg-IR_REG_FP_FIRST), =>label
		}
	}
}

static void ir_emit_load_mem_fp(ir_ctx *ctx, ir_type type, ir_reg reg, ir_mem mem)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg base_reg = IR_MEM_BASE(mem);
	ir_ref index_reg = IR_MEM_INDEX(mem);
	int32_t offset = IR_MEM_OFFSET(mem);

	if (index_reg == IR_REG_NONE) {
		if (aarch64_may_encode_addr_offset(offset, ir_type_size[type])) {
			if (type == IR_DOUBLE) {
				|	ldr Rd(reg-IR_REG_FP_FIRST), [Rx(base_reg), #offset]
			} else {
				IR_ASSERT(type == IR_FLOAT);
				|	ldr Rs(reg-IR_REG_FP_FIRST), [Rx(base_reg), #offset]
			}
		} else {
			index_reg = IR_REG_INT_TMP; /* reserved temporary register */

			ir_emit_load_imm_int(ctx, IR_ADDR, index_reg, offset);
		}
		return;
	} else {
		IR_ASSERT(offset == 0);
	}

	if (type == IR_DOUBLE) {
		|	ldr Rd(reg-IR_REG_FP_FIRST), [Rx(base_reg), Rx(index_reg)]
	} else {
		IR_ASSERT(type == IR_FLOAT);
		|	ldr Rs(reg-IR_REG_FP_FIRST), [Rx(base_reg), Rx(index_reg)]
	}
}

static void ir_emit_load_mem(ir_ctx *ctx, ir_type type, ir_reg reg, ir_mem mem)
{
	if (IR_IS_TYPE_INT(type)) {
		ir_emit_load_mem_int(ctx, type, reg, mem);
	} else {
		ir_emit_load_mem_fp(ctx, type, reg, mem);
	}
}

static int32_t ir_local_offset(ir_ctx *ctx, ir_insn *insn)
{
	IR_ASSERT(insn->op == IR_VAR || insn->op == IR_ALLOCA || insn->op == IR_VADDR);
	return IR_SPILL_POS_TO_OFFSET(insn->op3);
}

static void ir_load_local_addr(ir_ctx *ctx, ir_reg reg, ir_ref src)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg base = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
	ir_insn *var_insn;
	int32_t offset;

	IR_ASSERT(ir_rule(ctx, src) == IR_STATIC_ALLOCA);
	var_insn = &ctx->ir_base[src];
	if (var_insn->op == IR_VADDR) {
		var_insn = &ctx->ir_base[var_insn->op1];
	}
	offset = ir_local_offset(ctx, var_insn);
	if (aarch64_may_encode_imm12(offset)) {
		|	add Rx(reg), Rx(base), #offset
	} else {
		ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
		|	add Rx(reg), Rx(base), Rx(IR_REG_INT_TMP)
	}
}

static void ir_resolve_label_syms(ir_ctx *ctx)
{
	uint32_t b;
	ir_block *bb;

	for (b = 1, bb = &ctx->cfg_blocks[b]; b <= ctx->cfg_blocks_count; bb++, b++) {
		ir_insn *insn = &ctx->ir_base[bb->start];

		if (insn->op == IR_BEGIN && insn->op2) {
			IR_ASSERT(ctx->ir_base[insn->op2].op == IR_LABEL);
			ctx->ir_base[insn->op2].val.u32_hi = b;
		}
	}
}

static void ir_emit_load_label_addr(ir_ctx *ctx, ir_reg reg, ir_insn *label)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (!data->resolved_label_syms) {
		data->resolved_label_syms = 1;
		ir_resolve_label_syms(ctx);
	}

	IR_ASSERT(label->op == IR_LABEL);
	int b = label->val.u32_hi;

	b = ir_skip_empty_target_blocks(ctx, b);
	|	adr Rx(reg), =>b
}

static void ir_emit_load(ir_ctx *ctx, ir_type type, ir_reg reg, ir_ref src)
{
	if (IR_IS_CONST_REF(src)) {
		if (IR_IS_TYPE_INT(type)) {
			ir_insn *insn = &ctx->ir_base[src];

			if (insn->op == IR_SYM || insn->op == IR_FUNC) {
				void *addr = ir_sym_val(ctx, insn);
				IR_ASSERT(addr);
				ir_emit_load_imm_int(ctx, type, reg, (intptr_t)addr);
			} else if (insn->op == IR_STR) {
				ir_backend_data *data = ctx->data;
				dasm_State **Dst = &data->dasm_state;
				int label = ir_get_const_label(ctx, src);

				|	adr Rx(reg), =>label
			} else if (insn->op == IR_LABEL) {
				ir_emit_load_label_addr(ctx, reg, insn);
			} else {
				ir_emit_load_imm_int(ctx, type, reg, insn->val.i64);
			}
		} else {
			ir_emit_load_imm_fp(ctx, type, reg, src);
		}
	} else if (ctx->vregs[src]) {
		ir_mem mem = ir_ref_spill_slot(ctx, src);
		ir_emit_load_mem(ctx, type, reg, mem);
	} else {
		ir_load_local_addr(ctx, reg, src);
	}
}

static void ir_emit_store_mem_int(ir_ctx *ctx, ir_type type, ir_mem mem, ir_reg reg)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg base_reg = IR_MEM_BASE(mem);
	ir_reg index_reg = IR_MEM_INDEX(mem);
	int32_t offset = IR_MEM_OFFSET(mem);

	if (index_reg == IR_REG_NONE) {
		if (aarch64_may_encode_addr_offset(offset, ir_type_size[type])) {
			switch (ir_type_size[type]) {
				default:
					IR_ASSERT(0);
				case 8:
					|	str Rx(reg), [Rx(base_reg), #offset]
					break;
				case 4:
					|	str Rw(reg), [Rx(base_reg), #offset]
					break;
				case 2:
					|	strh Rw(reg), [Rx(base_reg), #offset]
					break;
				case 1:
					|	strb Rw(reg), [Rx(base_reg), #offset]
					break;
			}
			return;
		} else {
			index_reg = IR_REG_INT_TMP; /* reserved temporary register */

			ir_emit_load_imm_int(ctx, IR_ADDR, index_reg, offset);
	    }
	} else {
		IR_ASSERT(offset == 0);
	}

	switch (ir_type_size[type]) {
		default:
			IR_ASSERT(0);
		case 8:
			|	str Rx(reg), [Rx(base_reg), Rx(index_reg)]
			break;
		case 4:
			|	str Rw(reg), [Rx(base_reg), Rx(index_reg)]
			break;
		case 2:
			|	strh Rw(reg), [Rx(base_reg), Rx(index_reg)]
			break;
		case 1:
			|	strb Rw(reg), [Rx(base_reg), Rx(index_reg)]
			break;
	}
}

static void ir_emit_store_mem_fp(ir_ctx *ctx, ir_type type, ir_mem mem, ir_reg reg)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg base_reg = IR_MEM_BASE(mem);
	ir_reg index_reg = IR_MEM_INDEX(mem);
	int32_t offset = IR_MEM_OFFSET(mem);

	if (index_reg == IR_REG_NONE) {
		if (aarch64_may_encode_addr_offset(offset, ir_type_size[type])) {
			if (type == IR_DOUBLE) {
				|	str Rd(reg-IR_REG_FP_FIRST), [Rx(base_reg), #offset]
			} else {
				IR_ASSERT(type == IR_FLOAT);
				|	str Rs(reg-IR_REG_FP_FIRST), [Rx(base_reg), #offset]
			}
		} else {
			index_reg = IR_REG_INT_TMP; /* reserved temporary register */

			ir_emit_load_imm_int(ctx, IR_ADDR, index_reg, offset);
		}
		return;
	} else {
		IR_ASSERT(offset == 0);
	}

	if (type == IR_DOUBLE) {
		|	str Rd(reg-IR_REG_FP_FIRST), [Rx(base_reg), Rx(index_reg)]
	} else {
		IR_ASSERT(type == IR_FLOAT);
		|	str Rs(reg-IR_REG_FP_FIRST), [Rx(base_reg), Rx(index_reg)]
	}
}

static void ir_emit_store_mem(ir_ctx *ctx, ir_type type, ir_mem mem, ir_reg reg)
{
	if (IR_IS_TYPE_INT(type)) {
		ir_emit_store_mem_int(ctx, type, mem, reg);
	} else {
		ir_emit_store_mem_fp(ctx, type, mem, reg);
	}
}

static void ir_emit_store(ir_ctx *ctx, ir_type type, ir_ref dst, ir_reg reg)
{
	IR_ASSERT(dst >= 0);
	ir_emit_store_mem(ctx, type, ir_ref_spill_slot(ctx, dst), reg);
}

static void ir_emit_mov(ir_ctx *ctx, ir_type type, ir_reg dst, ir_reg src)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (ir_type_size[type] == 8) {
		if (dst == IR_REG_STACK_POINTER) {
			|	mov sp, Rx(src)
		} else if (src == IR_REG_STACK_POINTER) {
			|	mov Rx(dst), sp
		} else {
			|	mov Rx(dst), Rx(src)
		}
	} else {
		|	mov Rw(dst), Rw(src)
	}
}

static void ir_emit_mov_ext(ir_ctx *ctx, ir_type type, ir_reg dst, ir_reg src)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (ir_type_size[type] == 8) {
		|	mov Rx(dst), Rx(src)
	} else {
		|	mov Rw(dst), Rw(src)
	}
}
static void ir_emit_fp_mov(ir_ctx *ctx, ir_type type, ir_reg dst, ir_reg src)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (ir_type_size[type] == 8) {
		|	fmov Rd(dst-IR_REG_FP_FIRST), Rd(src-IR_REG_FP_FIRST)
	} else {
		|	fmov Rs(dst-IR_REG_FP_FIRST), Rs(src-IR_REG_FP_FIRST)
	}
}

static void ir_emit_prologue(ir_ctx *ctx)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	int offset;

	if (ctx->flags & IR_USE_FRAME_POINTER) {
		offset = -(ctx->stack_frame_size+16);
		if (aarch64_may_encode_imm7_addr_offset(offset, 8)) {
			|	stp x29, x30, [sp, #offset]!
		} else if (aarch64_may_encode_imm12(ctx->stack_frame_size+16)) {
			|	sub sp, sp, #(ctx->stack_frame_size+16)
			|	stp x29, x30, [sp]
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, ctx->stack_frame_size+16);
			|	sub sp, sp, Rx(IR_REG_INT_TMP)
			|	stp x29, x30, [sp]
		}
		|	mov x29, sp
		if (ctx->call_stack_size) {
			if (aarch64_may_encode_imm12(ctx->call_stack_size)) {
				|	sub sp, sp, #(ctx->call_stack_size)
			} else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, ctx->call_stack_size);
				|	sub sp, sp, Rx(IR_REG_INT_TMP)
			}
		}
	} else if (ctx->stack_frame_size + ctx->call_stack_size) {
		if (ctx->fixed_stack_red_zone) {
			IR_ASSERT(ctx->stack_frame_size + ctx->call_stack_size <= ctx->fixed_stack_red_zone);
		} else if (aarch64_may_encode_imm12(ctx->stack_frame_size + ctx->call_stack_size)) {
			|	sub sp, sp, #(ctx->stack_frame_size + ctx->call_stack_size)
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, ctx->stack_frame_size + ctx->call_stack_size);
			|	sub sp, sp, Rx(IR_REG_INT_TMP)
		}
	}
	if (ctx->used_preserved_regs) {
	    ir_reg fp;
		uint32_t i;
		ir_reg prev = IR_REG_NONE;
		ir_regset used_preserved_regs = (ir_regset)ctx->used_preserved_regs;

		if (ctx->flags & IR_USE_FRAME_POINTER) {
			fp = IR_REG_FRAME_POINTER;
			offset = ctx->stack_frame_size + sizeof(void*) * 2;
		} else {
			fp = IR_REG_STACK_POINTER;
			offset = ctx->stack_frame_size + ctx->call_stack_size;
		}
		for (i = 0; i < IR_REG_NUM; i++) {
			if (IR_REGSET_IN(used_preserved_regs, i)) {
			    if (prev == IR_REG_NONE) {
					prev = i;
				} else if (i < IR_REG_FP_FIRST) {
					offset -= sizeof(void*) * 2;
					if (aarch64_may_encode_imm7_addr_offset(offset, 8)) {
						|	stp Rx(prev), Rx(i), [Rx(fp), #offset]
					} else if (aarch64_may_encode_addr_offset(offset + 8, 8)) {
						|	str Rx(prev), [Rx(fp), #offset]
						|	str Rx(i), [Rx(fp), #(offset+8)]
					} else {
						ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
						|	str Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
						|	add Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #8
						|	str Rx(i), [Rx(fp), Rx(IR_REG_INT_TMP)]
					}
					prev = IR_REG_NONE;
				} else {
					if (prev < IR_REG_FP_FIRST) {
						offset -= sizeof(void*);
						if (aarch64_may_encode_addr_offset(offset, 8)) {
							|	str Rx(prev), [Rx(fp), #offset]
							offset -= sizeof(void*);
							|	str Rd(i-IR_REG_FP_FIRST), [Rx(fp), #offset]
						} else {
							ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
							offset -= sizeof(void*);
							|	str Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
							|	sub Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #8
							|	str Rd(i-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
						}
					} else {
						offset -= sizeof(void*) * 2;
						if (aarch64_may_encode_imm7_addr_offset(offset, 8)) {
							|	stp Rd(prev-IR_REG_FP_FIRST), Rd(i-IR_REG_FP_FIRST), [Rx(fp), #offset]
						} else if (aarch64_may_encode_addr_offset(offset + 8, 8)) {
							|	str Rd(prev-IR_REG_FP_FIRST), [Rx(fp), #offset]
							|	str Rd(i-IR_REG_FP_FIRST), [Rx(fp), #(offset+8)]
						} else {
							ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
							|	str Rd(prev-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
							|	add Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #8
							|	str Rd(i-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
						}
					}
					prev = IR_REG_NONE;
				}
			}
		}
	    if (prev != IR_REG_NONE) {
			if (prev < IR_REG_FP_FIRST) {
				offset -= sizeof(void*);
				if (aarch64_may_encode_addr_offset(offset, 8)) {
					|	str Rx(prev), [Rx(fp), #offset]
				} else {
					ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
					|	str Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			} else {
				offset -= sizeof(void*);
				if (aarch64_may_encode_addr_offset(offset, 8)) {
					|	str Rd(prev-IR_REG_FP_FIRST), [Rx(fp), #offset]
				} else {
					ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
					|	str Rd(prev-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			}
		}
	}

	if ((ctx->flags & IR_VARARG_FUNC) && (ctx->flags2 & IR_HAS_VA_START)) {
		const ir_call_conv_dsc *cc = data->ra_data.cc;

		if (cc->sysv_varargs) {
			ir_reg fp;
			int offset;
			int i;

			if (ctx->flags & IR_USE_FRAME_POINTER) {
				fp = IR_REG_FRAME_POINTER;

				offset = ctx->locals_area_size + sizeof(void*) * 2;
			} else {
				fp = IR_REG_STACK_POINTER;
				offset = ctx->locals_area_size + ctx->call_stack_size;
			}

			if ((ctx->flags2 & (IR_HAS_VA_ARG_GP|IR_HAS_VA_COPY)) && ctx->gp_reg_params < cc->int_param_regs_count) {
				ir_reg prev = IR_REG_NONE;

				/* skip named args */
				offset += sizeof(void*) * ctx->gp_reg_params;
				for (i = ctx->gp_reg_params; i < cc->int_param_regs_count; i++) {
					if (prev != IR_REG_NONE) {
						if (aarch64_may_encode_imm7_addr_offset(offset, 8)) {
							|	stp Rx(prev), Rx(cc->int_param_regs[i]), [Rx(fp), #offset]
						} else if (aarch64_may_encode_addr_offset(offset + 8, 8)) {
							|	str Rx(prev), [Rx(fp), #offset]
							|	str Rx(cc->int_param_regs[i]), [Rx(fp), #(offset+8)]
						} else {
							ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
							|	str Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
							|	add Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #8
							|	str Rx(cc->int_param_regs[i]), [Rx(fp), Rx(IR_REG_INT_TMP)]
						}
						prev = IR_REG_NONE;
						offset += sizeof(void*) * 2;
					} else {
						prev = cc->int_param_regs[i];
					}
				}
				if (prev != IR_REG_NONE) {
					if (aarch64_may_encode_addr_offset(offset + 8, 8)) {
						|	str Rx(prev), [Rx(fp), #offset]
					} else {
						ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
						|	str Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
					}
					offset += sizeof(void*);
				}
			}
			if ((ctx->flags2 & (IR_HAS_VA_ARG_FP|IR_HAS_VA_COPY)) && ctx->fp_reg_params < cc->fp_param_regs_count) {
				/* skip named args */
				offset += 16 * ctx->fp_reg_params;
				for (i = ctx->fp_reg_params; i < cc->fp_param_regs_count; i++) {
					// TODO: Rd->Rq stur->str ???
					if (aarch64_may_encode_addr_offset(offset, 8)) {
						|	str Rd(cc->fp_param_regs[i]-IR_REG_FP_FIRST), [Rx(fp), #offset]
					} else {
						ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
						|	str Rd(cc->fp_param_regs[i]-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
					}
					offset += 16;
				}
			}
		}
	}
}

static void ir_emit_epilogue(ir_ctx *ctx)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (ctx->used_preserved_regs) {
		int offset;
		uint32_t i;
		ir_reg prev = IR_REG_NONE;
		ir_reg fp = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
		ir_regset used_preserved_regs = (ir_regset)ctx->used_preserved_regs;

		if (ctx->flags & IR_USE_FRAME_POINTER) {
			offset = ctx->stack_frame_size + sizeof(void*) * 2;
		} else {
			offset = ctx->stack_frame_size + ctx->call_stack_size;
		}
		for (i = 0; i < IR_REG_NUM; i++) {
			if (IR_REGSET_IN(used_preserved_regs, i)) {
			    if (prev == IR_REG_NONE) {
					prev = i;
				} else if (i < IR_REG_FP_FIRST) {
					offset -= sizeof(void*) * 2;
					if (aarch64_may_encode_imm7_addr_offset(offset, 8)) {
						|	ldp Rx(prev), Rx(i), [Rx(fp), #offset]
					} else if (aarch64_may_encode_addr_offset(offset + 8, 8)) {
						|	ldr Rx(prev), [Rx(fp), #offset]
						|	ldr Rx(i), [Rx(fp), #(offset+8)]
					} else {
						ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
						|	ldr Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
						|	add Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #8
						|	ldr Rx(i), [Rx(fp), Rx(IR_REG_INT_TMP)]
					}
					prev = IR_REG_NONE;
				} else {
					if (prev < IR_REG_FP_FIRST) {
						offset -= sizeof(void*);
						if (aarch64_may_encode_addr_offset(offset, 8)) {
							|	ldr Rx(prev), [Rx(fp), #offset]
						} else {
							ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
							|	ldr Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
						}
						offset -= sizeof(void*);
						if (aarch64_may_encode_addr_offset(offset, 8)) {
							|	ldr Rd(i-IR_REG_FP_FIRST), [Rx(fp), #offset]
						} else {
							ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
							|	ldr Rd(i-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
						}
					} else {
						offset -= sizeof(void*) * 2;
						if (aarch64_may_encode_imm7_addr_offset(offset, 8)) {
							|	ldp Rd(prev-IR_REG_FP_FIRST), Rd(i-IR_REG_FP_FIRST), [Rx(fp), #offset]
						} else if (aarch64_may_encode_addr_offset(offset + 8, 8)) {
							|	ldr Rd(prev-IR_REG_FP_FIRST), [Rx(fp), #offset]
							|	ldr Rd(i-IR_REG_FP_FIRST), [Rx(fp), #(offset+8)]
						} else {
							ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
							|	ldr Rx(prev-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
							|	add Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #8
							|	ldr Rx(i-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
						}
					}
					prev = IR_REG_NONE;
				}
			}
		}
	    if (prev != IR_REG_NONE) {
			if (prev < IR_REG_FP_FIRST) {
				offset -= sizeof(void*);
				if (aarch64_may_encode_addr_offset(offset, 8)) {
					|	ldr Rx(prev), [Rx(fp), #offset]
				} else {
					ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
					|	ldr Rx(prev), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			} else {
				offset -= sizeof(void*);
				if (aarch64_may_encode_addr_offset(offset, 8)) {
					|	ldr Rd(prev-IR_REG_FP_FIRST), [Rx(fp), #offset]
				} else {
					ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
					|	ldr Rd(prev-IR_REG_FP_FIRST), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			}
		}
	}

	if (ctx->flags & IR_USE_FRAME_POINTER) {
		if (ctx->call_stack_size || (ctx->flags2 & IR_HAS_ALLOCA)) {
			| mov sp, x29
		}
		if (aarch64_may_encode_imm7_addr_offset(ctx->stack_frame_size+16, 8)) {
			|	ldp x29, x30, [sp], #(ctx->stack_frame_size+16)
		} else if (aarch64_may_encode_imm12(ctx->stack_frame_size+16)) {
			|	ldp x29, x30, [sp]
			|	add sp, sp, #(ctx->stack_frame_size+16)
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, ctx->stack_frame_size+16);
			|	ldp x29, x30, [sp]
			|	add sp, sp, Rx(IR_REG_INT_TMP)
		}
	} else if (ctx->stack_frame_size + ctx->call_stack_size) {
		if (ctx->fixed_stack_red_zone) {
			IR_ASSERT(ctx->stack_frame_size + ctx->call_stack_size <= ctx->fixed_stack_red_zone);
		} else if (aarch64_may_encode_imm12(ctx->stack_frame_size + ctx->call_stack_size)) {
			| add sp, sp, #(ctx->stack_frame_size + ctx->call_stack_size)
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, ctx->stack_frame_size + ctx->call_stack_size);
			| add sp, sp, Rx(IR_REG_INT_TMP)
		}
	}
}

static void ir_emit_binop_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_ref op2 = insn->op2;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];
	ir_reg tmp_reg;

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (op2_reg == IR_REG_NONE && op1 == op2) {
		op2_reg = op1_reg;
	}
	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			if (op1 != op2) {
				ir_emit_load(ctx, type, op2_reg, op2);
			}
		}
		switch (insn->op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_ADD:
				|	ASM_REG_REG_REG_OP add, type, def_reg, op1_reg, op2_reg
				break;
			case IR_ADD_OV:
				|	ASM_REG_REG_REG_OP adds, type, def_reg, op1_reg, op2_reg
				break;
			case IR_SUB:
				|	ASM_REG_REG_REG_OP sub, type, def_reg, op1_reg, op2_reg
				break;
			case IR_SUB_OV:
				|	ASM_REG_REG_REG_OP subs, type, def_reg, op1_reg, op2_reg
				break;
			case IR_MUL:
				|	ASM_REG_REG_REG_OP mul, type, def_reg, op1_reg, op2_reg
				break;
			case IR_MUL_OV:
				if (ir_type_size[type] == 8) {
					if (IR_IS_TYPE_SIGNED(type)) {
						tmp_reg = ctx->regs[def][3];
						IR_ASSERT(tmp_reg != IR_REG_NONE);
						|	smulh Rx(tmp_reg), Rx(op1_reg), Rx(op2_reg)
						|	mul Rx(def_reg), Rx(op1_reg), Rx(op2_reg)
						|	cmp Rx(tmp_reg), Rx(def_reg), asr #63
					} else {
						tmp_reg = ctx->regs[def][3];
						IR_ASSERT(tmp_reg != IR_REG_NONE);
						|	umulh Rx(tmp_reg), Rx(op1_reg), Rx(op2_reg)
						|	mul Rx(def_reg), Rx(op1_reg), Rx(op2_reg)
						|	cmp Rx(tmp_reg), xzr
					}
				} else {
					if (IR_IS_TYPE_SIGNED(type)) {
						tmp_reg = ctx->regs[def][3];
						IR_ASSERT(tmp_reg != IR_REG_NONE);
						|	smull Rx(def_reg), Rw(op1_reg), Rw(op2_reg)
						|	asr Rx(tmp_reg), Rx(def_reg), #32
						|	cmp Rx(tmp_reg), Rx(def_reg), asr #31
					} else {
						|	umull Rx(def_reg), Rw(op1_reg), Rw(op2_reg)
						|	cmp xzr, Rx(def_reg), lsr #32
					}
				}
				break;
			case IR_DIV:
				if (IR_IS_TYPE_SIGNED(type)) {
					|	ASM_REG_REG_REG_OP sdiv, type, def_reg, op1_reg, op2_reg
				} else {
					|	ASM_REG_REG_REG_OP udiv, type, def_reg, op1_reg, op2_reg
				}
				break;
			case IR_MOD:
				tmp_reg = ctx->regs[def][3];
				IR_ASSERT(tmp_reg != IR_REG_NONE);
				if (IR_IS_TYPE_SIGNED(type)) {
					|	ASM_REG_REG_REG_OP sdiv, type, tmp_reg, op1_reg, op2_reg
					|	ASM_REG_REG_REG_REG_OP msub, type, def_reg, tmp_reg, op2_reg, op1_reg
				} else {
					|	ASM_REG_REG_REG_OP udiv, type, tmp_reg, op1_reg, op2_reg
					|	ASM_REG_REG_REG_REG_OP msub, type, def_reg, tmp_reg, op2_reg, op1_reg
				}
				break;
			case IR_OR:
				|	ASM_REG_REG_REG_OP orr, type, def_reg, op1_reg, op2_reg
				break;
			case IR_AND:
				|	ASM_REG_REG_REG_OP and, type, def_reg, op1_reg, op2_reg
				break;
			case IR_XOR:
				|	ASM_REG_REG_REG_OP eor, type, def_reg, op1_reg, op2_reg
				break;
		}
	} else {
		IR_ASSERT(IR_IS_CONST_REF(op2));
		IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[op2].op));
		int32_t val = ctx->ir_base[op2].val.i32;
		switch (insn->op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_ADD:
				|	ASM_REG_REG_IMM_OP add, type, def_reg, op1_reg, val
				break;
			case IR_ADD_OV:
				|	ASM_REG_REG_IMM_OP adds, type, def_reg, op1_reg, val
				break;
			case IR_SUB:
				|	ASM_REG_REG_IMM_OP sub, type, def_reg, op1_reg, val
				break;
			case IR_SUB_OV:
				|	ASM_REG_REG_IMM_OP subs, type, def_reg, op1_reg, val
				break;
			case IR_OR:
				if (ir_type_size[type] == 8) {
					uint64_t val = ctx->ir_base[op2].val.u64;
					|	ASM_REG_REG_IMM_OP orr, type, def_reg, op1_reg, val
				} else {
					|	ASM_REG_REG_IMM_OP orr, type, def_reg, op1_reg, val
				}
				break;
			case IR_AND:
				if (ir_type_size[type] == 8) {
					uint64_t val = ctx->ir_base[op2].val.u64;
					|	ASM_REG_REG_IMM_OP and, type, def_reg, op1_reg, val
				} else {
					|	ASM_REG_REG_IMM_OP and, type, def_reg, op1_reg, val
				}
				break;
			case IR_XOR:
				if (ir_type_size[type] == 8) {
					uint64_t val = ctx->ir_base[op2].val.u64;
					|	ASM_REG_REG_IMM_OP eor, type, def_reg, op1_reg, val
				} else {
					|	ASM_REG_REG_IMM_OP eor, type, def_reg, op1_reg, val
				}
				break;
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_min_max_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_ref op2 = insn->op2;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE && op2_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, type, op2_reg, op2);
	}

	if (op1 == op2) {
		return;
	}

	if (ir_type_size[type] == 8) {
		|	cmp Rx(op1_reg), Rx(op2_reg)
		if (insn->op == IR_MIN) {
			if (IR_IS_TYPE_SIGNED(type)) {
				|	csel Rx(def_reg), Rx(op1_reg), Rx(op2_reg), le
			} else {
				|	csel Rx(def_reg), Rx(op1_reg), Rx(op2_reg), ls
			}
		} else {
			IR_ASSERT(insn->op == IR_MAX);
			if (IR_IS_TYPE_SIGNED(type)) {
				|	csel Rx(def_reg), Rx(op1_reg), Rx(op2_reg), ge
			} else {
				|	csel Rx(def_reg), Rx(op1_reg), Rx(op2_reg), hs
			}
		}
	} else {
		|	cmp Rw(op1_reg), Rw(op2_reg)
		if (insn->op == IR_MIN) {
			if (IR_IS_TYPE_SIGNED(type)) {
				|	csel Rw(def_reg), Rw(op1_reg), Rw(op2_reg), le
			} else {
				|	csel Rw(def_reg), Rw(op1_reg), Rw(op2_reg), ls
			}
		} else {
			IR_ASSERT(insn->op == IR_MAX);
			if (IR_IS_TYPE_SIGNED(type)) {
				|	csel Rw(def_reg), Rw(op1_reg), Rw(op2_reg), ge
			} else {
				|	csel Rw(def_reg), Rw(op1_reg), Rw(op2_reg), hs
			}
		}
	}

	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_overflow(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_insn *math_insn = &ctx->ir_base[insn->op1];
	ir_type type = math_insn->type;

	IR_ASSERT(def_reg != IR_REG_NONE);
	IR_ASSERT(IR_IS_TYPE_INT(type));
	if (math_insn->op == IR_MUL_OV) {
		|	cset Rw(def_reg), ne
	} else if (IR_IS_TYPE_SIGNED(type)) {
		|	cset Rw(def_reg), vs
	} else {
		|	cset Rw(def_reg), cs
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, insn->type, def, def_reg);
	}
}

static void ir_emit_overflow_and_branch(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn, uint32_t next_block)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_insn *overflow_insn = &ctx->ir_base[insn->op2];
	ir_insn *math_insn = &ctx->ir_base[overflow_insn->op1];
	ir_type type = math_insn->type;
	uint32_t true_block, false_block;
	bool reverse = 0;

	ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
	if (true_block == next_block) {
		reverse = 1;
		true_block = false_block;
		false_block = 0;
	} else if (false_block == next_block) {
		false_block = 0;
	}

	if (math_insn->op == IR_MUL_OV) {
		if (reverse) {
			|	beq =>true_block
		} else {
			|	bne =>true_block
		}
	} else if (IR_IS_TYPE_SIGNED(type)) {
		if (reverse) {
			|	bvc =>true_block
		} else {
			|	bvs =>true_block
		}
	} else {
		if (reverse) {
			|	bcc =>true_block
		} else {
			|	bcs =>true_block
		}
	}
	if (false_block) {
		|	b =>false_block
	}
}

static void ir_emit_reg_binop_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_insn *op_insn = &ctx->ir_base[insn->op2];
	ir_type type = op_insn->type;
	ir_ref op2 = op_insn->op2;
	ir_reg op2_reg = ctx->regs[insn->op2][2];
	ir_reg reg;

	IR_ASSERT(insn->op == IR_RSTORE);
	reg = insn->op3;

	if (op2_reg == IR_REG_NONE) {
		ir_val *val = &ctx->ir_base[op2].val;

		IR_ASSERT(IR_IS_CONST_REF(op2));
		IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[op2].op));
		switch (op_insn->op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_ADD:
				|	ASM_REG_REG_IMM_OP add, type, reg, reg, val->i32
				break;
			case IR_SUB:
				|	ASM_REG_REG_IMM_OP sub, type, reg, reg, val->i32
				break;
			case IR_OR:
				|	ASM_REG_REG_IMM_OP orr, type, reg, reg, val->i32
				break;
			case IR_AND:
				|	ASM_REG_REG_IMM_OP and, type, reg, reg, val->i32
				break;
			case IR_XOR:
				|	ASM_REG_REG_IMM_OP eor, type, reg, reg, val->i32
				break;
		}
	} else {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			ir_emit_load(ctx, type, op2_reg, op2);
		}
		switch (op_insn->op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_ADD:
				|	ASM_REG_REG_REG_OP add, type, reg, reg, op2_reg
				break;
			case IR_SUB:
				|	ASM_REG_REG_REG_OP sub, type, reg, reg, op2_reg
				break;
			case IR_MUL:
				|	ASM_REG_REG_REG_OP mul, type, reg, reg, op2_reg
				break;
			case IR_OR:
				|	ASM_REG_REG_REG_OP orr, type, reg, reg, op2_reg
				break;
			case IR_AND:
				|	ASM_REG_REG_REG_OP and, type, reg, reg, op2_reg
				break;
			case IR_XOR:
				|	ASM_REG_REG_REG_OP eor, type, reg, reg, op2_reg
				break;
		}
	}
}

static void ir_emit_mul_div_mod_pwr2(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_CONST_REF(insn->op2));
	IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[insn->op2].op));
	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (insn->op == IR_MUL) {
		uint32_t shift = IR_LOG2(ctx->ir_base[insn->op2].val.u64);
		if (shift == 1) {
			|	ASM_REG_REG_REG_OP add, type, def_reg, op1_reg, op1_reg
		} else {
			|	ASM_REG_REG_IMM_OP lsl, type, def_reg, op1_reg, shift
		}
	} else if (insn->op == IR_DIV) {
		uint32_t shift = IR_LOG2(ctx->ir_base[insn->op2].val.u64);
		|	ASM_REG_REG_IMM_OP lsr, type, def_reg, op1_reg, shift
	} else {
		IR_ASSERT(insn->op == IR_MOD);
		uint64_t mask = ctx->ir_base[insn->op2].val.u64 - 1;
		|	ASM_REG_REG_IMM_OP and, type, def_reg, op1_reg, mask
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_sdiv_pwr2(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];
	uint32_t shift = IR_LOG2(ctx->ir_base[insn->op2].val.u64);
	int64_t offset = ctx->ir_base[insn->op2].val.u64 - 1;

	IR_ASSERT(shift != 0);
	IR_ASSERT(IR_IS_CONST_REF(insn->op2));
	IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[insn->op2].op));
	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE && def_reg != op1_reg);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}

	if (op2_reg != IR_REG_NONE) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load_imm_int(ctx, type, op2_reg, offset);
	}

	if (ir_type_size[type] == 8) {
		|	cmp Rx(op1_reg), #0
		if (op2_reg != IR_REG_NONE) {
			|	add Rx(def_reg), Rx(op1_reg), Rx(op2_reg)
		} else {
			|	add Rx(def_reg), Rx(op1_reg), #offset
		}
		|	csel Rx(def_reg), Rx(def_reg), Rx(op1_reg), lt
		|	asr Rx(def_reg), Rx(def_reg), #shift
	} else {
		|	cmp Rw(op1_reg), #0
		if (op2_reg != IR_REG_NONE) {
			|	add Rw(def_reg), Rw(op1_reg), Rw(op2_reg)
		} else {
			|	add Rw(def_reg), Rw(op1_reg), #offset
		}
		|	csel Rw(def_reg), Rw(def_reg), Rw(op1_reg), lt
		if (ir_type_size[type] == 4) {
			|	asr Rw(def_reg), Rw(def_reg), #shift
		} else if (ir_type_size[type] == 2) {
			|	ubfx Rw(def_reg), Rw(def_reg), #shift, #16
		} else {
			IR_ASSERT(ir_type_size[type] == 1);
			|	ubfx Rw(def_reg), Rw(def_reg), #shift, #8
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_smod_pwr2(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg tmp_reg = ctx->regs[def][3];
//	uint32_t shift = IR_LOG2(ctx->ir_base[insn->op2].val.u64);
	uint64_t mask = ctx->ir_base[insn->op2].val.u64 - 1;

	IR_ASSERT(mask != 0);
	IR_ASSERT(IR_IS_CONST_REF(insn->op2));
	IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[insn->op2].op));
	IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE && def_reg != tmp_reg);

	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (def_reg != op1_reg) {
		if (op1_reg != IR_REG_NONE) {
			ir_emit_mov(ctx, type, def_reg, op1_reg);
		} else {
			ir_emit_load(ctx, type, def_reg, op1);
		}
	}

//	|	ASM_REG_REG_IMM_OP asr, type, tmp_reg, def_reg, (ir_type_size[type]*8-1)
//	|	ASM_REG_REG_IMM_OP lsr, type, tmp_reg, tmp_reg, (ir_type_size[type]*8-shift)
//	|	ASM_REG_REG_REG_OP add, type, def_reg, def_reg, tmp_reg
//	|	ASM_REG_REG_IMM_OP and, type, def_reg, def_reg, mask
//	|	ASM_REG_REG_REG_OP sub, type, def_reg, def_reg, tmp_reg

	|	ASM_REG_REG_OP negs, type, tmp_reg, def_reg
	|	ASM_REG_REG_IMM_OP and, type, def_reg, def_reg, mask
	|	ASM_REG_REG_IMM_OP and, type, tmp_reg, tmp_reg, mask
	|	ASM_REG_REG_REG_TXT_OP csneg, type, def_reg, def_reg, tmp_reg, mi

	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_shift(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];
	ir_reg tmp_reg;

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE && op2_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, insn->op1);
	}
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, type, op2_reg, insn->op2);
	}
	switch (insn->op) {
		default:
			IR_ASSERT(0);
		case IR_SHL:
			if (ir_type_size[type] == 1) {
				|	and Rw(def_reg), Rw(op1_reg), #0xff
				|	lsl Rw(def_reg), Rw(def_reg), Rw(op2_reg)
			} else if (ir_type_size[type] == 2) {
				|	and Rw(def_reg), Rw(op1_reg), #0xffff
				|	lsl Rw(def_reg), Rw(def_reg), Rw(op2_reg)
			} else {
				|	ASM_REG_REG_REG_OP lsl, type, def_reg, op1_reg, op2_reg
			}
			break;
		case IR_SHR:
			if (ir_type_size[type] == 1) {
				|	and Rw(def_reg), Rw(op1_reg), #0xff
				|	lsr Rw(def_reg), Rw(def_reg), Rw(op2_reg)
			} else if (ir_type_size[type] == 2) {
				|	and Rw(def_reg), Rw(op1_reg), #0xffff
				|	lsr Rw(def_reg), Rw(def_reg), Rw(op2_reg)
			} else {
				|	ASM_REG_REG_REG_OP lsr, type, def_reg, op1_reg, op2_reg
			}
			break;
		case IR_SAR:
			if (ir_type_size[type] == 1) {
				|	sxtb Rw(def_reg), Rw(op1_reg)
				|	asr Rw(def_reg), Rw(def_reg), Rw(op2_reg)
			} else if (ir_type_size[type] == 2) {
				|	sxth Rw(def_reg), Rw(op1_reg)
				|	asr Rw(def_reg), Rw(def_reg), Rw(op2_reg)
			} else {
				|	ASM_REG_REG_REG_OP asr, type, def_reg, op1_reg, op2_reg
			}
			break;
		case IR_ROL:
			tmp_reg = ctx->regs[def][3];
			IR_ASSERT(tmp_reg != IR_REG_NONE);
			if (ir_type_size[type] == 1) {
				|	and Rw(def_reg), Rw(op1_reg), #0xff
				|	add Rw(def_reg), Rw(def_reg), Rw(def_reg), lsl #8
				|	add Rw(def_reg), Rw(def_reg), Rw(def_reg), lsl #16
				|	neg Rw(tmp_reg), Rw(op2_reg)
				|	ror Rw(def_reg), Rw(def_reg), Rw(tmp_reg)
				|	and Rw(def_reg), Rw(def_reg), #0xff
			} else if (ir_type_size[type] == 2) {
				|	and Rw(def_reg), Rw(op1_reg), #0xffff
				|	add Rw(def_reg), Rw(def_reg), Rw(def_reg), lsl #16
				|	neg Rw(tmp_reg), Rw(op2_reg)
				|	ror Rw(def_reg), Rw(def_reg), Rw(tmp_reg)
				|	and Rw(def_reg), Rw(def_reg), #0xffff
			} else if (ir_type_size[type] == 8) {
				|	neg Rx(tmp_reg), Rx(op2_reg)
				|	ror Rx(def_reg), Rx(op1_reg), Rx(tmp_reg)
			} else {
				|	neg Rw(tmp_reg), Rw(op2_reg)
				|	ror Rw(def_reg), Rw(op1_reg), Rw(tmp_reg)
			}
			break;
		case IR_ROR:
			if (ir_type_size[type] == 1) {
				tmp_reg = ctx->regs[def][3];
				IR_ASSERT(tmp_reg != IR_REG_NONE);
				|	and Rw(tmp_reg), Rw(op1_reg), #0xff
				|	add Rw(tmp_reg), Rw(tmp_reg), Rw(tmp_reg), lsl #8
				|	add Rw(tmp_reg), Rw(tmp_reg), Rw(tmp_reg), lsl #16
				|	ror Rw(def_reg), Rw(tmp_reg), Rw(op2_reg)
				|	and Rw(def_reg), Rw(def_reg), #0xff
			} else if (ir_type_size[type] == 2) {
				tmp_reg = ctx->regs[def][3];
				IR_ASSERT(tmp_reg != IR_REG_NONE);
				|	and Rw(tmp_reg), Rw(op1_reg), #0xffff
				|	add Rw(tmp_reg), Rw(tmp_reg), Rw(tmp_reg), lsl #16
				|	ror Rw(def_reg), Rw(tmp_reg), Rw(op2_reg)
				|	and Rw(def_reg), Rw(def_reg), #0xffff
			} else {
				|	ASM_REG_REG_REG_OP ror, type, def_reg, op1_reg, op2_reg
			}
			break;
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_shift_const(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	uint32_t shift = ctx->ir_base[insn->op2].val.u64;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg tmp_reg;

	IR_ASSERT(IR_IS_CONST_REF(insn->op2));
	IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[insn->op2].op));
	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	switch (insn->op) {
		default:
			IR_ASSERT(0);
		case IR_SHL:
			if (ir_type_size[type] == 1) {
				|	ubfiz Rw(def_reg), Rw(op1_reg), #shift, #(8-shift)
			} else if (ir_type_size[type] == 2) {
				|	ubfiz Rw(def_reg), Rw(op1_reg), #shift, #(16-shift)
			} else {
				|	ASM_REG_REG_IMM_OP lsl, type, def_reg, op1_reg, shift
			}
			break;
		case IR_SHR:
			if (ir_type_size[type] == 1) {
				|	ubfx Rw(def_reg), Rw(op1_reg), #shift, #(8-shift)
			} else if (ir_type_size[type] == 2) {
				|	ubfx Rw(def_reg), Rw(op1_reg), #shift, #(16-shift)
			} else {
				|	ASM_REG_REG_IMM_OP lsr, type, def_reg, op1_reg, shift
			}
			break;
		case IR_SAR:
			if (ir_type_size[type] == 1) {
				|	sbfx Rw(def_reg), Rw(op1_reg), #shift, #(8-shift)
			} else if (ir_type_size[type] == 2) {
				|	sbfx Rw(def_reg), Rw(op1_reg), #shift, #(16-shift)
			} else {
				|	ASM_REG_REG_IMM_OP asr, type, def_reg, op1_reg, shift
			}
			break;
		case IR_ROL:
			if (ir_type_size[type] == 1) {
				tmp_reg = ctx->regs[def][3];
				|	ubfx Rw(tmp_reg), Rw(op1_reg), #(8-shift), #shift
				|	orr Rw(def_reg), Rw(tmp_reg), Rw(op1_reg), lsl #shift
			} else if (ir_type_size[type] == 2) {
				tmp_reg = ctx->regs[def][3];
				|	ubfx Rw(tmp_reg), Rw(op1_reg), #(16-shift), #shift
				|	orr Rw(def_reg), Rw(tmp_reg), Rw(op1_reg), lsl #shift
			} else if (ir_type_size[type] == 8) {
				shift = (64 - shift) % 64;
				|	ror Rx(def_reg), Rx(op1_reg), #shift
			} else {
				shift = (32 - shift) % 32;
				|	ror Rw(def_reg), Rw(op1_reg), #shift
			}
			break;
		case IR_ROR:
			if (ir_type_size[type] == 1) {
				tmp_reg = ctx->regs[def][3];
				|	ubfx Rw(tmp_reg), Rw(op1_reg), #shift, #(8-shift)
				|	orr Rw(def_reg), Rw(tmp_reg), Rw(op1_reg), lsl #(8-shift)
			} else if (ir_type_size[type] == 2) {
				tmp_reg = ctx->regs[def][3];
				|	ubfx Rw(tmp_reg), Rw(op1_reg), #shift, #(16-shift)
				|	orr Rw(def_reg), Rw(tmp_reg), Rw(op1_reg), lsl #(16-shift)
			} else {
				|	ASM_REG_REG_IMM_OP ror, type, def_reg, op1_reg, shift
			}
			break;
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_op_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (insn->op == IR_NOT) {
		if (insn->type == IR_BOOL) {
			|	ASM_REG_IMM_OP cmp, type, op1_reg, 0
			|	cset Rw(def_reg), eq
		} else {
			|	ASM_REG_REG_OP mvn, insn->type, def_reg, op1_reg
		}
	} else if (insn->op == IR_NEG) {
		|	ASM_REG_REG_OP neg, insn->type, def_reg, op1_reg
	} else if (insn->op == IR_ABS) {
		if (ir_type_size[type] == 8) {
			|	cmp Rx(op1_reg), #0
			|	cneg Rx(def_reg), Rx(op1_reg), lt
		} else {
			|	cmp Rw(op1_reg), #0
			|	cneg Rw(def_reg), Rw(op1_reg), lt
		}
	} else if (insn->op == IR_CTLZ) {
		if (ir_type_size[type] == 1) {
			|	and	Rw(def_reg), Rw(op1_reg), #0xff
			|	clz Rw(def_reg), Rw(def_reg)
			|	sub Rw(def_reg), Rw(def_reg), #24
		} else if (ir_type_size[type] == 2) {
			|	and	Rw(def_reg), Rw(op1_reg), #0xffff
			|	clz Rw(def_reg), Rw(def_reg)
			|	sub Rw(def_reg), Rw(def_reg), #16
		} else {
			|	ASM_REG_REG_OP clz, type, def_reg, op1_reg
		}
	} else if (insn->op == IR_CTTZ) {
		|	ASM_REG_REG_OP rbit, insn->type, def_reg, op1_reg
		|	ASM_REG_REG_OP clz, insn->type, def_reg, def_reg
	} else {
		IR_ASSERT(insn->op == IR_BSWAP);
		|	ASM_REG_REG_OP rev, insn->type, def_reg, op1_reg
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_ctpop(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg tmp_reg = ctx->regs[def][2];
	uint32_t code1 = 0x0e205800 | (tmp_reg-IR_REG_FP_FIRST); // cnt v0.8b, v0.8b
	uint32_t code2 = 0x0e31b800 | (tmp_reg-IR_REG_FP_FIRST); // addv b0, v0.8b

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	switch (ir_type_size[insn->type]) {
		default:
			IR_ASSERT(0);
		case 1:
			|	and	Rw(def_reg), Rw(op1_reg), #0xff
			|	fmov Rs(tmp_reg-IR_REG_FP_FIRST), Rw(def_reg)
			|	.long code1 // cnt v0.8b, v0.8b
			|	.long code2 // addv b0, v0.8b
			|	fmov Rw(def_reg), Rs(tmp_reg-IR_REG_FP_FIRST)
			break;
		case 2:
			|	and	Rw(def_reg), Rw(op1_reg), #0xffff
			|	fmov Rs(tmp_reg-IR_REG_FP_FIRST), Rw(def_reg)
			|	.long code1 // cnt v0.8b, v0.8b
			|	.long code2 // addv b0, v0.8b
			|	fmov Rw(def_reg), Rs(tmp_reg-IR_REG_FP_FIRST)
			break;
		case 4:
			|	fmov Rs(tmp_reg-IR_REG_FP_FIRST), Rw(op1_reg)
			|	.long code1 // cnt v0.8b, v0.8b
			|	.long code2 // addv b0, v0.8b
			|	fmov Rw(def_reg), Rs(tmp_reg-IR_REG_FP_FIRST)
			break;
		case 8:
			|	fmov Rd(tmp_reg-IR_REG_FP_FIRST), Rx(op1_reg)
			|	.long code1 // cnt v0.8b, v0.8b
			|	.long code2 // addv b0, v0.8b
			|	fmov Rx(def_reg), Rd(tmp_reg-IR_REG_FP_FIRST)
			break;
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_op_fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (insn->op == IR_NEG) {
		if (type == IR_DOUBLE) {
			|	fneg Rd(def_reg-IR_REG_FP_FIRST), Rd(op1_reg-IR_REG_FP_FIRST)
		} else {
			IR_ASSERT(type == IR_FLOAT);
			|	fneg Rs(def_reg-IR_REG_FP_FIRST), Rs(op1_reg-IR_REG_FP_FIRST)
		}
	} else {
		IR_ASSERT(insn->op == IR_ABS);
		if (type == IR_DOUBLE) {
			|	fabs Rd(def_reg-IR_REG_FP_FIRST), Rd(op1_reg-IR_REG_FP_FIRST)
		} else {
			IR_ASSERT(type == IR_FLOAT);
			|	fabs Rs(def_reg-IR_REG_FP_FIRST), Rs(op1_reg-IR_REG_FP_FIRST)
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, insn->type, def, def_reg);
	}
}

static void ir_emit_binop_fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_ref op2 = insn->op2;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE && op2_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		if (op1 != op2) {
			ir_emit_load(ctx, type, op2_reg, op2);
		}
	}
	switch (insn->op) {
		default:
			IR_ASSERT(0 && "NIY binary op");
		case IR_ADD:
			|	ASM_FP_REG_REG_REG_OP fadd, type, def_reg, op1_reg, op2_reg
			break;
		case IR_SUB:
			|	ASM_FP_REG_REG_REG_OP fsub, type, def_reg, op1_reg, op2_reg
			break;
		case IR_MUL:
			|	ASM_FP_REG_REG_REG_OP fmul, type, def_reg, op1_reg, op2_reg
			break;
		case IR_DIV:
			|	ASM_FP_REG_REG_REG_OP fdiv, type, def_reg, op1_reg, op2_reg
			break;
		case IR_MIN:
			|	ASM_FP_REG_REG_REG_OP fmin, type, def_reg, op1_reg, op2_reg
			break;
		case IR_MAX:
			|	ASM_FP_REG_REG_REG_OP fmax, type, def_reg, op1_reg, op2_reg
			break;
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, insn->type, def, def_reg);
	}
}

static void ir_emit_fix_type(ir_ctx *ctx, ir_type type, ir_reg op1_reg)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	// TODO: prevent repeatable sign/zero extension ???
	if (ir_type_size[type] == 2) {
		if (IR_IS_TYPE_SIGNED(type)) {
			|	sxth Rw(op1_reg), Rw(op1_reg)
		} else {
			|	uxth Rw(op1_reg), Rw(op1_reg)
		}
	} else if (ir_type_size[type] == 1) {
		if (IR_IS_TYPE_SIGNED(type)) {
			|	sxtb Rw(op1_reg), Rw(op1_reg)
		} else {
			|	uxtb Rw(op1_reg), Rw(op1_reg)
		}
	}
}

static void ir_emit_cmp_int_common(ir_ctx *ctx, ir_type type, ir_reg op1_reg, ir_ref op1, ir_reg op2_reg, ir_ref op2)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	IR_ASSERT(op1_reg != IR_REG_NONE);
	if (ir_type_size[type] < 4) {
		ir_emit_fix_type(ctx, type, op1_reg);
	}
	if (op2_reg != IR_REG_NONE) {
		if (ir_type_size[type] == 8) {
			|	cmp Rx(op1_reg), Rx(op2_reg)
		} else if (ir_type_size[type] == 4) {
			|	cmp Rw(op1_reg), Rw(op2_reg)
		} else if (ir_type_size[type] == 2) {
			if (IR_IS_TYPE_SIGNED(type)) {
				|	cmp Rw(op1_reg), Rw(op2_reg), sxth
			} else {
				|	cmp Rw(op1_reg), Rw(op2_reg), uxth
			}
		} else if (ir_type_size[type] == 1) {
			if (IR_IS_TYPE_SIGNED(type)) {
				|	cmp Rw(op1_reg), Rw(op2_reg), sxtb
			} else {
				|	cmp Rw(op1_reg), Rw(op2_reg), uxtb
			}
		} else {
			IR_ASSERT(0);
		}
	} else {
		IR_ASSERT(IR_IS_CONST_REF(op2));
		IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[op2].op));
		int32_t val = ctx->ir_base[op2].val.i32;

		if (ir_type_size[type] == 8) {
			|	cmp Rx(op1_reg), #val
		} else {
			|	cmp Rw(op1_reg), #val
		}
	}
}

static void ir_emit_cmp_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = ctx->ir_base[insn->op1].type;
	ir_op op = insn->op;
	ir_ref op1 = insn->op1;
	ir_ref op2 = insn->op2;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];

	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			if (op1 != op2) {
				ir_emit_load(ctx, type, op2_reg, op2);
			}
		}
	}
	if (IR_IS_CONST_REF(insn->op2)
	 && !IR_IS_SYM_CONST(ctx->ir_base[insn->op2].op)
	 && ctx->ir_base[insn->op2].val.u64 == 0) {
		if (op == IR_ULT) {
			/* always false */
			ir_emit_load_imm_int(ctx, IR_BOOL, def_reg, 0);
			if (IR_REG_SPILLED(ctx->regs[def][0])) {
				ir_emit_store(ctx, insn->type, def, def_reg);
			}
			return;
		} else if (op == IR_UGE) {
			/* always true */
			ir_emit_load_imm_int(ctx, IR_BOOL, def_reg, 1);
			if (IR_REG_SPILLED(ctx->regs[def][0])) {
				ir_emit_store(ctx, insn->type, def, def_reg);
			}
			return;
		} else if (op == IR_ULE) {
			op = IR_EQ;
		} else if (op == IR_UGT) {
			op = IR_NE;
		}
	}
	ir_emit_cmp_int_common(ctx, type, op1_reg, op1, op2_reg, op2);
	switch (op) {
		default:
			IR_ASSERT(0 && "NIY binary op");
		case IR_EQ:
			|	cset Rw(def_reg), eq
			break;
		case IR_NE:
			|	cset Rw(def_reg), ne
			break;
		case IR_LT:
			|	cset Rw(def_reg), lt
			break;
		case IR_GE:
			|	cset Rw(def_reg), ge
			break;
		case IR_LE:
			|	cset Rw(def_reg), le
			break;
		case IR_GT:
			|	cset Rw(def_reg), gt
			break;
		case IR_ULT:
			|	cset Rw(def_reg), lo
			break;
		case IR_UGE:
			|	cset Rw(def_reg), hs
			break;
		case IR_ULE:
			|	cset Rw(def_reg), ls
			break;
		case IR_UGT:
			|	cset Rw(def_reg), hi
			break;
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, insn->type, def, def_reg);
	}
}

static ir_op ir_emit_cmp_fp_common(ir_ctx *ctx, ir_ref root, ir_ref cmp_ref, ir_insn *cmp_insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = ctx->ir_base[cmp_insn->op1].type;
	ir_op op = cmp_insn->op;
	ir_ref op1, op2;
	ir_reg op1_reg, op2_reg;

	op1 = cmp_insn->op1;
	op2 = cmp_insn->op2;
	if (UNEXPECTED(ctx->rules[cmp_ref] & IR_FUSED_REG)) {
		op1_reg = ir_get_fused_reg(ctx, root, cmp_ref * sizeof(ir_ref) + 1);
		op2_reg = ir_get_fused_reg(ctx, root, cmp_ref * sizeof(ir_ref) + 2);
	} else {
		op1_reg = ctx->regs[cmp_ref][1];
		op2_reg = ctx->regs[cmp_ref][2];
	}

	IR_ASSERT(op1_reg != IR_REG_NONE && op2_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		if (op1 != op2) {
			ir_emit_load(ctx, type, op2_reg, op2);
		}
	}
	if (type == IR_DOUBLE) {
		|	fcmp Rd(op1_reg-IR_REG_FP_FIRST), Rd(op2_reg-IR_REG_FP_FIRST)
	} else {
		IR_ASSERT(type == IR_FLOAT);
		|	fcmp Rs(op1_reg-IR_REG_FP_FIRST), Rs(op2_reg-IR_REG_FP_FIRST)
	}
	return op;
}

static void ir_emit_cmp_fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_op op = ir_emit_cmp_fp_common(ctx, def, def, insn);
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
//???	ir_reg tmp_reg = ctx->regs[def][3]; // TODO: take into account vs flag

	IR_ASSERT(def_reg != IR_REG_NONE);
	switch (op) {
		default:
			IR_ASSERT(0 && "NIY binary op");
		case IR_EQ:
			|	cset Rw(def_reg), eq
			break;
		case IR_NE:
			|	cset Rw(def_reg), ne
			break;
		case IR_LT:
			|	cset Rw(def_reg), mi
			break;
		case IR_GE:
			|	cset Rw(def_reg), ge
			break;
		case IR_LE:
			|	cset Rw(def_reg), ls
			break;
		case IR_GT:
			|	cset Rw(def_reg), gt
			break;
		case IR_ULT:
			|	cset Rw(def_reg), lt
			break;
		case IR_UGE:
			|	cset Rw(def_reg), hs
			break;
		case IR_ULE:
			|	cset Rw(def_reg), le
			break;
		case IR_UGT:
			|	cset Rw(def_reg), hi
			break;
		case IR_ORDERED:
			|	cset Rw(def_reg), vc
			break;
		case IR_UNORDERED:
			|	cset Rw(def_reg), vs
			break;
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, insn->type, def, def_reg);
	}
}

static void ir_emit_jmp_true(ir_ctx *ctx, uint32_t b, ir_ref def, uint32_t next_block)
{
	uint32_t true_block, false_block;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
	if (true_block != next_block) {
		|	b =>true_block
	}
}

static void ir_emit_jmp_false(ir_ctx *ctx, uint32_t b, ir_ref def, uint32_t next_block)
{
	uint32_t true_block, false_block;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
	if (false_block != next_block) {
		|	b =>false_block
	}
}

static void ir_emit_jz(ir_ctx *ctx, uint32_t b, uint32_t next_block, uint8_t op, ir_type type, ir_reg reg)
{
	uint32_t true_block, false_block;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
	if (true_block == next_block) {
		IR_ASSERT(op < IR_LT);
		op ^= 1; // reverse
		true_block = false_block;
		false_block = 0;
	} else if (false_block == next_block) {
		false_block = 0;
	}

	if (op == IR_EQ) {
		if (ir_type_size[type] == 8) {
			|	cbz Rx(reg), =>true_block
		} else {
			|	cbz Rw(reg), =>true_block
		}
	} else {
		IR_ASSERT(op == IR_NE);
		if (ir_type_size[type] == 8) {
			|	cbnz Rx(reg), =>true_block
		} else {
			|	cbnz Rw(reg), =>true_block
		}
	}
	if (false_block) {
		|	b =>false_block
	}
}

static void ir_emit_jcc(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn, uint32_t next_block, uint8_t op, bool int_cmp)
{
	uint32_t true_block, false_block;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
	if (true_block == next_block) {
		/* swap to avoid unconditional JMP */
		if (int_cmp || op == IR_EQ || op == IR_NE || op == IR_ORDERED || op == IR_UNORDERED) {
			op ^= 1; // reverse
		} else {
			op ^= 5; // reverse
		}
		true_block = false_block;
		false_block = 0;
	} else if (false_block == next_block) {
		false_block = 0;
	}

	if (int_cmp) {
		switch (op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_EQ:
				|	beq =>true_block
				break;
			case IR_NE:
				|	bne =>true_block
				break;
			case IR_LT:
				|	blt =>true_block
				break;
			case IR_GE:
				|	bge =>true_block
				break;
			case IR_LE:
				|	ble =>true_block
				break;
			case IR_GT:
				|	bgt =>true_block
				break;
			case IR_ULT:
				|	blo =>true_block
				break;
			case IR_UGE:
				|	bhs =>true_block
				break;
			case IR_ULE:
				|	bls =>true_block
				break;
			case IR_UGT:
				|	bhi =>true_block
				break;
		}
	} else {
		switch (op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_EQ:
				|	beq =>true_block
				break;
			case IR_NE:
				|	bne =>true_block
				break;
			case IR_LT:
				|	bmi =>true_block
				break;
			case IR_GE:
				|	bge =>true_block
				break;
			case IR_LE:
				|	bls =>true_block
				break;
			case IR_GT:
				|	bgt =>true_block
				break;
			case IR_ULT:
				|	blt =>true_block
				break;
			case IR_UGE:
				|	bhs =>true_block
				break;
			case IR_ULE:
				|	ble =>true_block
				break;
			case IR_UGT:
				|	bhi =>true_block
				break;
			case IR_ORDERED:
				|	bvc =>true_block
				break;
			case IR_UNORDERED:
				|	bvs =>true_block
		}
	}
	if (false_block) {
		|	b =>false_block
	}
}

static void ir_emit_cmp_and_branch_int(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn, uint32_t next_block)
{
	ir_insn *cmp_insn = &ctx->ir_base[insn->op2];
	ir_op op = cmp_insn->op;
	ir_type type = ctx->ir_base[cmp_insn->op1].type;
	ir_ref op1 = cmp_insn->op1;
	ir_ref op2 = cmp_insn->op2;
	ir_reg op1_reg, op2_reg;

	if (UNEXPECTED(ctx->rules[insn->op2] & IR_FUSED_REG)) {
		op1_reg = ir_get_fused_reg(ctx, def, insn->op2 * sizeof(ir_ref) + 1);
		op2_reg = ir_get_fused_reg(ctx, def, insn->op2 * sizeof(ir_ref) + 2);
	} else {
		op1_reg = ctx->regs[insn->op2][1];
		op2_reg = ctx->regs[insn->op2][2];
	}

	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			if (op1 != op2) {
				ir_emit_load(ctx, type, op2_reg, op2);
			}
		}
	}
	if (IR_IS_CONST_REF(op2)
	 && !IR_IS_SYM_CONST(ctx->ir_base[op2].op)
	 && ctx->ir_base[op2].val.u64 == 0) {
		if (op == IR_ULT) {
			/* always false */
			ir_emit_jmp_false(ctx, b, def, next_block);
			return;
		} else if (op == IR_UGE) {
			/* always true */
			ir_emit_jmp_true(ctx, b, def, next_block);
			return;
		} else if (op == IR_ULE) {
			op = IR_EQ;
		} else if (op == IR_UGT) {
			op = IR_NE;
		}
		if (op1_reg != IR_REG_NONE && (op == IR_EQ || op == IR_NE)) {
			ir_emit_jz(ctx, b, next_block, op, type, op1_reg);
			return;
		}
	}
	ir_emit_cmp_int_common(ctx, type, op1_reg, op1, op2_reg, op2);
	ir_emit_jcc(ctx, b, def, insn, next_block, op, 1);
}

static void ir_emit_cmp_and_branch_fp(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn, uint32_t next_block)
{
	ir_op op = ir_emit_cmp_fp_common(ctx, def, insn->op2, &ctx->ir_base[insn->op2]);
	ir_emit_jcc(ctx, b, def, insn, next_block, op, 0);
}

static void ir_emit_if_int(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn, uint32_t next_block)
{
	ir_type type = ctx->ir_base[insn->op2].type;
	ir_reg op2_reg = ctx->regs[def][2];
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (IR_IS_CONST_REF(insn->op2)) {
		uint32_t true_block, false_block;

		ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
		if (ir_const_is_true(&ctx->ir_base[insn->op2])) {
			if (true_block != next_block) {
				|	b =>true_block
			}
		} else {
			if (false_block != next_block) {
				|	b =>false_block
			}
		}
		return;
	} else if (ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA) {
		uint32_t true_block, false_block;

		ir_get_true_false_blocks(ctx, b, &true_block, &false_block);
		if (true_block != next_block) {
			|	b =>true_block
		}
		return;
	}
	IR_ASSERT(op2_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, type, op2_reg, insn->op2);
	}
	|	ASM_REG_IMM_OP cmp, type, op2_reg, 0
	ir_emit_jcc(ctx, b, def, insn, next_block, IR_NE, 1);
}

static void ir_emit_cond(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type = insn->type;
	ir_ref op1 = insn->op1;
	ir_ref op2 = insn->op2;
	ir_ref op3 = insn->op3;
	ir_type op1_type = ctx->ir_base[op1].type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];
	ir_reg op2_reg = ctx->regs[def][2];
	ir_reg op3_reg = ctx->regs[def][3];

	IR_ASSERT(def_reg != IR_REG_NONE);

	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, type, op2_reg, op2);
		if (op1 == op2) {
			op1_reg = op2_reg;
		}
		if (op3 == op2) {
			op3_reg = op2_reg;
		}
	}
	if (IR_REG_SPILLED(op3_reg)) {
		op3_reg = IR_REG_NUM(op3_reg);
		ir_emit_load(ctx, type, op3_reg, op3);
		if (op1 == op3) {
			op1_reg = op3_reg;
		}
	}
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, op1_type, op1_reg, op1);
	}

	if (IR_IS_TYPE_INT(op1_type)) {
		|	ASM_REG_IMM_OP cmp, op1_type, op1_reg, 0
	} else{
		|	ASM_FP_REG_IMM_OP fcmp, op1_type, op1_reg, 0.0
	}

	if (IR_IS_TYPE_INT(type)) {
		if (ir_type_size[type] == 8) {
			|	csel Rx(def_reg), Rx(op2_reg), Rx(op3_reg), ne
		} else {
			|	csel Rw(def_reg), Rw(op2_reg), Rw(op3_reg), ne
		}
	} else{
		if (type == IR_DOUBLE) {
			|	fcsel Rd(def_reg-IR_REG_FP_FIRST), Rd(op2_reg-IR_REG_FP_FIRST), Rd(op3_reg-IR_REG_FP_FIRST), ne
		} else {
			|	fcsel Rs(def_reg-IR_REG_FP_FIRST), Rs(op2_reg-IR_REG_FP_FIRST), Rs(op3_reg-IR_REG_FP_FIRST), ne
		}
	}

	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_return_void(ir_ctx *ctx)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	ir_emit_epilogue(ctx);
	|	ret
}

static void ir_emit_return_int(ir_ctx *ctx, ir_ref ref, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	ir_reg ret_reg = data->ra_data.cc->int_ret_reg;
	ir_reg op2_reg = ctx->regs[ref][2];

	if (op2_reg != ret_reg) {
		ir_type type = ctx->ir_base[insn->op2].type;

		if (op2_reg != IR_REG_NONE && !IR_REG_SPILLED(op2_reg)) {
			ir_emit_mov(ctx, type, ret_reg, op2_reg);
		} else {
			ir_emit_load(ctx, type, ret_reg, insn->op2);
		}
	}
	ir_emit_return_void(ctx);
}

static void ir_emit_return_fp(ir_ctx *ctx, ir_ref ref, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	ir_reg ret_reg = data->ra_data.cc->fp_ret_reg;
	ir_reg op2_reg = ctx->regs[ref][2];
	ir_type type = ctx->ir_base[insn->op2].type;

	if (op2_reg != ret_reg) {
		if (op2_reg != IR_REG_NONE && !IR_REG_SPILLED(op2_reg)) {
			ir_emit_fp_mov(ctx, type, ret_reg, op2_reg);
		} else {
			ir_emit_load(ctx, type, ret_reg, insn->op2);
		}
	}
	ir_emit_return_void(ctx);
}

static void ir_emit_sext(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_TYPE_INT(src_type));
	IR_ASSERT(IR_IS_TYPE_INT(dst_type));
	IR_ASSERT(ir_type_size[dst_type] > ir_type_size[src_type]);
	IR_ASSERT(def_reg != IR_REG_NONE);
	if ((op1_reg != IR_REG_NONE) && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}

	if (op1_reg != IR_REG_NONE) {
		if (ir_type_size[src_type] == 1) {
			if (ir_type_size[dst_type] == 2) {
				|	sxtb Rw(def_reg), Rw(op1_reg)
			} else if (ir_type_size[dst_type] == 4) {
				|	sxtb Rw(def_reg), Rw(op1_reg)
			} else {
				IR_ASSERT(ir_type_size[dst_type] == 8);
				|	sxtb Rx(def_reg), Rx(op1_reg)
			}
		} else if (ir_type_size[src_type] == 2) {
			if (ir_type_size[dst_type] == 4) {
				|	sxth Rw(def_reg), Rw(op1_reg)
			} else {
				IR_ASSERT(ir_type_size[dst_type] == 8);
				|	sxth Rx(def_reg), Rx(op1_reg)
			}
		} else {
			IR_ASSERT(ir_type_size[src_type] == 4);
			IR_ASSERT(ir_type_size[dst_type] == 8);
			|	sxtw Rx(def_reg), Rw(op1_reg)
		}
	} else if (IR_IS_CONST_REF(insn->op1)) {
		IR_ASSERT(0);
	} else {
		ir_reg fp;
		int32_t offset = ir_ref_spill_slot_offset(ctx, insn->op1, &fp);

		if (ir_type_size[src_type] == 1) {
			if (aarch64_may_encode_addr_offset(offset, ir_type_size[src_type])) {
				if (ir_type_size[dst_type] == 2) {
					|	ldrsb Rw(def_reg), [Rx(fp), #offset]
				} else if (ir_type_size[dst_type] == 4) {
					|	ldrsb Rw(def_reg), [Rx(fp), #offset]
				} else {
					IR_ASSERT(ir_type_size[dst_type] == 8);
					|	ldrsb Rx(def_reg), [Rx(fp), #offset]
				}
			} else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
				if (ir_type_size[dst_type] == 2) {
					|	ldrsb Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				} else if (ir_type_size[dst_type] == 4) {
					|	ldrsb Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				} else {
					IR_ASSERT(ir_type_size[dst_type] == 8);
					|	ldrsb Rx(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			}
		} else if (ir_type_size[src_type] == 2) {
			if (aarch64_may_encode_addr_offset(offset, ir_type_size[src_type])) {
				if (ir_type_size[dst_type] == 4) {
					|	ldrsh Rw(def_reg), [Rx(fp), #offset]
				} else {
					IR_ASSERT(ir_type_size[dst_type] == 8);
					|	ldrsh Rx(def_reg), [Rx(fp), #offset]
				}
			} else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
				if (ir_type_size[dst_type] == 4) {
					|	ldrsh Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				} else {
					IR_ASSERT(ir_type_size[dst_type] == 8);
					|	ldrsh Rx(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			}
		} else {
			IR_ASSERT(ir_type_size[src_type] == 4);
			IR_ASSERT(ir_type_size[dst_type] == 8);
			if (aarch64_may_encode_addr_offset(offset, ir_type_size[src_type])) {
				|	ldrsw Rx(def_reg), [Rx(fp), #offset]
			} else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
				|	ldrsw Rx(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
			}
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_zext(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_TYPE_INT(src_type));
	IR_ASSERT(IR_IS_TYPE_INT(dst_type));
	IR_ASSERT(ir_type_size[dst_type] > ir_type_size[src_type]);
	IR_ASSERT(def_reg != IR_REG_NONE);
	if ((op1_reg != IR_REG_NONE) && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}

	if (op1_reg != IR_REG_NONE) {
		if (ir_type_size[src_type] == 1) {
			|	uxtb Rw(def_reg), Rw(op1_reg)
		} else if (ir_type_size[src_type] == 2) {
			|	uxth Rw(def_reg), Rw(op1_reg)
		} else {
			|	mov Rw(def_reg), Rw(op1_reg)
		}
	} else if (IR_IS_CONST_REF(insn->op1)) {
		IR_ASSERT(0);
	} else {
		ir_reg fp;
		int32_t offset = ir_ref_spill_slot_offset(ctx, insn->op1, &fp);

		if (aarch64_may_encode_addr_offset(offset, ir_type_size[src_type])) {
			if (ir_type_size[src_type] == 1) {
				|	ldrb Rw(def_reg), [Rx(fp), #offset]
			} else if (ir_type_size[src_type] == 2) {
				|	ldrh Rw(def_reg), [Rx(fp), #offset]
			} else {
				IR_ASSERT(ir_type_size[src_type] == 4);
				IR_ASSERT(ir_type_size[dst_type] == 8);
				|	ldr Rw(def_reg), [Rx(fp), #offset]
			}
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
			if (ir_type_size[src_type] == 1) {
				|	ldrb Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
			} else if (ir_type_size[src_type] == 2) {
				|	ldrh Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
			} else {
				IR_ASSERT(ir_type_size[src_type] == 4);
				IR_ASSERT(ir_type_size[dst_type] == 8);
				|	ldr Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
			}
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_trunc(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_TYPE_INT(src_type));
	IR_ASSERT(IR_IS_TYPE_INT(dst_type));
	IR_ASSERT(ir_type_size[dst_type] < ir_type_size[src_type]);
	IR_ASSERT(def_reg != IR_REG_NONE);
	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}
	if (op1_reg != IR_REG_NONE) {
		if (ir_type_size[dst_type] == 1) {
			|	and Rw(def_reg), Rw(op1_reg), #0xff
		} else if (ir_type_size[dst_type] == 2) {
			|	and Rw(def_reg), Rw(op1_reg), #0xffff
		} else if (op1_reg != def_reg) {
			ir_emit_mov(ctx, dst_type, def_reg, op1_reg);
		}
	} else {
		ir_emit_load(ctx, dst_type, def_reg, insn->op1);
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_bitcast(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(ir_type_size[dst_type] == ir_type_size[src_type]);
	IR_ASSERT(def_reg != IR_REG_NONE);
	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}
	if (IR_IS_TYPE_INT(src_type) && IR_IS_TYPE_INT(dst_type)) {
		if (op1_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op1_reg)) {
				op1_reg = IR_REG_NUM(op1_reg);
				ir_emit_load(ctx, src_type, op1_reg, insn->op1);
			}
			if (op1_reg != def_reg) {
				ir_emit_mov(ctx, dst_type, def_reg, op1_reg);
			}
		} else {
			ir_emit_load(ctx, dst_type, def_reg, insn->op1);
		}
	} else if (IR_IS_TYPE_FP(src_type) && IR_IS_TYPE_FP(dst_type)) {
		if (op1_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op1_reg)) {
				op1_reg = IR_REG_NUM(op1_reg);
				ir_emit_load(ctx, src_type, op1_reg, insn->op1);
			}
			if (op1_reg != def_reg) {
				ir_emit_fp_mov(ctx, dst_type, def_reg, op1_reg);
			}
		} else {
			ir_emit_load(ctx, dst_type, def_reg, insn->op1);
		}
	} else if (IR_IS_TYPE_FP(src_type)) {
		IR_ASSERT(IR_IS_TYPE_INT(dst_type));
		if (op1_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op1_reg)) {
				op1_reg = IR_REG_NUM(op1_reg);
				ir_emit_load(ctx, src_type, op1_reg, insn->op1);
			}
			if (src_type == IR_DOUBLE) {
				|	fmov Rx(def_reg), Rd(op1_reg-IR_REG_FP_FIRST)
			} else {
				IR_ASSERT(src_type == IR_FLOAT);
				|	fmov Rw(def_reg), Rs(op1_reg-IR_REG_FP_FIRST)
			}
		} else if (IR_IS_CONST_REF(insn->op1)) {
			IR_ASSERT(0); //???
		} else {
			ir_reg fp;
			int32_t offset = ir_ref_spill_slot_offset(ctx, insn->op1, &fp);

			if (aarch64_may_encode_addr_offset(offset, ir_type_size[src_type])) {
				if (src_type == IR_DOUBLE) {
					|	ldr Rx(def_reg), [Rx(fp), #offset]
				} else {
					IR_ASSERT(src_type == IR_FLOAT);
					|	ldr Rw(def_reg), [Rx(fp), #offset]
				}
			} else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
				if (src_type == IR_DOUBLE) {
					|	ldr Rx(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				} else {
					IR_ASSERT(src_type == IR_FLOAT);
					|	ldr Rw(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			}
		}
	} else if (IR_IS_TYPE_FP(dst_type)) {
		IR_ASSERT(IR_IS_TYPE_INT(src_type));
		if (op1_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op1_reg)) {
				op1_reg = IR_REG_NUM(op1_reg);
				ir_emit_load(ctx, src_type, op1_reg, insn->op1);
			}
			if (dst_type == IR_DOUBLE) {
				|	fmov Rd(def_reg-IR_REG_FP_FIRST), Rx(op1_reg)
			} else {
				IR_ASSERT(dst_type == IR_FLOAT);
				|	fmov Rs(def_reg-IR_REG_FP_FIRST), Rw(op1_reg)
			}
		} else if (IR_IS_CONST_REF(insn->op1)) {
			IR_ASSERT(0); //???
		} else {
			ir_reg fp;
			int32_t offset = ir_ref_spill_slot_offset(ctx, insn->op1, &fp);

			if (aarch64_may_encode_addr_offset(offset, ir_type_size[src_type])) {
				if (dst_type == IR_DOUBLE) {
					|	ldr Rd(def_reg), [Rx(fp), #offset]
				} else {
					IR_ASSERT(dst_type == IR_FLOAT);
					|	ldr Rs(def_reg), [Rx(fp), #offset]
				}
			 } else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
				if (dst_type == IR_DOUBLE) {
					|	ldr Rd(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				} else {
					IR_ASSERT(dst_type == IR_FLOAT);
					|	ldr Rs(def_reg), [Rx(fp), Rx(IR_REG_INT_TMP)]
				}
			 }
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_int2fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_TYPE_INT(src_type));
	IR_ASSERT(IR_IS_TYPE_FP(dst_type));
	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}

	if (ir_type_size[src_type] == 8) {
		if (IR_IS_TYPE_SIGNED(src_type)) {
			if (dst_type == IR_DOUBLE) {
				|	scvtf Rd(def_reg-IR_REG_FP_FIRST), Rx(op1_reg)
			} else {
				IR_ASSERT(dst_type == IR_FLOAT);
				|	scvtf Rs(def_reg-IR_REG_FP_FIRST), Rx(op1_reg)
			}
		} else {
			if (dst_type == IR_DOUBLE) {
				|	ucvtf Rd(def_reg-IR_REG_FP_FIRST), Rx(op1_reg)
			} else {
				IR_ASSERT(dst_type == IR_FLOAT);
				|	ucvtf Rs(def_reg-IR_REG_FP_FIRST), Rx(op1_reg)
			}
		}
	} else {
		if (IR_IS_TYPE_SIGNED(src_type)) {
			if (ir_type_size[src_type] == 2) {
				ir_emit_fix_type(ctx, IR_I16, op1_reg);
			} else if (ir_type_size[src_type] == 1) {
				ir_emit_fix_type(ctx, IR_I8, op1_reg);
			}
			if (dst_type == IR_DOUBLE) {
				|	scvtf Rd(def_reg-IR_REG_FP_FIRST), Rw(op1_reg)
			} else {
				IR_ASSERT(dst_type == IR_FLOAT);
				|	scvtf Rs(def_reg-IR_REG_FP_FIRST), Rw(op1_reg)
			}
		} else {
			if (ir_type_size[src_type] == 2) {
				ir_emit_fix_type(ctx, IR_U16, op1_reg);
			} else if (ir_type_size[src_type] == 1) {
				ir_emit_fix_type(ctx, IR_U8, op1_reg);
			}
			if (dst_type == IR_DOUBLE) {
				|	ucvtf Rd(def_reg-IR_REG_FP_FIRST), Rw(op1_reg)
			} else {
				IR_ASSERT(dst_type == IR_FLOAT);
				|	ucvtf Rs(def_reg-IR_REG_FP_FIRST), Rw(op1_reg)
			}
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_fp2int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_TYPE_FP(src_type));
	IR_ASSERT(IR_IS_TYPE_INT(dst_type));
	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}
	if (ir_type_size[dst_type] == 8) {
		if (IR_IS_TYPE_SIGNED(dst_type)) {
			if (src_type == IR_DOUBLE) {
				|	fcvtzs Rx(def_reg), Rd(op1_reg-IR_REG_FP_FIRST)
			} else {
				IR_ASSERT(src_type == IR_FLOAT);
				|	fcvtzs Rx(def_reg), Rs(op1_reg-IR_REG_FP_FIRST)
			}
		} else {
			if (src_type == IR_DOUBLE) {
				|	fcvtzu Rx(def_reg), Rd(op1_reg-IR_REG_FP_FIRST)
			} else {
				IR_ASSERT(src_type == IR_FLOAT);
				|	fcvtzu Rx(def_reg), Rs(op1_reg-IR_REG_FP_FIRST)
			}
		}
	} else {
		if (IR_IS_TYPE_SIGNED(dst_type)) {
			if (src_type == IR_DOUBLE) {
				|	fcvtzs Rw(def_reg), Rd(op1_reg-IR_REG_FP_FIRST)
			} else {
				IR_ASSERT(src_type == IR_FLOAT);
				|	fcvtzs Rw(def_reg), Rs(op1_reg-IR_REG_FP_FIRST)
			}
		} else {
			if (src_type == IR_DOUBLE) {
				|	fcvtzu Rw(def_reg), Rd(op1_reg-IR_REG_FP_FIRST)
			} else {
				IR_ASSERT(src_type == IR_FLOAT);
				|	fcvtzu Rw(def_reg), Rs(op1_reg-IR_REG_FP_FIRST)
			}
		}
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_fp2fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type dst_type = insn->type;
	ir_type src_type = ctx->ir_base[insn->op1].type;
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(IR_IS_TYPE_FP(src_type));
	IR_ASSERT(IR_IS_TYPE_FP(dst_type));
	IR_ASSERT(def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, src_type, op1_reg, insn->op1);
	}
	if (src_type == dst_type) {
		if (op1_reg != def_reg) {
			ir_emit_fp_mov(ctx, dst_type, def_reg, op1_reg);
		}
	} else if (src_type == IR_DOUBLE) {
		|	fcvt Rs(def_reg-IR_REG_FP_FIRST), Rd(op1_reg-IR_REG_FP_FIRST)
	} else {
		IR_ASSERT(src_type == IR_FLOAT);
		|	fcvt Rd(def_reg-IR_REG_FP_FIRST), Rs(op1_reg-IR_REG_FP_FIRST)
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, dst_type, def, def_reg);
	}
}

static void ir_emit_copy_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_ref type = insn->type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(def_reg != IR_REG_NONE || op1_reg != IR_REG_NONE);
	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, insn->op1);
	}
	if (def_reg == op1_reg) {
		/* same reg */
	} else if (def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE) {
		ir_emit_mov(ctx, type, def_reg, op1_reg);
	} else if (def_reg != IR_REG_NONE) {
		ir_emit_load(ctx, type, def_reg, insn->op1);
	} else if (op1_reg != IR_REG_NONE) {
		ir_emit_store(ctx, type, def, op1_reg);
	} else {
		IR_ASSERT(0);
	}
	if (def_reg != IR_REG_NONE && IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_copy_fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_type type = insn->type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg op1_reg = ctx->regs[def][1];

	IR_ASSERT(def_reg != IR_REG_NONE || op1_reg != IR_REG_NONE);
	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, insn->op1);
	}
	if (def_reg == op1_reg) {
		/* same reg */
	} else if (def_reg != IR_REG_NONE && op1_reg != IR_REG_NONE) {
		ir_emit_fp_mov(ctx, type, def_reg, op1_reg);
	} else if (def_reg != IR_REG_NONE) {
		ir_emit_load(ctx, type, def_reg, insn->op1);
	} else if (op1_reg != IR_REG_NONE) {
		ir_emit_store(ctx, type, def, op1_reg);
	} else {
		IR_ASSERT(0);
	}
	if (def_reg != IR_REG_NONE && IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_vaddr(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_ref type = insn->type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	int32_t offset;
	ir_reg fp;

	IR_ASSERT(def_reg != IR_REG_NONE);
	offset = ir_var_spill_slot(ctx, insn->op1, &fp);
	if (aarch64_may_encode_imm12(offset)) {
		|	add Rx(def_reg), Rx(fp), #offset
	} else {
		ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, offset);
		|	add Rx(def_reg), Rx(fp), Rx(IR_REG_INT_TMP)
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_vload(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_insn *var_insn = &ctx->ir_base[insn->op2];
	ir_ref type = insn->type;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_reg fp;
	int32_t offset;
	ir_mem mem;

	if (ctx->use_lists[def].count == 1) {
		/* dead load */
		return;
	}
	IR_ASSERT(var_insn->op == IR_VAR);
	fp = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
	offset = IR_SPILL_POS_TO_OFFSET(var_insn->op3);
	mem = IR_MEM_BO(fp, offset);
	if (def_reg == IR_REG_NONE && ir_is_same_mem_var(ctx, def, var_insn->op3)) {
		return; // fake load
	}
	IR_ASSERT(def_reg != IR_REG_NONE);
	ir_emit_load_mem(ctx, type, def_reg, mem);
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_vstore(ir_ctx *ctx, ir_ref ref, ir_insn *insn)
{
	ir_insn *var_insn = &ctx->ir_base[insn->op2];
	ir_insn *val_insn = &ctx->ir_base[insn->op3];
	ir_ref type = val_insn->type;
	ir_reg op3_reg = ctx->regs[ref][3];
	ir_reg fp;
	int32_t offset;
	ir_mem mem;

	IR_ASSERT(var_insn->op == IR_VAR);
	fp = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
	offset = IR_SPILL_POS_TO_OFFSET(var_insn->op3);
	IR_ASSERT(op3_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op3_reg)
	 && !IR_IS_CONST_REF(insn->op3)
	 && ir_rule(ctx, insn->op3) != IR_STATIC_ALLOCA
	 && ir_is_same_mem_var(ctx, insn->op3, var_insn->op3)) {
		return; // fake store
	}
	if (IR_REG_SPILLED(op3_reg)) {
		op3_reg = IR_REG_NUM(op3_reg);
		ir_emit_load(ctx, type, op3_reg, insn->op3);
	}
	mem = IR_MEM_BO(fp, offset);
	ir_emit_store_mem(ctx, type, mem, op3_reg);
}

static ir_mem ir_fuse_addr(ir_ctx *ctx, ir_ref root, ir_ref ref)
{
	ir_insn *addr_insn = &ctx->ir_base[ref];
	ir_reg reg;
	int32_t offset;

	if (addr_insn->op == IR_ADD) {
		IR_ASSERT(!IR_IS_CONST_REF(addr_insn->op1) && IR_IS_CONST_REF(addr_insn->op2));
		IR_ASSERT(!IR_IS_SYM_CONST(ctx->ir_base[addr_insn->op2].op));
		if (ir_rule(ctx, addr_insn->op1) == IR_STATIC_ALLOCA) {
			reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			offset = ir_local_offset(ctx, &ctx->ir_base[addr_insn->op1]);
			offset += ctx->ir_base[addr_insn->op2].val.i32;
			return IR_MEM_BO(reg, offset);
		} else {
			if (UNEXPECTED(ctx->rules[ref] & IR_FUSED_REG)) {
				reg = ir_get_fused_reg(ctx, root, ref * sizeof(ir_ref) + 1);
			} else {
				reg = ctx->regs[ref][1];
			}
			if (IR_REG_SPILLED(reg)) {
				reg = IR_REG_NUM(reg);
				ir_emit_load(ctx, IR_ADDR, reg, addr_insn->op1);
			}
			return IR_MEM_BO(reg, ctx->ir_base[addr_insn->op2].val.i32);
		}
	} else {
		IR_ASSERT(addr_insn->op == IR_ALLOCA || addr_insn->op == IR_VADDR);
		reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
		offset = ir_local_offset(ctx, &ctx->ir_base[ref]);
		return IR_MEM_BO(reg, offset);
	}
}

static void ir_emit_load_int(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_ref type = insn->type;
	ir_reg op2_reg = ctx->regs[def][2];
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_mem mem;

	if (ctx->use_lists[def].count == 1) {
		/* dead load */
		return;
	}
	IR_ASSERT(def_reg != IR_REG_NONE);
	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			IR_ASSERT(ctx->ir_base[insn->op2].type == IR_ADDR);
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		}
		mem = IR_MEM_B(op2_reg);
	} else if (IR_IS_CONST_REF(insn->op2)) {
		op2_reg = def_reg;
		ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		mem = IR_MEM_B(op2_reg);
	} else {
		IR_ASSERT(ir_rule(ctx, insn->op2) & IR_FUSED);
		mem = ir_fuse_addr(ctx, def, insn->op2);
		if (IR_REG_SPILLED(ctx->regs[def][0]) && ir_is_same_spill_slot(ctx, def, mem)) {
			if (!ir_may_avoid_spill_load(ctx, def, def)) {
				ir_emit_load_mem_int(ctx, type, def_reg, mem);
			}
			/* avoid load to the same location (valid only when register is not reused) */
			return;
		}
	}
	ir_emit_load_mem_int(ctx, type, def_reg, mem);
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_load_fp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_ref type = insn->type;
	ir_reg op2_reg = ctx->regs[def][2];
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
	ir_mem mem;

	if (ctx->use_lists[def].count == 1) {
		/* dead load */
		return;
	}
	IR_ASSERT(def_reg != IR_REG_NONE);
	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			IR_ASSERT(ctx->ir_base[insn->op2].type == IR_ADDR);
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		}
		mem = IR_MEM_B(op2_reg);
	} else if (IR_IS_CONST_REF(insn->op2)) {
		op2_reg = def_reg;
		ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		mem = IR_MEM_B(op2_reg);
	} else {
		IR_ASSERT(ir_rule(ctx, insn->op2) & IR_FUSED);
		mem = ir_fuse_addr(ctx, def, insn->op2);
		if (IR_REG_SPILLED(ctx->regs[def][0]) && ir_is_same_spill_slot(ctx, def, mem)) {
			if (!ir_may_avoid_spill_load(ctx, def, def)) {
				ir_emit_load_mem_fp(ctx, type, def_reg, mem);
			}
			/* avoid load to the same location (valid only when register is not reused) */
			return;
		}
	}
	ir_emit_load_mem_fp(ctx, type, def_reg, mem);
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, type, def, def_reg);
	}
}

static void ir_emit_store_int(ir_ctx *ctx, ir_ref ref, ir_insn *insn)
{
	ir_insn *val_insn = &ctx->ir_base[insn->op3];
	ir_ref type = val_insn->type;
	ir_reg op2_reg = ctx->regs[ref][2];
	ir_reg op3_reg = ctx->regs[ref][3];
	ir_mem mem;

	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			IR_ASSERT(ctx->ir_base[insn->op2].type == IR_ADDR);
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		}
		mem = IR_MEM_B(op2_reg);
	} else {
		IR_ASSERT(!IR_IS_CONST_REF(insn->op2) && (ir_rule(ctx, insn->op2) & IR_FUSED));
		mem = ir_fuse_addr(ctx, ref, insn->op2);
		if (!IR_IS_CONST_REF(insn->op3)
		 && IR_REG_SPILLED(op3_reg)
		 && ir_rule(ctx, insn->op3) != IR_STATIC_ALLOCA
		 && ir_is_same_spill_slot(ctx, insn->op3, mem)) {
			if (!ir_may_avoid_spill_load(ctx, insn->op3, ref)) {
				op3_reg = IR_REG_NUM(op3_reg);
				ir_emit_load(ctx, type, op3_reg, insn->op3);
			}
			/* avoid store to the same location */
			return;
		}
	}

	if (op3_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op3_reg)) {
			op3_reg = IR_REG_NUM(op3_reg);
			ir_emit_load(ctx, type, op3_reg, insn->op3);
		}
	} else {
		IR_ASSERT(IR_IS_CONST_REF(insn->op3) && !IR_IS_SYM_CONST(ctx->ir_base[insn->op3].op) && ctx->ir_base[insn->op3].val.i64 == 0);
		op3_reg = IR_REG_ZR;
	}
	ir_emit_store_mem_int(ctx, type, mem, op3_reg);
}

static void ir_emit_store_fp(ir_ctx *ctx, ir_ref ref, ir_insn *insn)
{
	ir_ref type = ctx->ir_base[insn->op3].type;
	ir_reg op2_reg = ctx->regs[ref][2];
	ir_reg op3_reg = ctx->regs[ref][3];
	ir_mem mem;

	IR_ASSERT(op3_reg != IR_REG_NONE);
	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			IR_ASSERT(ctx->ir_base[insn->op2].type == IR_ADDR);
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		}
		mem = IR_MEM_B(op2_reg);
	} else {
		IR_ASSERT(!IR_IS_CONST_REF(insn->op2) && (ir_rule(ctx, insn->op2) & IR_FUSED));
		mem = ir_fuse_addr(ctx, ref, insn->op2);
		if (!IR_IS_CONST_REF(insn->op3)
		 && IR_REG_SPILLED(op3_reg)
		 && ir_rule(ctx, insn->op3) != IR_STATIC_ALLOCA
		 && ir_is_same_spill_slot(ctx, insn->op3, mem)) {
			if (!ir_may_avoid_spill_load(ctx, insn->op3, ref)) {
				op3_reg = IR_REG_NUM(op3_reg);
				ir_emit_load(ctx, type, op3_reg, insn->op3);
			}
			/* avoid store to the same location */
			return;
		}
	}

	if (IR_REG_SPILLED(op3_reg)) {
		op3_reg = IR_REG_NUM(op3_reg);
		ir_emit_load(ctx, type, op3_reg, insn->op3);
	}
	ir_emit_store_mem_fp(ctx, type, mem, op3_reg);
}

static void ir_emit_rload(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_reg src_reg = insn->op2;
	ir_type type = insn->type;

	if (IR_REGSET_IN(IR_REGSET_UNION((ir_regset)ctx->fixed_regset, IR_REGSET_FIXED), src_reg)) {
		if (ctx->vregs[def]
		 && ctx->live_intervals[ctx->vregs[def]]
		 && ctx->live_intervals[ctx->vregs[def]]->stack_spill_pos != -1) {
			ir_emit_store(ctx, type, def, src_reg);
		}
	} else {
		ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);

		if (def_reg == IR_REG_NONE) {
			/* op3 is used as a flag that the value is already stored in memory.
			 * If op3 is set we don't have to store the value once again (in case of spilling)
			 */
			if (!insn->op3 || !ir_is_same_spill_slot(ctx, def, IR_MEM_BO(ctx->spill_base, insn->op3))) {
				ir_emit_store(ctx, type, def, src_reg);
			}
		} else {
			if (src_reg != def_reg) {
				if (IR_IS_TYPE_INT(type)) {
					ir_emit_mov(ctx, type, def_reg, src_reg);
				} else {
					IR_ASSERT(IR_IS_TYPE_FP(type));
					ir_emit_fp_mov(ctx, type, def_reg, src_reg);
				}
			}
			if (IR_REG_SPILLED(ctx->regs[def][0])
			 && (!insn->op3 || !ir_is_same_spill_slot(ctx, def, IR_MEM_BO(ctx->spill_base, insn->op3)))) {
				ir_emit_store(ctx, type, def, def_reg);
			}
		}
	}
}

static void ir_emit_rstore(ir_ctx *ctx, ir_ref ref, ir_insn *insn)
{
	ir_ref type = ctx->ir_base[insn->op2].type;
	ir_reg op2_reg = ctx->regs[ref][2];
	ir_reg dst_reg = insn->op3;

	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			ir_emit_load(ctx, type, op2_reg, insn->op2);
		}
		if (op2_reg != dst_reg) {
			if (IR_IS_TYPE_INT(type)) {
				ir_emit_mov(ctx, type, dst_reg, op2_reg);
			} else {
				IR_ASSERT(IR_IS_TYPE_FP(type));
				ir_emit_fp_mov(ctx, type, dst_reg, op2_reg);
			}
		}
	} else {
		ir_emit_load(ctx, type, dst_reg, insn->op2);
	}
}

static void ir_emit_alloca(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);

	if (ctx->use_lists[def].count == 1) {
		/* dead alloca */
		return;
	}
	if (IR_IS_CONST_REF(insn->op2)) {
		ir_insn *val = &ctx->ir_base[insn->op2];
		int32_t size = val->val.i32;

		IR_ASSERT(IR_IS_TYPE_INT(val->type));
		IR_ASSERT(!IR_IS_SYM_CONST(val->op));
		IR_ASSERT(IR_IS_TYPE_UNSIGNED(val->type) || val->val.i64 >= 0);

		/* Stack must be 16 byte aligned */
		size = IR_ALIGNED_SIZE(size, 16);
		if (aarch64_may_encode_imm12(size)) {
			|	sub sp, sp, #size
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, size);
			|	sub sp, sp, Rx(IR_REG_INT_TMP)
		}
		if (!(ctx->flags & IR_USE_FRAME_POINTER)) {
			ctx->call_stack_size += size;
		}
	} else {
		int32_t alignment = 16;
		ir_reg op2_reg = ctx->regs[def][2];
		ir_type type = ctx->ir_base[insn->op2].type;

		IR_ASSERT(ctx->flags & IR_FUNCTION);
		IR_ASSERT(ctx->flags & IR_USE_FRAME_POINTER);
		IR_ASSERT(def_reg != IR_REG_NONE && op2_reg != IR_REG_NONE);
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			ir_emit_load(ctx, type, op2_reg, insn->op2);
		}
		|	add Rx(def_reg), Rx(op2_reg), #(alignment-1)
		|	and Rx(def_reg), Rx(def_reg), #(~(alignment-1))
		|	sub sp, sp, Rx(def_reg);
	}
	if (def_reg != IR_REG_NONE) {
		|	mov Rx(def_reg), sp
		if (IR_REG_SPILLED(ctx->regs[def][0])) {
			ir_emit_store(ctx, insn->type, def, def_reg);
		}
	} else {
		ir_emit_store(ctx, IR_ADDR, def, IR_REG_STACK_POINTER);
	}
}

static void ir_emit_afree(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (IR_IS_CONST_REF(insn->op2)) {
		ir_insn *val = &ctx->ir_base[insn->op2];
		int32_t size = val->val.i32;

		IR_ASSERT(IR_IS_TYPE_INT(val->type));
		IR_ASSERT(!IR_IS_SYM_CONST(val->op));
		IR_ASSERT(IR_IS_TYPE_UNSIGNED(val->type) || val->val.i64 > 0);

		/* Stack must be 16 byte aligned */
		size = IR_ALIGNED_SIZE(size, 16);
		if (aarch64_may_encode_imm12(size)) {
			|	add sp, sp, #size
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, size);
			|	add sp, sp, Rx(IR_REG_INT_TMP)
		}
		if (!(ctx->flags & IR_USE_FRAME_POINTER)) {
			ctx->call_stack_size -= size;
		}
	} else {
//		int32_t alignment = 16;
		ir_reg op2_reg = ctx->regs[def][2];
		ir_type type = ctx->ir_base[insn->op2].type;

		IR_ASSERT(ctx->flags & IR_FUNCTION);
		IR_ASSERT(op2_reg != IR_REG_NONE);
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			ir_emit_load(ctx, type, op2_reg, insn->op2);
		}

		// TODO: alignment

		|	add sp, sp, Rx(op2_reg);
	}
}

static void ir_emit_block_begin(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);

	if (ctx->use_lists[def].count == 1) {
		/* dead load */
		return;
	}
	|	mov Rx(def_reg), sp

	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, IR_ADDR, def, def_reg);
	}
}

static void ir_emit_block_end(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg op2_reg = ctx->regs[def][2];

	IR_ASSERT(op2_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
	}

	|	mov sp, Rx(op2_reg)
}

static void ir_emit_frame_addr(ir_ctx *ctx, ir_ref def)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);

	if (ctx->flags & IR_USE_FRAME_POINTER) {
		|	mov Rx(def_reg), Rx(IR_REG_X29)
	} else if (aarch64_may_encode_imm12(ctx->stack_frame_size + ctx->call_stack_size)) {
		|	add Rx(def_reg), Rx(IR_REG_X31), #(ctx->stack_frame_size + ctx->call_stack_size)
	} else {
		ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, ctx->stack_frame_size + ctx->call_stack_size);
		|	add Rx(def_reg), Rx(IR_REG_X31), Rx(IR_REG_INT_TMP)
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, IR_ADDR, def, def_reg);
	}
}

static void ir_emit_va_start(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = data->ra_data.cc;
	dasm_State **Dst = &data->dasm_state;

	if (!cc->sysv_varargs) {
		ir_reg fp;
		int arg_area_offset;
		ir_reg op2_reg = ctx->regs[def][2];
		ir_reg tmp_reg = ctx->regs[def][3];
		int32_t offset;

		IR_ASSERT(tmp_reg != IR_REG_NONE);
		if (op2_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op2_reg)) {
				op2_reg = IR_REG_NUM(op2_reg);
				ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
			}
			offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA);
			op2_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			offset = ir_local_offset(ctx, &ctx->ir_base[insn->op2]);
		}

		if (ctx->flags & IR_USE_FRAME_POINTER) {
			fp = IR_REG_FRAME_POINTER;
			arg_area_offset = ctx->stack_frame_size + sizeof(void*) * 2 + ctx->param_stack_size;
		} else {
			fp = IR_REG_STACK_POINTER;
			arg_area_offset = ctx->call_stack_size + ctx->stack_frame_size + ctx->param_stack_size;
		}
		|	add Rx(tmp_reg), Rx(fp), #arg_area_offset
		|	str Rx(tmp_reg), [Rx(op2_reg), #offset]
	} else {
		ir_reg fp;
		int reg_save_area_offset;
		int overflow_arg_area_offset;
		ir_reg op2_reg = ctx->regs[def][2];
		ir_reg tmp_reg = ctx->regs[def][3];
		int32_t offset;

		IR_ASSERT(tmp_reg != IR_REG_NONE);
		if (op2_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op2_reg)) {
				op2_reg = IR_REG_NUM(op2_reg);
				ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
			}
			offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA);
			op2_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			offset = ir_local_offset(ctx, &ctx->ir_base[insn->op2]);
		}

		if (ctx->flags & IR_USE_FRAME_POINTER) {
			fp = IR_REG_FRAME_POINTER;
			reg_save_area_offset = ctx->locals_area_size + sizeof(void*) * 2;
			overflow_arg_area_offset = ctx->stack_frame_size + sizeof(void*) * 2 + ctx->param_stack_size;
		} else {
			fp = IR_REG_STACK_POINTER;
			reg_save_area_offset = ctx->locals_area_size + ctx->call_stack_size;
			overflow_arg_area_offset = ctx->call_stack_size + ctx->stack_frame_size + ctx->param_stack_size;
		}

		/* Set va_list.stack */
		|	add Rx(tmp_reg), Rx(fp), #overflow_arg_area_offset
		|	str Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, stack))]
		if ((ctx->flags2 & (IR_HAS_VA_ARG_GP|IR_HAS_VA_COPY)) && ctx->gp_reg_params < cc->int_param_regs_count) {
			reg_save_area_offset += sizeof(void*) * cc->int_param_regs_count;
			/* Set va_list.gr_top */
			if (overflow_arg_area_offset != reg_save_area_offset) {
				|	add Rx(tmp_reg), Rx(fp), #reg_save_area_offset
			}
			|	str Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, gr_top))]
			/* Set va_list.gr_offset */
			|	movn Rw(tmp_reg), #~(0 - (sizeof(void*) * (cc->int_param_regs_count - ctx->gp_reg_params)))
			|	str Rw(tmp_reg),  [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, gr_offset))]
		} else {
			/* Set va_list.gr_offset */
			|	str wzr,  [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, gr_offset))]
		}
		if ((ctx->flags2 & (IR_HAS_VA_ARG_FP|IR_HAS_VA_COPY)) && ctx->fp_reg_params < cc->fp_param_regs_count) {
			reg_save_area_offset += 16 * cc->fp_param_regs_count;
			/* Set va_list.vr_top */
			if (overflow_arg_area_offset != reg_save_area_offset || ctx->gp_reg_params < cc->int_param_regs_count) {
				|	add Rx(tmp_reg), Rx(fp), #reg_save_area_offset
			}
			|	str Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, vr_top))]
			/* Set va_list.vr_offset */
			|	movn Rw(tmp_reg), #~(0 - (16 * (cc->fp_param_regs_count - ctx->fp_reg_params)))
			|	str Rw(tmp_reg),  [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, vr_offset))]
		} else {
			/* Set va_list.vr_offset */
			|	str wzr,  [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, vr_offset))]
		}
	}
}

static void ir_emit_va_copy(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = data->ra_data.cc;
	dasm_State **Dst = &data->dasm_state;

	if (!cc->sysv_varargs) {
		ir_reg tmp_reg = ctx->regs[def][1];
		ir_reg op2_reg = ctx->regs[def][2];
		ir_reg op3_reg = ctx->regs[def][3];
		int32_t op2_offset, op3_offset;

		IR_ASSERT(tmp_reg != IR_REG_NONE);
		if (op2_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op2_reg)) {
				op2_reg = IR_REG_NUM(op2_reg);
				ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
			}
			op2_offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA);
			op2_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			op2_offset = ir_local_offset(ctx, &ctx->ir_base[insn->op2]);
		}
		if (op3_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op3_reg)) {
				op3_reg = IR_REG_NUM(op3_reg);
				ir_emit_load(ctx, IR_ADDR, op3_reg, insn->op3);
			}
			op3_offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op3) == IR_STATIC_ALLOCA);
			op3_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			op3_offset = ir_local_offset(ctx, &ctx->ir_base[insn->op3]);
		}
		|	ldr Rx(tmp_reg), [Rx(op3_reg), #op3_offset]
		|	str Rx(tmp_reg), [Rx(op2_reg), #op2_offset]
	} else {
		ir_reg tmp_reg = ctx->regs[def][1];
		ir_reg op2_reg = ctx->regs[def][2];
		ir_reg op3_reg = ctx->regs[def][3];
		int32_t op2_offset, op3_offset;

		IR_ASSERT(tmp_reg != IR_REG_NONE);
		if (op2_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op2_reg)) {
				op2_reg = IR_REG_NUM(op2_reg);
				ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
			}
			op2_offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA);
			op2_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			op2_offset = ir_local_offset(ctx, &ctx->ir_base[insn->op2]);
		}
		if (op3_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op3_reg)) {
				op3_reg = IR_REG_NUM(op3_reg);
				ir_emit_load(ctx, IR_ADDR, op3_reg, insn->op3);
			}
			op3_offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op3) == IR_STATIC_ALLOCA);
			op3_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			op3_offset = ir_local_offset(ctx, &ctx->ir_base[insn->op3]);
		}
		|	ldr Rx(tmp_reg), [Rx(op3_reg), #op3_offset]
		|	str Rx(tmp_reg), [Rx(op2_reg), #op2_offset]
		|	ldr Rx(tmp_reg), [Rx(op3_reg), #(op3_offset+8)]
		|	str Rx(tmp_reg), [Rx(op2_reg), #(op2_offset+8)]
		|	ldr Rx(tmp_reg), [Rx(op3_reg), #(op3_offset+16)]
		|	str Rx(tmp_reg), [Rx(op2_reg), #(op2_offset+16)]
		|	ldr Rx(tmp_reg), [Rx(op3_reg), #(op3_offset+24)]
		|	str Rx(tmp_reg), [Rx(op2_reg), #(op2_offset+24)]
	}
}

static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = data->ra_data.cc;
	dasm_State **Dst = &data->dasm_state;

	if (!cc->sysv_varargs) {
		ir_type type = insn->type;
		ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
		ir_reg op2_reg = ctx->regs[def][2];
		ir_reg tmp_reg = ctx->regs[def][3];
		int32_t offset;

		IR_ASSERT((def_reg != IR_REG_NONE || ctx->use_lists[def].count == 1) && tmp_reg != IR_REG_NONE);
		if (op2_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op2_reg)) {
				op2_reg = IR_REG_NUM(op2_reg);
				ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
			}
			offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA);
			op2_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			offset = ir_local_offset(ctx, &ctx->ir_base[insn->op2]);
		}
		|	ldr Rx(tmp_reg), [Rx(op2_reg), #offset]
		if (def_reg  != IR_REG_NONE) {
			ir_emit_load_mem(ctx, type, def_reg, IR_MEM_BO(tmp_reg, 0));
		}
		|	add Rx(tmp_reg), Rx(tmp_reg), #IR_MAX(ir_type_size[type], sizeof(void*))
		|	str Rx(tmp_reg), [Rx(op2_reg), #offset]
		if (def_reg != IR_REG_NONE && IR_REG_SPILLED(ctx->regs[def][0])) {
			ir_emit_store(ctx, type, def, def_reg);
		}
	} else {
		ir_type type = insn->type;
		ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);
		ir_reg op2_reg = ctx->regs[def][2];
		ir_reg tmp_reg = ctx->regs[def][3];
		int32_t offset;

		IR_ASSERT((def_reg != IR_REG_NONE || ctx->use_lists[def].count == 1) && tmp_reg != IR_REG_NONE);
		if (op2_reg != IR_REG_NONE) {
			if (IR_REG_SPILLED(op2_reg)) {
				op2_reg = IR_REG_NUM(op2_reg);
				ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
			}
			offset = 0;
		} else {
			IR_ASSERT(ir_rule(ctx, insn->op2) == IR_STATIC_ALLOCA);
			op2_reg = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;
			offset = ir_local_offset(ctx, &ctx->ir_base[insn->op2]);
		}
		if (IR_IS_TYPE_INT(type)) {
			|	ldr Rw(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, gr_offset))]
			|	cmp Rw(tmp_reg), wzr
			|	bge >1
			|	ldr Rx(IR_REG_INT_TMP), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, gr_top))]
			|	sxtw Rx(tmp_reg), Rw(tmp_reg)
			|	add Rx(IR_REG_INT_TMP), Rx(tmp_reg), Rx(IR_REG_INT_TMP)
			if (def_reg  != IR_REG_NONE) {
				|	ldr Rx(def_reg), [Rx(IR_REG_INT_TMP)]
			}
			|	add Rw(tmp_reg), Rw(tmp_reg), #sizeof(void*)
			|	str Rw(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, gr_offset))]
			|	b >2
			|1:
			|	ldr Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, stack))]
			if (def_reg  != IR_REG_NONE) {
				|	ldr Rx(def_reg), [Rx(tmp_reg)]
			}
			|	add Rx(tmp_reg), Rx(tmp_reg), #sizeof(void*)
			|	str Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, stack))]
			|2:
		} else {
			|	ldr Rw(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, vr_offset))]
			|	cmp Rw(tmp_reg), wzr
			|	bge >1
			|	ldr Rx(IR_REG_INT_TMP), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, vr_top))]
			|	sxtw Rx(tmp_reg), Rw(tmp_reg)
			|	add Rx(IR_REG_INT_TMP), Rx(tmp_reg), Rx(IR_REG_INT_TMP)
			if (def_reg  != IR_REG_NONE) {
				|	ldr Rd(def_reg-IR_REG_FP_FIRST), [Rx(IR_REG_INT_TMP)]
			}
			|	add Rw(tmp_reg), Rw(tmp_reg), #16
			|	str Rw(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, vr_offset))]
			|	b >2
			|1:
			|	ldr Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, stack))]
			if (def_reg  != IR_REG_NONE) {
				|	ldr Rd(def_reg-IR_REG_FP_FIRST), [Rx(tmp_reg)]
			}
			|	add Rx(tmp_reg), Rx(tmp_reg), #sizeof(void*)
			|	str Rx(tmp_reg), [Rx(op2_reg), #(offset+offsetof(ir_aarch64_sysv_va_list, stack))]
			|2:
		}
		if (def_reg != IR_REG_NONE && IR_REG_SPILLED(ctx->regs[def][0])) {
			ir_emit_store(ctx, type, def, def_reg);
		}
	}
}

static void ir_emit_switch(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_type type;
	ir_block *bb;
	ir_insn *use_insn, *val;
	uint32_t n, *p, use_block;
	int i;
	int label, default_label = 0;
	int count = 0;
	ir_val min, max;
	ir_reg op1_reg, op2_reg, tmp_reg;
	bool has_case_range = 0;

	type = ctx->ir_base[insn->op2].type;
	if (IR_IS_TYPE_SIGNED(type)) {
		min.u64 = 0x7fffffffffffffff;
		max.u64 = 0x8000000000000000;
	} else {
		min.u64 = 0xffffffffffffffff;
		max.u64 = 0x0;
	}

	bb = &ctx->cfg_blocks[b];
	p = &ctx->cfg_edges[bb->successors];
	for (n = bb->successors_count; n != 0; p++, n--) {
		use_block = *p;
		use_insn = &ctx->ir_base[ctx->cfg_blocks[use_block].start];
		if (use_insn->op == IR_CASE_VAL) {
			val = &ctx->ir_base[use_insn->op2];
			IR_ASSERT(!IR_IS_SYM_CONST(val->op));
			if (IR_IS_TYPE_SIGNED(type)) {
				IR_ASSERT(IR_IS_TYPE_SIGNED(val->type));
				min.i64 = IR_MIN(min.i64, val->val.i64);
				max.i64 = IR_MAX(max.i64, val->val.i64);
			} else {
				IR_ASSERT(!IR_IS_TYPE_SIGNED(val->type));
				min.u64 = (int64_t)IR_MIN(min.u64, val->val.u64);
				max.u64 = (int64_t)IR_MAX(max.u64, val->val.u64);
			}
			count++;
		} else if (use_insn->op == IR_CASE_RANGE) {
			has_case_range = 1;
			val = &ctx->ir_base[use_insn->op2];
			IR_ASSERT(!IR_IS_SYM_CONST(val->op));
			ir_insn *val2 = &ctx->ir_base[use_insn->op3];
			IR_ASSERT(!IR_IS_SYM_CONST(val2->op));
			if (IR_IS_TYPE_SIGNED(type)) {
				IR_ASSERT(IR_IS_TYPE_SIGNED(val->type));
				min.i64 = IR_MIN(min.i64, val->val.i64);
				max.i64 = IR_MAX(max.i64, val2->val.i64);
			} else {
				IR_ASSERT(!IR_IS_TYPE_SIGNED(val->type));
				min.u64 = (int64_t)IR_MIN(min.u64, val->val.u64);
				max.u64 = (int64_t)IR_MAX(max.u64, val2->val.u64);
			}
			count++;
		} else {
			IR_ASSERT(use_insn->op == IR_CASE_DEFAULT);
			default_label = ir_skip_empty_target_blocks(ctx, use_block);
		}
	}

	op1_reg = ctx->regs[def][1];
	op2_reg = ctx->regs[def][2];
	tmp_reg = ctx->regs[def][3];

	IR_ASSERT(op2_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, type, op2_reg, insn->op2);
	}

	/* Generate a table jmp or a sequence of calls */
	if (!has_case_range && count > 2 && (max.i64-min.i64) < count * 8) {
		int *labels = ir_mem_malloc(sizeof(int) * (max.i64 - min.i64 + 1));

		for (i = 0; i <= (max.i64 - min.i64); i++) {
			labels[i] = default_label;
		}
		p = &ctx->cfg_edges[bb->successors];
		for (n = bb->successors_count; n != 0; p++, n--) {
			use_block = *p;
			use_insn = &ctx->ir_base[ctx->cfg_blocks[use_block].start];
			if (use_insn->op == IR_CASE_VAL) {
				val = &ctx->ir_base[use_insn->op2];
				IR_ASSERT(!IR_IS_SYM_CONST(val->op));
				label = ir_skip_empty_target_blocks(ctx, use_block);
				labels[val->val.i64 - min.i64] = label;
			}
		}

		if (default_label) {
			if (aarch64_may_encode_imm12(max.i64)) {
				|	ASM_REG_IMM_OP cmp, type, op2_reg, max.i64
			} else {
				ir_emit_load_imm_int(ctx, type, tmp_reg, max.i64);
				|	ASM_REG_REG_OP cmp, type, op2_reg, tmp_reg
			}
			if (IR_IS_TYPE_SIGNED(type)) {
				|	bgt =>default_label
			} else {
				|	bhi =>default_label
			}
		}

		if (op1_reg == IR_REG_NONE) {
			op1_reg = op2_reg;
		}
		if (aarch64_may_encode_imm12(min.i64)) {
			|	ASM_REG_REG_IMM_OP subs, type, op1_reg, op2_reg, min.i64
		} else {
			ir_emit_load_imm_int(ctx, type, tmp_reg, min.i64);
			|	ASM_REG_REG_REG_OP subs, type, op1_reg, op2_reg, tmp_reg
		}

		if (default_label) {
			if (IR_IS_TYPE_SIGNED(type)) {
				|	blt =>default_label
			} else {
				|	blo =>default_label
			}
		}

		|	adr Rx(tmp_reg), >1
		|	ldr Rx(tmp_reg), [Rx(tmp_reg), Rx(op1_reg), lsl #3]
		|	br Rx(tmp_reg)
		|.jmp_table
		if (!data->jmp_table_label) {
			data->jmp_table_label = ctx->cfg_blocks_count + ctx->consts_count + 3;
			|=>data->jmp_table_label:
		}
		|.align 8
		|1:
		for (i = 0; i <= (max.i64 - min.i64); i++) {
			int b = labels[i];
			if (b) {
				ir_block *bb = &ctx->cfg_blocks[b];
				ir_insn *insn = &ctx->ir_base[bb->end];

				if (insn->op == IR_IJMP && IR_IS_CONST_REF(insn->op2)) {
					ir_ref prev = ctx->prev_ref[bb->end];
					if (prev != bb->start && ctx->ir_base[prev].op == IR_SNAPSHOT) {
						prev = ctx->prev_ref[prev];
					}
					if (prev == bb->start) {
						void *addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op2]);

						|	.addr &addr
						if (ctx->ir_base[bb->start].op1 == def
						 && ctx->ir_base[bb->start].op != IR_CASE_DEFAULT) {
							bb->flags |= IR_BB_EMPTY;
						}
						continue;
					}
				}
				|	.addr =>b
			} else {
				|	.addr 0
			}
		}
		|.code
		ir_mem_free(labels);
	} else {
		p = &ctx->cfg_edges[bb->successors];
		for (n = bb->successors_count; n != 0; p++, n--) {
			use_block = *p;
			use_insn = &ctx->ir_base[ctx->cfg_blocks[use_block].start];
			if (use_insn->op == IR_CASE_VAL) {
				val = &ctx->ir_base[use_insn->op2];
				IR_ASSERT(!IR_IS_SYM_CONST(val->op));
				label = ir_skip_empty_target_blocks(ctx, use_block);
				if (aarch64_may_encode_imm12(val->val.i64)) {
					|	ASM_REG_IMM_OP cmp, type, op2_reg, val->val.i64
				} else {
					ir_emit_load_imm_int(ctx, type, tmp_reg, val->val.i64);
					|	ASM_REG_REG_OP cmp, type, op2_reg, tmp_reg

				}
				|	beq =>label
			} else if (use_insn->op == IR_CASE_RANGE) {
				val = &ctx->ir_base[use_insn->op2];
				IR_ASSERT(!IR_IS_SYM_CONST(val->op));
				label = ir_skip_empty_target_blocks(ctx, use_block);
				if (aarch64_may_encode_imm12(val->val.i64)) {
					|	ASM_REG_IMM_OP cmp, type, op2_reg, val->val.i64
				} else {
					ir_emit_load_imm_int(ctx, type, tmp_reg, val->val.i64);
					|	ASM_REG_REG_OP cmp, type, op2_reg, tmp_reg

				}
				if (IR_IS_TYPE_SIGNED(type)) {
					|	blt >1
				} else {
					|	blo >1
				}
				val = &ctx->ir_base[use_insn->op3];
				IR_ASSERT(!IR_IS_SYM_CONST(val->op));
				label = ir_skip_empty_target_blocks(ctx, use_block);
				if (aarch64_may_encode_imm12(val->val.i64)) {
					|	ASM_REG_IMM_OP cmp, type, op2_reg, val->val.i64
				} else {
					ir_emit_load_imm_int(ctx, type, tmp_reg, val->val.i64);
					|	ASM_REG_REG_OP cmp, type, op2_reg, tmp_reg

				}
				if (IR_IS_TYPE_SIGNED(type)) {
					|	ble =>label
				} else {
					|	bls =>label
				}
				|1:
			}
		}
		if (default_label) {
			|	b =>default_label
		}
	}
}

static int32_t ir_call_used_stack(ir_ctx *ctx, ir_insn *insn, const ir_call_conv_dsc *cc, int32_t *copy_stack_ptr)
{
	int j, n;
	ir_type type;
	int int_param = 0;
	int fp_param = 0;
	int32_t used_stack = 0, copy_stack = 0;

	/* On APPLE "unnamed" arguments always passed through stack */
	int last_named_input;

	if (!cc->sysv_varargs) {
		const ir_proto_t *proto = ir_call_proto(ctx, insn);
		last_named_input = (proto && (proto->flags & IR_VARARG_FUNC)) ? proto->params_count + 2 : insn->inputs_count;
	} else {
		last_named_input = insn->inputs_count;
	}

	n = insn->inputs_count;
	for (j = 3; j <= n; j++) {
		ir_insn *arg = &ctx->ir_base[ir_insn_op(insn, j)];
		type = arg->type;
		if (arg->op == IR_ARGVAL) {
			int size = arg->op2;
			int align = arg->op3;
			copy_stack += size;
			align = IR_MAX((int)sizeof(void*), align);
			copy_stack = IR_ALIGNED_SIZE(copy_stack, align);
			type = IR_ADDR;
		}
		if (j > last_named_input) {
			used_stack += IR_MAX(sizeof(void*), ir_type_size[type]);
		} else if (IR_IS_TYPE_INT(type)) {
			if (int_param >= cc->int_param_regs_count) {
				used_stack += IR_MAX(sizeof(void*), ir_type_size[type]);
			}
			int_param++;
		} else {
			IR_ASSERT(IR_IS_TYPE_FP(type));
			if (fp_param >= cc->fp_param_regs_count) {
				used_stack += IR_MAX(sizeof(void*), ir_type_size[type]);
			}
			fp_param++;
		}
	}

	copy_stack = IR_ALIGNED_SIZE(copy_stack, 16);
	*copy_stack_ptr = copy_stack;
	return used_stack + copy_stack;
}

static int32_t ir_emit_arguments(ir_ctx *ctx, ir_ref def, ir_insn *insn, const ir_call_conv_dsc *cc, ir_reg tmp_reg)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	int j, n;
	ir_ref arg;
	ir_insn *arg_insn;
	uint8_t type;
	ir_reg src_reg, dst_reg;
	int int_param = 0;
	int fp_param = 0;
	int count = 0;
	int32_t used_stack, copy_stack = 0, stack_offset = 0, copy_stack_offset = 0;
	ir_copy *copies;
	bool do_pass3 = 0;
	/* For temporaries we may use any scratch registers except for registers used for parameters */
	ir_reg tmp_fp_reg = IR_REG_FP_LAST; /* Temporary register for FP loads and swap */

	n = insn->inputs_count;
	if (n < 3) {
		return 0;
	}

	if (tmp_reg == IR_REG_NONE) {
		tmp_reg = IR_REG_IP0;
	}

	if (insn->op == IR_CALL && (ctx->flags & IR_PREALLOCATED_STACK)) {
		// TODO: support for preallocated stack
		used_stack = 0;
	} else {
		used_stack = ir_call_used_stack(ctx, insn, cc, &copy_stack);
		/* Stack must be 16 byte aligned */
		used_stack = IR_ALIGNED_SIZE(used_stack, 16);
		if (ctx->fixed_call_stack_size && used_stack <= ctx->fixed_call_stack_size) {
			used_stack = 0;
		} else {
			ctx->call_stack_size += used_stack;
			if (used_stack) {
				if (insn->op == IR_TAILCALL && !(ctx->flags & IR_USE_FRAME_POINTER)) {
					ctx->flags |= IR_USE_FRAME_POINTER;
					|	stp x29, x30, [sp, # (-(ctx->stack_frame_size+16))]!
					|	mov x29, sp
				}
				|	sub sp, sp, #used_stack
			}
		}
	}

	/* On APPLE "unnamed" arguments always passed through stack */
	int last_named_input;

	if (!cc->sysv_varargs) {
		const ir_proto_t *proto = ir_call_proto(ctx, insn);
		last_named_input = (proto && (proto->flags & IR_VARARG_FUNC)) ? proto->params_count + 2 : insn->inputs_count;
	} else {
		last_named_input = insn->inputs_count;
	}

	if (copy_stack) {
		/* Copy struct arguments */
		for (j = 3; j <= n; j++) {
			arg = ir_insn_op(insn, j);
			src_reg = ir_get_alocated_reg(ctx, def, j);
			arg_insn = &ctx->ir_base[arg];
			type = arg_insn->type;

			if (arg_insn->op == IR_ARGVAL) {
				/* make a stack copy */
				void *addr = memcpy;
				int size = arg_insn->op2;
				int align = arg_insn->op3;

				copy_stack_offset += size;
				align = IR_MAX((int)sizeof(void*), align);
				copy_stack_offset = IR_ALIGNED_SIZE(copy_stack_offset, align);
				src_reg = ctx->regs[arg][1];

				|	add Rx(ir_call_conv_default.int_param_regs[0]), sp, #(used_stack - copy_stack_offset)
				if (src_reg != IR_REG_NONE) {
					if (IR_REG_SPILLED(src_reg)) {
						src_reg = IR_REG_NUM(src_reg);
						ir_emit_load(ctx, IR_ADDR, src_reg, arg_insn->op1);
					}
					|	mov Rx(ir_call_conv_default.int_param_regs[1]), Rx(src_reg)
				} else {
					ir_emit_load(ctx, IR_ADDR, ir_call_conv_default.int_param_regs[1], arg_insn->op1);
				}
				ir_emit_load_imm_int(ctx, IR_ADDR, ir_call_conv_default.int_param_regs[2], size);

				if (aarch64_may_use_b(ctx->code_buffer, addr)) {
					|	bl &addr
				} else {
					ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
					|	blr Rx(IR_REG_INT_TMP)
				}
			}
		}
		copy_stack_offset = 0;
	}

	/* 1. move all register arguments that should be passed through stack
	 *    and collect arguments that should be passed through registers */
	copies = ir_mem_malloc((n - 2) * sizeof(ir_copy));
	for (j = 3; j <= n; j++) {
		arg = ir_insn_op(insn, j);
		src_reg = ir_get_alocated_reg(ctx, def, j);
		arg_insn = &ctx->ir_base[arg];
		type = arg_insn->type;

		if (j > last_named_input) {
			if (arg_insn->op == IR_ARGVAL) {
				do_pass3 = 1;
				continue;
			}
			dst_reg = IR_REG_NONE; /* pass argument through stack */
		} else if (IR_IS_TYPE_INT(type)) {
			if (int_param < cc->int_param_regs_count) {
				dst_reg = cc->int_param_regs[int_param];
			} else {
				dst_reg = IR_REG_NONE; /* pass argument through stack */
			}
			int_param++;
			if (arg_insn->op == IR_ARGVAL) {
				do_pass3 = 1;
				continue;
			}
		} else {
			IR_ASSERT(IR_IS_TYPE_FP(type));
			if (fp_param < cc->fp_param_regs_count) {
				dst_reg = cc->fp_param_regs[fp_param];
			} else {
				dst_reg = IR_REG_NONE; /* pass argument through stack */
			}
			fp_param++;
		}
		if (dst_reg != IR_REG_NONE) {
			if (IR_IS_CONST_REF(arg) ||
			    src_reg == IR_REG_NONE ||
			    (IR_REG_SPILLED(src_reg) && !IR_REGSET_IN(cc->preserved_regs, IR_REG_NUM(src_reg)))) {
				/* delay CONST->REG and MEM->REG moves to third pass */
				do_pass3 = 1;
			} else {
				IR_ASSERT(src_reg != IR_REG_NONE);
				if (IR_REG_SPILLED(src_reg)) {
					src_reg = IR_REG_NUM(src_reg);
					ir_emit_load(ctx, type, src_reg, arg);
				}
				if (src_reg != dst_reg) {
					/* delay REG->REG moves to second pass */
					copies[count].type = type;
					copies[count].from = src_reg;
					copies[count].to = dst_reg;
					count++;
				}
			}
		} else {
			/* Pass register arguments to stack (REG->MEM moves) */
			if (!IR_IS_CONST_REF(arg) && src_reg != IR_REG_NONE && !IR_REG_SPILLED(src_reg)) {
				ir_emit_store_mem(ctx, type, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), src_reg);
			} else {
				do_pass3 = 1;
			}
			stack_offset += IR_MAX(sizeof(void*), ir_type_size[type]);
		}
	}

	/* 2. move all arguments that should be passed from one register to another (REG->REG movs) */
	if (count) {
		ir_parallel_copy(ctx, copies, count, tmp_reg, tmp_fp_reg);
	}
	ir_mem_free(copies);

	/* 3. move the remaining memory and immediate values */
	if (do_pass3) {
		stack_offset = 0;
		int_param = 0;
		fp_param = 0;
		for (j = 3; j <= n; j++) {
			arg = ir_insn_op(insn, j);
			src_reg = ir_get_alocated_reg(ctx, def, j);
			arg_insn = &ctx->ir_base[arg];
			type = arg_insn->type;
			if (arg_insn->op == IR_ARGVAL) {
				/* pass pointer to the copy on stack */
				int size = arg_insn->op2;
				int align = arg_insn->op3;

				copy_stack_offset += size;
				align = IR_MAX((int)sizeof(void*), align);
				copy_stack_offset = IR_ALIGNED_SIZE(copy_stack_offset, align);
				if (j > last_named_input) {
					|	add Rx(tmp_reg), sp, #(used_stack - copy_stack_offset)
					ir_emit_store_mem_int(ctx, IR_ADDR, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), tmp_reg);
				} else if (int_param < cc->int_param_regs_count) {
					dst_reg = cc->int_param_regs[int_param];
					|	add Rx(dst_reg), sp, #(used_stack - copy_stack_offset)
				} else {
					|	add Rx(tmp_reg), sp, #(used_stack - copy_stack_offset)
					ir_emit_store_mem_int(ctx, IR_ADDR, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), tmp_reg);
					stack_offset += sizeof(void*);
				}
				int_param++;
				continue;
			}
			if (j > last_named_input) {
				dst_reg = IR_REG_NONE; /* pass argument through stack */
			} else if (IR_IS_TYPE_INT(type)) {
				if (int_param < cc->int_param_regs_count) {
					dst_reg = cc->int_param_regs[int_param];
				} else {
					dst_reg = IR_REG_NONE; /* argument already passed through stack */
				}
				int_param++;
			} else {
				IR_ASSERT(IR_IS_TYPE_FP(type));
				if (fp_param < cc->fp_param_regs_count) {
					dst_reg = cc->fp_param_regs[fp_param];
				} else {
					dst_reg = IR_REG_NONE; /* argument already passed through stack */
				}
				fp_param++;
			}
			if (dst_reg != IR_REG_NONE) {
				if (IR_IS_CONST_REF(arg) ||
				    src_reg == IR_REG_NONE ||
				    (IR_REG_SPILLED(src_reg) && !IR_REGSET_IN(cc->preserved_regs, IR_REG_NUM(src_reg)))) {
					if (IR_IS_CONST_REF(arg) && IR_IS_TYPE_INT(type)) {
						if (ir_type_size[type] == 1) {
							type = IR_ADDR;
						}
					}
					ir_emit_load(ctx, type, dst_reg, arg);
				}
			} else {
				if (IR_IS_TYPE_INT(type)) {
					if (src_reg == IR_REG_NONE) {
						IR_ASSERT(tmp_reg != IR_REG_NONE);
						ir_emit_load(ctx, type, tmp_reg, arg);
						if (IR_IS_CONST_REF(arg)) {
							type = IR_ADDR; //TODO: ???
						}
						ir_emit_store_mem_int(ctx, type, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), tmp_reg);
					} else if (IR_REG_SPILLED(src_reg)) {
						src_reg = IR_REG_NUM(src_reg);
						ir_emit_load(ctx, type, src_reg, arg);
						ir_emit_store_mem_int(ctx, type, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), src_reg);
					}
				} else {
					if (src_reg == IR_REG_NONE) {
						IR_ASSERT(tmp_fp_reg != IR_REG_NONE);
						ir_emit_load(ctx, type, tmp_fp_reg, arg);
						ir_emit_store_mem_fp(ctx, IR_DOUBLE, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), tmp_fp_reg);
					} else if (IR_REG_SPILLED(src_reg)) {
						src_reg = IR_REG_NUM(src_reg);
						ir_emit_load(ctx, type, src_reg, arg);
						ir_emit_store_mem_fp(ctx, type, IR_MEM_BO(IR_REG_STACK_POINTER, stack_offset), src_reg);
					}
				}
				stack_offset += IR_MAX(sizeof(void*), ir_type_size[type]);
			}
		}
	}
	return used_stack;
}

static void ir_emit_call_ex(ir_ctx *ctx, ir_ref def, ir_insn *insn, const ir_call_conv_dsc *cc, int32_t used_stack)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg;

	if (IR_IS_CONST_REF(insn->op2)) {
		void *addr = ir_call_addr(ctx, insn, &ctx->ir_base[insn->op2]);

		if (aarch64_may_use_b(ctx->code_buffer, addr)) {
			|	bl &addr
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
			|	blr Rx(IR_REG_INT_TMP)
		}
    } else {
		ir_reg op2_reg = ctx->regs[def][2];

		IR_ASSERT(op2_reg != IR_REG_NONE);
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		}
		|	blr Rx(op2_reg)
    }

	if (used_stack) {
		|	add sp, sp, #used_stack
		ctx->call_stack_size -= used_stack;
	}

	if (insn->type != IR_VOID) {
		if (IR_IS_TYPE_INT(insn->type)) {
			def_reg = IR_REG_NUM(ctx->regs[def][0]);
			if (def_reg != IR_REG_NONE) {
				if (def_reg != cc->int_ret_reg) {
					ir_emit_mov(ctx, insn->type, def_reg, cc->int_ret_reg);
				}
				if (IR_REG_SPILLED(ctx->regs[def][0])) {
					ir_emit_store(ctx, insn->type, def, def_reg);
				}
			} else if (ctx->use_lists[def].count > 1) {
				ir_emit_store(ctx, insn->type, def, cc->int_ret_reg);
			}
		} else {
			IR_ASSERT(IR_IS_TYPE_FP(insn->type));
			def_reg = IR_REG_NUM(ctx->regs[def][0]);
			if (def_reg != IR_REG_NONE) {
				if (def_reg != cc->fp_ret_reg) {
					ir_emit_fp_mov(ctx, insn->type, def_reg, cc->fp_ret_reg);
				}
				if (IR_REG_SPILLED(ctx->regs[def][0])) {
					ir_emit_store(ctx, insn->type, def, def_reg);
				}
			} else if (ctx->use_lists[def].count > 1) {
				ir_emit_store(ctx, insn->type, def, cc->fp_ret_reg);
			}
		}
	}
}

static void ir_emit_call(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	const ir_proto_t *proto = ir_call_proto(ctx, insn);
	const ir_call_conv_dsc *cc = ir_get_call_conv_dsc(proto ? proto->flags : IR_CC_DEFAULT);
	int32_t used_stack = ir_emit_arguments(ctx, def, insn, cc, ctx->regs[def][1]);
	ir_emit_call_ex(ctx, def, insn, cc, used_stack);
}

static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	const ir_proto_t *proto = ir_call_proto(ctx, insn);
	const ir_call_conv_dsc *cc = ir_get_call_conv_dsc(proto ? proto->flags : IR_CC_DEFAULT);
	int32_t used_stack = ir_emit_arguments(ctx, def, insn, cc, ctx->regs[def][1]);

	if (used_stack != 0) {
		ir_emit_call_ex(ctx, def, insn, cc, used_stack);
		ir_emit_return_void(ctx);
		return;
	}

	/* Move op2 to a tmp register before epilogue if it's in
	 * used_preserved_regs, because it will be overridden. */

	ir_reg op2_reg = IR_REG_NONE;
	if (!IR_IS_CONST_REF(insn->op2)) {
		op2_reg = ctx->regs[def][2];
		IR_ASSERT(op2_reg != IR_REG_NONE);

		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_INT_TMP;
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		} else if (IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, IR_REG_NUM(op2_reg))) {
			ir_reg orig_op2_reg = op2_reg;
			op2_reg = IR_REG_INT_TMP;

			ir_type type = ctx->ir_base[insn->op2].type;
			| ASM_REG_REG_OP mov, type, op2_reg, IR_REG_NUM(orig_op2_reg)
		} else {
			op2_reg = IR_REG_NUM(op2_reg);
		}
	}

	ir_emit_epilogue(ctx);

	if (IR_IS_CONST_REF(insn->op2)) {
		void *addr = ir_call_addr(ctx, insn, &ctx->ir_base[insn->op2]);

		if (aarch64_may_use_b(ctx->code_buffer, addr)) {
			|	b &addr
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
			|	br Rx(IR_REG_INT_TMP)
		}
    } else {
		IR_ASSERT(op2_reg != IR_REG_NONE);
		IR_ASSERT(!IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, op2_reg));
		|	br Rx(op2_reg)
    }
}

static void ir_emit_ijmp(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg op2_reg = ctx->regs[def][2];

	if (op2_reg != IR_REG_NONE) {
		if (IR_REG_SPILLED(op2_reg)) {
			op2_reg = IR_REG_NUM(op2_reg);
			ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2);
		}
		|	br Rx(op2_reg)
	} else if (IR_IS_CONST_REF(insn->op2)) {
		if (ctx->ir_base[insn->op2].op == IR_LABEL) {
			if (!data->resolved_label_syms) {
				data->resolved_label_syms = 1;
				ir_resolve_label_syms(ctx);
			}

			uint32_t target = ctx->ir_base[insn->op2].val.u32_hi;
			target = ir_skip_empty_target_blocks(ctx, target);

			|	b =>target
			return;
		}

		void *addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op2]);

		if (aarch64_may_use_b(ctx->code_buffer, addr)) {
			|	b &addr
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
			|	br Rx(IR_REG_INT_TMP)
		}
	} else {
		IR_ASSERT(0);
	}
}

static void ir_emit_guard(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_reg op2_reg = ctx->regs[def][2];
	ir_type type = ctx->ir_base[insn->op2].type;

	IR_ASSERT(IR_IS_TYPE_INT(type));
	if (IR_IS_CONST_REF(insn->op2)) {
		bool is_true = ir_ref_is_true(ctx, insn->op2);

		if ((insn->op == IR_GUARD && !is_true) || (insn->op == IR_GUARD_NOT && is_true)) {
			if (IR_IS_CONST_REF(insn->op3)) {
				void *addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op3]);

				if (aarch64_may_use_b(ctx->code_buffer, addr)) {
					|	b &addr
				} else {
					ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
					|	br Rx(IR_REG_INT_TMP)
				}
			} else {
				IR_ASSERT(0);
			}
		}
		return;
	}

	IR_ASSERT(op2_reg != IR_REG_NONE);
	if (IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		ir_emit_load(ctx, type, op2_reg, insn->op2);
	}

	if (IR_IS_CONST_REF(insn->op3)) {
		void *addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op3]);

		if (insn->op == IR_GUARD) {
			if (ir_type_size[type] == 8) {
				|	cbz Rx(op2_reg), &addr
			} else {
				|	cbz Rw(op2_reg), &addr
			}
		} else {
			if (ir_type_size[type] == 8) {
				|	cbnz Rx(op2_reg), &addr
			} else {
				|	cbnz Rw(op2_reg), &addr
			}
		}
	} else {
		IR_ASSERT(0);
	}
}

static void ir_emit_guard_jz(ir_ctx *ctx, uint8_t op, void *addr, ir_type type, ir_reg reg)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (op == IR_EQ) {
		if (ir_type_size[type] == 8) {
			|	cbnz Rx(reg), &addr
		} else {
			|	cbnz Rw(reg), &addr
		}
	} else {
		IR_ASSERT(op == IR_NE);
		if (ir_type_size[type] == 8) {
			|	cbz Rx(reg), &addr
		} else {
			|	cbz Rw(reg), &addr
		}
	}
}

static void ir_emit_guard_jcc(ir_ctx *ctx, uint8_t op, void *addr, bool int_cmp)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;

	if (int_cmp) {
		switch (op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_EQ:
				|	beq &addr
				break;
			case IR_NE:
				|	bne &addr
				break;
			case IR_LT:
				|	blt &addr
				break;
			case IR_GE:
				|	bge &addr
				break;
			case IR_LE:
				|	ble &addr
				break;
			case IR_GT:
				|	bgt &addr
				break;
			case IR_ULT:
				|	blo &addr
				break;
			case IR_UGE:
				|	bhs &addr
				break;
			case IR_ULE:
				|	bls &addr
				break;
			case IR_UGT:
				|	bhi &addr
				break;
		}
	} else {
		switch (op) {
			default:
				IR_ASSERT(0 && "NIY binary op");
			case IR_EQ:
				|	beq &addr
				break;
			case IR_NE:
				|	bne &addr
				break;
			case IR_LT:
				|	bmi &addr
				break;
			case IR_GE:
				|	bge &addr
				break;
			case IR_LE:
				|	bls &addr
				break;
			case IR_GT:
				|	bgt &addr
				break;
			case IR_ULT:
				|	blt &addr
				break;
			case IR_UGE:
				|	bhs &addr
				break;
			case IR_ULE:
				|	ble &addr
				break;
			case IR_UGT:
				|	bhi &addr
				break;
			case IR_ORDERED:
				|	bvc &addr
				break;
			case IR_UNORDERED:
				|	bvs &addr
		}
	}
}

static void ir_emit_guard_cmp_int(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_insn *cmp_insn = &ctx->ir_base[insn->op2];
	ir_op op = cmp_insn->op;
	ir_type type = ctx->ir_base[cmp_insn->op1].type;
	ir_ref op1 = cmp_insn->op1;
	ir_ref op2 = cmp_insn->op2;
	void *addr;
	ir_reg op1_reg, op2_reg;

	if (UNEXPECTED(ctx->rules[insn->op2] & IR_FUSED_REG)) {
		op1_reg = ir_get_fused_reg(ctx, def, insn->op2 * sizeof(ir_ref) + 1);
		op2_reg = ir_get_fused_reg(ctx, def, insn->op2 * sizeof(ir_ref) + 2);
	} else {
		op1_reg = ctx->regs[insn->op2][1];
		op2_reg = ctx->regs[insn->op2][2];
	}

	if (op1_reg != IR_REG_NONE && IR_REG_SPILLED(op1_reg)) {
		op1_reg = IR_REG_NUM(op1_reg);
		ir_emit_load(ctx, type, op1_reg, op1);
	}
	if (op2_reg != IR_REG_NONE && IR_REG_SPILLED(op2_reg)) {
		op2_reg = IR_REG_NUM(op2_reg);
		if (op1 != op2) {
			ir_emit_load(ctx, type, op2_reg, op2);
		}
	}

	addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op3]);

	if (IR_IS_CONST_REF(op2)
	 && !IR_IS_SYM_CONST(ctx->ir_base[op2].op)
	 && ctx->ir_base[op2].val.u64 == 0) {
		if (op == IR_ULT) {
			/* always false */
			if (aarch64_may_use_b(ctx->code_buffer, addr)) {
				|	b &addr
			} else {
				ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
				|	br Rx(IR_REG_INT_TMP)
			}
			return;
		} else if (op == IR_UGE) {
			/* always true */
			return;
		} else if (op == IR_ULE) {
			op = IR_EQ;
		} else if (op == IR_UGT) {
			op = IR_NE;
		}
		if (op1_reg != IR_REG_NONE && (op == IR_EQ || op == IR_NE)) {
			if (insn->op == IR_GUARD_NOT) {
				op ^= 1; // reverse
			}
			ir_emit_guard_jz(ctx, op, addr, type, op1_reg);
			return;
		}
	}
	ir_emit_cmp_int_common(ctx, type, op1_reg, op1, op2_reg, op2);

	if (insn->op == IR_GUARD) {
		op ^= 1; // reverse
	}

	ir_emit_guard_jcc(ctx, op, addr, 1);
}

static void ir_emit_guard_cmp_fp(ir_ctx *ctx, uint32_t b, ir_ref def, ir_insn *insn)
{
	ir_op op = ir_emit_cmp_fp_common(ctx, def, insn->op2, &ctx->ir_base[insn->op2]);
	void *addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op3]);

	if (insn->op == IR_GUARD) {
		if (op == IR_EQ || op == IR_NE || op == IR_ORDERED || op == IR_UNORDERED) {
			op ^= 1; // reverse
		} else {
			op ^= 5; // reverse
		}
	}
	ir_emit_guard_jcc(ctx, op, addr, 0);
}

static void ir_emit_guard_overflow(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	ir_insn *overflow_insn = &ctx->ir_base[insn->op2];
	ir_insn *math_insn = &ctx->ir_base[overflow_insn->op1];
	ir_type type = math_insn->type;
	void *addr = ir_jmp_addr(ctx, insn, &ctx->ir_base[insn->op3]);

	IR_ASSERT(IR_IS_TYPE_INT(type));
	if (math_insn->op == IR_MUL_OV) {
		if (insn->op == IR_GUARD) {
			|	beq &addr
		} else {
			|	bne &addr
		}
	} else if (IR_IS_TYPE_SIGNED(type)) {
		if (insn->op == IR_GUARD) {
			|	bvc &addr
		} else {
			|	bvs &addr
		}
	} else {
		if (insn->op == IR_GUARD) {
			|	bcc &addr
		} else {
			|	bcs &addr
		}
	}
}

static void ir_emit_tls(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	dasm_State **Dst = &data->dasm_state;
	uint32_t code;
	ir_reg reg = IR_REG_NUM(ctx->regs[def][0]);

	if (ctx->use_lists[def].count == 1) {
		/* dead load */
		return;
	}

||#ifdef __APPLE__
||	code = 0xd53bd060 | reg; // TODO: hard-coded: mrs reg, tpidrro_el0
|	.long code
|	and Rx(reg), Rx(reg), #0xfffffffffffffff8
|//???	MEM_ACCESS_64_WITH_UOFFSET_64 ldr, Rx(reg), Rx(reg), #insn->op2, TMP1
|//???	MEM_ACCESS_64_WITH_UOFFSET_64 ldr, Rx(reg), Rx(reg), #insn->op3, TMP1
||#else
||	code = 0xd53bd040 | reg; // TODO: hard-coded: mrs reg, tpidr_el0
|	.long code
||# ifdef __FreeBSD__
||	if (insn->op3 == IR_NULL) {
|		ldr Rx(reg), [Rx(reg), #insn->op2]
||	} else {
|		ldr Rx(reg), [Rx(reg), #0]
|		ldr Rx(reg), [Rx(reg), #insn->op2]
|		ldr Rx(reg), [Rx(reg), #insn->op3]
||	}
||# elif defined(__MUSL__)
||	if (insn->op3 == IR_NULL) {
|		ldr Rx(reg), [Rx(reg), #insn->op2]
||	} else {
|		ldr Rx(reg), [Rx(reg), #-8]
|		ldr Rx(reg), [Rx(reg), #insn->op2]
|		ldr Rx(reg), [Rx(reg), #insn->op3]
||	}
||# else
||//???	IR_ASSERT(insn->op2 <= LDR_STR_PIMM64);
|	ldr Rx(reg), [Rx(reg), #insn->op2]
||# endif
||#endif
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, IR_ADDR, def, reg);
	}
}

static void ir_emit_exitcall(ir_ctx *ctx, ir_ref def, ir_insn *insn)
{
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = &ir_call_conv_default;
	dasm_State **Dst = &data->dasm_state;
	ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]);

	IR_ASSERT(def_reg != IR_REG_NONE);

	|	stp d30, d31, [sp, #-16]!
	|	stp d28, d29, [sp, #-16]!
	|	stp d26, d27, [sp, #-16]!
	|	stp d24, d25, [sp, #-16]!
	|	stp d22, d23, [sp, #-16]!
	|	stp d20, d21, [sp, #-16]!
	|	stp d18, d19, [sp, #-16]!
	|	stp d16, d17, [sp, #-16]!
	|	stp d14, d15, [sp, #-16]!
	|	stp d12, d13, [sp, #-16]!
	|	stp d10, d11, [sp, #-16]!
	|	stp d8, d9, [sp, #-16]!
	|	stp d6, d7, [sp, #-16]!
	|	stp d4, d5, [sp, #-16]!
	|	stp d2, d3, [sp, #-16]!
	|	stp d0, d1, [sp, #-16]!

	|	str x30, [sp, #-16]!
	|	stp x28, x29, [sp, #-16]!
	|	stp x26, x27, [sp, #-16]!
	|	stp x24, x25, [sp, #-16]!
	|	stp x22, x23, [sp, #-16]!
	|	stp x20, x21, [sp, #-16]!
	|	stp x18, x19, [sp, #-16]!
	|	stp x16, x17, [sp, #-16]!
	|	stp x14, x15, [sp, #-16]!
	|	stp x12, x13, [sp, #-16]!
	|	stp x10, x11, [sp, #-16]!
	|	stp x8, x9, [sp, #-16]!
	|	stp x6, x7, [sp, #-16]!
	|	stp x4, x5, [sp, #-16]!
	|	stp x2, x3, [sp, #-16]!
	|	stp x0, x1, [sp, #-16]!

	|	mov Rx(cc->int_param_regs[1]), sp
	|	add Rx(cc->int_param_regs[0]), Rx(cc->int_param_regs[1]), #(32*8+32*8)
	|	str Rx(cc->int_param_regs[0]), [sp, #(31*8)]
	|	mov Rx(cc->int_param_regs[0]), Rx(IR_REG_INT_TMP)

	if (IR_IS_CONST_REF(insn->op2)) {
		void *addr = ir_call_addr(ctx, insn, &ctx->ir_base[insn->op2]);

		if (aarch64_may_use_b(ctx->code_buffer, addr)) {
			|	bl &addr
		} else {
			ir_emit_load_imm_int(ctx, IR_ADDR, IR_REG_INT_TMP, (intptr_t)addr);
			|	blr Rx(IR_REG_INT_TMP)
		}
	} else {
		IR_ASSERT(0);
	}

	|	add sp, sp, #(32*8+32*8)

	if (def_reg != cc->int_ret_reg) {
		ir_emit_mov(ctx, insn->type, def_reg, cc->int_ret_reg);
	}
	if (IR_REG_SPILLED(ctx->regs[def][0])) {
		ir_emit_store(ctx, insn->type, def, def_reg);
	}
}

static void ir_emit_param_move(ir_ctx *ctx, uint8_t type, ir_reg from_reg, ir_reg to_reg, ir_ref to, int32_t offset)
{
	ir_reg fp = (ctx->flags & IR_USE_FRAME_POINTER) ? IR_REG_FRAME_POINTER : IR_REG_STACK_POINTER;

	offset = IR_SPILL_POS_TO_OFFSET(offset);
	IR_ASSERT(from_reg != IR_REG_NONE || to_reg != IR_REG_NONE);

	if (IR_IS_TYPE_INT(type)) {
		if (from_reg != IR_REG_NONE) {
			if (to_reg != IR_REG_NONE) {
				ir_emit_mov(ctx, type, to_reg, from_reg);
			} else {
				ir_emit_store(ctx, type, to, from_reg);
			}
		} else {
			ir_emit_load_mem_int(ctx, type, to_reg, IR_MEM_BO(fp, offset));
		}
	} else {
		if (from_reg != IR_REG_NONE) {
			if (to_reg != IR_REG_NONE) {
				ir_emit_fp_mov(ctx, type, to_reg, from_reg);
			} else {
				ir_emit_store(ctx, type, to, from_reg);
			}
		} else {
			ir_emit_load_mem_fp(ctx, type, to_reg, IR_MEM_BO(fp, offset));
		}
	}
}

static void ir_emit_load_params(ir_ctx *ctx)
{
	ir_use_list *use_list = &ctx->use_lists[1];
	ir_insn *insn;
	ir_ref i, n, *p, use;
	int int_param_num = 0;
	int fp_param_num = 0;
	ir_reg src_reg;
	ir_reg dst_reg;
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = data->ra_data.cc;
	int32_t stack_offset = 0;
	int32_t stack_start = ctx->stack_frame_size;

	n = use_list->count;
	for (i = 0, p = &ctx->use_edges[use_list->refs]; i < n; i++, p++) {
		use = *p;
		insn = &ctx->ir_base[use];
		if (insn->op == IR_PARAM) {
			if (IR_IS_TYPE_INT(insn->type)) {
				if (int_param_num < cc->int_param_regs_count) {
					src_reg = cc->int_param_regs[int_param_num];
				} else {
					src_reg = IR_REG_NONE;
				}
				int_param_num++;
			} else {
				if (fp_param_num < cc->fp_param_regs_count) {
					src_reg = cc->fp_param_regs[fp_param_num];
				} else {
					src_reg = IR_REG_NONE;
				}
				fp_param_num++;
			}
			if (ctx->vregs[use]) {
				dst_reg = IR_REG_NUM(ctx->regs[use][0]);
				IR_ASSERT(src_reg != IR_REG_NONE || dst_reg != IR_REG_NONE ||
					stack_start + stack_offset == ctx->live_intervals[ctx->vregs[use]]->stack_spill_pos);
				if (src_reg != dst_reg) {
					ir_emit_param_move(ctx, insn->type, src_reg, dst_reg, use, stack_start + stack_offset);
				}
				if (dst_reg != IR_REG_NONE && IR_REG_SPILLED(ctx->regs[use][0])) {
					ir_emit_store(ctx, insn->type, use, dst_reg);
				}
			}
			if (src_reg == IR_REG_NONE) {
				if (sizeof(void*) == 8) {
					stack_offset += sizeof(void*);
				} else {
					stack_offset += IR_MAX(sizeof(void*), ir_type_size[insn->type]);
				}
			}
		}
	}
}

static ir_reg ir_get_free_reg(ir_type type, ir_regset available)
{
	if (IR_IS_TYPE_INT(type)) {
		available = IR_REGSET_INTERSECTION(available, IR_REGSET_GP);
	} else {
		IR_ASSERT(IR_IS_TYPE_FP(type));
		available = IR_REGSET_INTERSECTION(available, IR_REGSET_FP);
	}
	IR_ASSERT(!IR_REGSET_IS_EMPTY(available));
	return IR_REGSET_FIRST(available);
}

static int ir_fix_dessa_tmps(ir_ctx *ctx, uint8_t type, ir_ref from, ir_ref to, void *dessa_from_block)
{
	ir_ref ref = ctx->cfg_blocks[(intptr_t)dessa_from_block].end;

	if (to == 0) {
		if (IR_IS_TYPE_INT(type)) {
			if (ctx->regs[ref][0] == IR_REG_NONE) {
				ctx->regs[ref][0] = IR_REG_X0;
			}
		} else {
			IR_ASSERT(IR_IS_TYPE_FP(type));
			if (ctx->regs[ref][1] == IR_REG_NONE) {
				ctx->regs[ref][1] = IR_REG_V0;
			}
		}
	} else if (from != 0) {
		if (IR_IS_TYPE_INT(type)) {
			if (ctx->regs[ref][0] == IR_REG_NONE) {
				ctx->regs[ref][0] = IR_REG_X0;
			}
		} else {
			IR_ASSERT(IR_IS_TYPE_FP(type));
			if (ctx->regs[ref][1] == IR_REG_NONE) {
				ctx->regs[ref][1] = IR_REG_V0;
			}
		}
	}
	return 1;
}

static void ir_fix_param_spills(ir_ctx *ctx)
{
	ir_use_list *use_list = &ctx->use_lists[1];
	ir_insn *insn;
	ir_ref i, n, *p, use;
	int int_param_num = 0;
	int fp_param_num = 0;
	ir_reg src_reg;
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = data->ra_data.cc;
	int32_t stack_offset = 0;
	int32_t stack_start = ctx->stack_frame_size;

	n = use_list->count;
	for (i = 0, p = &ctx->use_edges[use_list->refs]; i < n; i++, p++) {
		use = *p;
		insn = &ctx->ir_base[use];
		if (insn->op == IR_PARAM) {
			if (IR_IS_TYPE_INT(insn->type)) {
				if (int_param_num < cc->int_param_regs_count) {
					src_reg = cc->int_param_regs[int_param_num];
				} else {
					src_reg = IR_REG_NONE;
				}
				int_param_num++;
			} else {
				if (fp_param_num < cc->fp_param_regs_count) {
					src_reg = cc->fp_param_regs[fp_param_num];
				} else {
					src_reg = IR_REG_NONE;
				}
				fp_param_num++;
			}
			if (src_reg == IR_REG_NONE) {
				if (ctx->vregs[use]) {
					ir_live_interval *ival = ctx->live_intervals[ctx->vregs[use]];
					if ((ival->flags & IR_LIVE_INTERVAL_MEM_PARAM)
					 && ival->stack_spill_pos == -1
					 && (ival->next || ival->reg == IR_REG_NONE)) {
						ival->stack_spill_pos = stack_start + stack_offset;
					}
				}
				if (sizeof(void*) == 8) {
					stack_offset += sizeof(void*);
				} else {
					stack_offset += IR_MAX(sizeof(void*), ir_type_size[insn->type]);
				}
			}
		}
	}

	ctx->gp_reg_params = IR_MIN(int_param_num, cc->int_param_regs_count);
	ctx->fp_reg_params = IR_MIN(fp_param_num, cc->fp_param_regs_count);
	ctx->param_stack_size = stack_offset;
}

static void ir_allocate_unique_spill_slots(ir_ctx *ctx)
{
	uint32_t b;
	ir_block *bb;
	ir_insn *insn;
	ir_ref i, n, j, *p;
	uint32_t *rule, insn_flags;
	ir_regset available = 0;
	ir_target_constraints constraints;
	uint32_t def_flags;
	ir_reg reg;
	ir_backend_data *data = ctx->data;
	const ir_call_conv_dsc *cc = data->ra_data.cc;
	ir_regset scratch = ir_scratch_regset[cc->scratch_reg - IR_REG_NUM];

	ctx->regs = ir_mem_malloc(sizeof(ir_regs) * ctx->insns_count);
	memset(ctx->regs, IR_REG_NONE, sizeof(ir_regs) * ctx->insns_count);

	/* vregs + tmp + fixed + SRATCH + ALL */
	ctx->live_intervals = ir_mem_calloc(ctx->vregs_count + 1 + IR_REG_NUM + 2, sizeof(ir_live_interval*));

    if (!ctx->arena) {
		ctx->arena = ir_arena_create(16 * 1024);
	}

	for (b = 1, bb = ctx->cfg_blocks + b; b <= ctx->cfg_blocks_count; b++, bb++) {
		IR_ASSERT(!(bb->flags & IR_BB_UNREACHABLE));
		for (i = bb->start, insn = ctx->ir_base + i, rule = ctx->rules + i; i <= bb->end;) {
			switch (ctx->rules ? *rule : insn->op) {
				case IR_START:
				case IR_BEGIN:
				case IR_END:
				case IR_IF_TRUE:
				case IR_IF_FALSE:
				case IR_CASE_VAL:
				case IR_CASE_RANGE:
				case IR_CASE_DEFAULT:
				case IR_MERGE:
				case IR_LOOP_BEGIN:
				case IR_LOOP_END:
				case IR_IGOTO_DUP:
					break;
				default:
					def_flags = ir_get_target_constraints(ctx, i, &constraints);
					if (ctx->rules
					 && *rule != IR_CMP_AND_BRANCH_INT
					 && *rule != IR_CMP_AND_BRANCH_FP
					 && *rule != IR_GUARD_CMP_INT
					 && *rule != IR_GUARD_CMP_FP) {
						available = scratch;
					}
					if (ctx->vregs[i]) {
						reg = constraints.def_reg;
						if (reg != IR_REG_NONE && IR_REGSET_IN(available, reg)) {
							IR_REGSET_EXCL(available, reg);
							ctx->regs[i][0] = reg | IR_REG_SPILL_STORE;
						} else if (def_flags & IR_USE_MUST_BE_IN_REG) {
							if ((insn->op == IR_VLOAD || insn->op == IR_VLOAD_v)
							 && ctx->live_intervals[ctx->vregs[i]]
							 && ctx->live_intervals[ctx->vregs[i]]->stack_spill_pos != -1
							 && ir_is_same_mem_var(ctx, i, ctx->ir_base[insn->op2].op3)) {
								/* pass */
							} else if (insn->op != IR_PARAM) {
								reg = ir_get_free_reg(insn->type, available);
								IR_REGSET_EXCL(available, reg);
								ctx->regs[i][0] = reg | IR_REG_SPILL_STORE;
							}
						}
						if (!ctx->live_intervals[ctx->vregs[i]]) {
							ir_live_interval *ival = ir_arena_alloc(&ctx->arena, sizeof(ir_live_interval));
							memset(ival, 0, sizeof(ir_live_interval));
							ctx->live_intervals[ctx->vregs[i]] = ival;
							ival->type = insn->type;
							ival->reg = IR_REG_NONE;
							ival->vreg = ctx->vregs[i];
							ival->stack_spill_pos = -1;
							if (insn->op == IR_PARAM && reg == IR_REG_NONE) {
								ival->flags |= IR_LIVE_INTERVAL_MEM_PARAM;
							} else {
								ival->stack_spill_pos = ir_allocate_spill_slot(ctx, ival->type);
							}
						} else if (insn->op == IR_PARAM) {
							IR_ASSERT(0 && "unexpected PARAM");
							return;
						}
					} else if (insn->op == IR_VAR) {
						ir_use_list *use_list = &ctx->use_lists[i];
						ir_ref n = use_list->count;

						if (n > 0) {
							int32_t stack_spill_pos = insn->op3 = ir_allocate_spill_slot(ctx, insn->type);
							ir_ref i, *p, use;
							ir_insn *use_insn;

							for (i = 0, p = &ctx->use_edges[use_list->refs]; i < n; i++, p++) {
								use = *p;
								use_insn = &ctx->ir_base[use];
								if (use_insn->op == IR_VLOAD || use_insn->op == IR_VLOAD_v) {
									if (ctx->vregs[use]
									 && !ctx->live_intervals[ctx->vregs[use]]) {
										ir_live_interval *ival = ir_arena_alloc(&ctx->arena, sizeof(ir_live_interval));
										memset(ival, 0, sizeof(ir_live_interval));
										ctx->live_intervals[ctx->vregs[use]] = ival;
										ival->type = insn->type;
										ival->reg = IR_REG_NONE;
										ival->vreg = ctx->vregs[use];
										ival->stack_spill_pos = stack_spill_pos;
									}
								} else if (use_insn->op == IR_VSTORE || use_insn->op == IR_STORE_v) {
									if (!IR_IS_CONST_REF(use_insn->op3)
									 && ctx->vregs[use_insn->op3]
									 && !ctx->live_intervals[ctx->vregs[use_insn->op3]]) {
										ir_live_interval *ival = ir_arena_alloc(&ctx->arena, sizeof(ir_live_interval));
										memset(ival, 0, sizeof(ir_live_interval));
										ctx->live_intervals[ctx->vregs[use_insn->op3]] = ival;
										ival->type = insn->type;
										ival->reg = IR_REG_NONE;
										ival->vreg = ctx->vregs[use_insn->op3];
										ival->stack_spill_pos = stack_spill_pos;
									}
								}
							}
						}
					}

					insn_flags = ir_op_flags[insn->op];
					n = constraints.tmps_count;
					if (n) {
						do {
							n--;
							if (constraints.tmp_regs[n].type) {
								ir_reg reg = ir_get_free_reg(constraints.tmp_regs[n].type, available);
								ir_ref *ops = insn->ops;
								IR_REGSET_EXCL(available, reg);
								if (constraints.tmp_regs[n].num > 0) {
									if (IR_IS_CONST_REF(ops[constraints.tmp_regs[n].num])) {
										/* rematerialization */
										reg |= IR_REG_SPILL_LOAD;
									} else if (ctx->ir_base[ops[constraints.tmp_regs[n].num]].op == IR_ALLOCA ||
											ctx->ir_base[ops[constraints.tmp_regs[n].num]].op == IR_VADDR) {
										/* local address rematerialization */
										reg |= IR_REG_SPILL_LOAD;
									}
								}
								ctx->regs[i][constraints.tmp_regs[n].num] = reg;
							} else {
								reg = constraints.tmp_regs[n].reg;
								if (reg >= IR_REG_NUM) {
									available = IR_REGSET_DIFFERENCE(available, ir_scratch_regset[reg - IR_REG_NUM]);
								} else {
									IR_REGSET_EXCL(available, reg);
								}
							}
						} while (n);
					}
					n = insn->inputs_count;
					for (j = 1, p = insn->ops + 1; j <= n; j++, p++) {
						ir_ref input = *p;
						if (IR_OPND_KIND(insn_flags, j) == IR_OPND_DATA && input > 0 && ctx->vregs[input]) {
							if ((def_flags & IR_DEF_REUSES_OP1_REG) && j == 1) {
								ir_reg reg = IR_REG_NUM(ctx->regs[i][0]);
								ctx->regs[i][1] = reg | IR_REG_SPILL_LOAD;
							} else {
								uint8_t use_flags = IR_USE_FLAGS(def_flags, j);
								ir_reg reg = (j < constraints.hints_count) ? constraints.hints[j] : IR_REG_NONE;

								if (reg != IR_REG_NONE && IR_REGSET_IN(available, reg)) {
									IR_REGSET_EXCL(available, reg);
									ctx->regs[i][j] = reg | IR_REG_SPILL_LOAD;
								} else if (IR_IS_FOLDABLE_OP(insn->op) && j > 1 && input == insn->op1 && ctx->regs[i][1] != IR_REG_NONE) {
									ctx->regs[i][j] = ctx->regs[i][1];
								} else if (use_flags & IR_USE_MUST_BE_IN_REG) {
									reg = ir_get_free_reg(ctx->ir_base[input].type, available);
									IR_REGSET_EXCL(available, reg);
									ctx->regs[i][j] = reg | IR_REG_SPILL_LOAD;
								}
							}
						}
					}
					break;
			}
			n = ir_insn_len(insn);
			i += n;
			insn += n;
			rule += n;
		}
		if (bb->flags & IR_BB_DESSA_MOVES) {
			ir_gen_dessa_moves(ctx, b, ir_fix_dessa_tmps, (void*)(intptr_t)b);
		}
	}

	ctx->used_preserved_regs = ctx->fixed_save_regset;
	ctx->flags |= IR_NO_STACK_COMBINE;
	ir_fix_stack_frame(ctx);
}

static void ir_preallocate_call_stack(ir_ctx *ctx)
{
	int call_stack_size, peak_call_stack_size = 0;
	ir_ref i, n;
	ir_insn *insn;

	for (i = 1, insn = ctx->ir_base + 1; i < ctx->insns_count;) {
		if (insn->op == IR_CALL) {
			const ir_proto_t *proto = ir_call_proto(ctx, insn);
			const ir_call_conv_dsc *cc = ir_get_call_conv_dsc(proto ? proto->flags : IR_CC_DEFAULT);
			int32_t copy_stack;

			call_stack_size = ir_call_used_stack(ctx, insn, cc, &copy_stack);
			if (call_stack_size > peak_call_stack_size) {
				peak_call_stack_size = call_stack_size;
			}
		}
		n = ir_insn_len(insn);
		i += n;
		insn += n;
	}
	if (peak_call_stack_size) {
		ctx->call_stack_size = peak_call_stack_size;
		ctx->flags |= IR_PREALLOCATED_STACK;
	}
}

void ir_fix_stack_frame(ir_ctx *ctx)
{
	uint32_t additional_size = 0;

	ctx->locals_area_size = ctx->stack_frame_size;

	if (ctx->used_preserved_regs) {
		ir_regset used_preserved_regs = (ir_regset)ctx->used_preserved_regs;
		ir_reg reg;
		(void) reg;

		IR_REGSET_FOREACH(used_preserved_regs, reg) {
			additional_size += sizeof(void*);
		} IR_REGSET_FOREACH_END();
	}

	if ((ctx->flags & IR_VARARG_FUNC) && (ctx->flags2 & IR_HAS_VA_START)) {
		ir_backend_data *data = ctx->data;
		const ir_call_conv_dsc *cc = data->ra_data.cc;

		if ((ctx->flags2 & (IR_HAS_VA_ARG_GP|IR_HAS_VA_COPY)) && ctx->gp_reg_params < cc->int_param_regs_count) {
			additional_size += sizeof(void*) * cc->int_param_regs_count;
		}
		if ((ctx->flags2 & (IR_HAS_VA_ARG_FP|IR_HAS_VA_COPY)) && ctx->fp_reg_params < cc->fp_param_regs_count) {
			additional_size += 16 * cc->int_param_regs_count;
		}
	}

	ctx->stack_frame_size = IR_ALIGNED_SIZE(ctx->stack_frame_size, sizeof(void*));
	ctx->stack_frame_size += additional_size;
	ctx->call_stack_size = 0;

	if (!(ctx->flags & IR_FUNCTION)) {
		while (IR_ALIGNED_SIZE(ctx->stack_frame_size, 16) != ctx->stack_frame_size) {
			ctx->stack_frame_size += sizeof(void*);
		}
	} else {
		/* Stack must be 16 byte aligned */
		if (!(ctx->flags & IR_FUNCTION)) {
			while (IR_ALIGNED_SIZE(ctx->stack_frame_size, 16) != ctx->stack_frame_size) {
				ctx->stack_frame_size += sizeof(void*);
			}
		} else if (ctx->flags & IR_USE_FRAME_POINTER) {
			while (IR_ALIGNED_SIZE(ctx->stack_frame_size + sizeof(void*) * 2, 16) != ctx->stack_frame_size + sizeof(void*) * 2) {
				ctx->stack_frame_size += sizeof(void*);
			}
		} else {
			if (!(ctx->flags & IR_NO_STACK_COMBINE)) {
				ir_preallocate_call_stack(ctx);
			}
			while (IR_ALIGNED_SIZE(ctx->stack_frame_size + ctx->call_stack_size, 16) !=
					ctx->stack_frame_size + ctx->call_stack_size) {
				ctx->stack_frame_size += sizeof(void*);
			}
		}
	}

	ir_fix_param_spills(ctx);
}

static void* dasm_labels[ir_lb_MAX];

/* Veneers support (TODO: avid global variable usage) */
static ir_ctx *ir_current_ctx;

static uint32_t _ir_next_block(ir_ctx *ctx, uint32_t _b)
{
	uint32_t b = ctx->cfg_schedule[++_b];

	/* Check for empty ENTRY block */
	while (b && ((ctx->cfg_blocks[b].flags & (IR_BB_START|IR_BB_EMPTY)) == IR_BB_EMPTY)) {
		b = ctx->cfg_schedule[++_b];
	}
	return b;
}

void *ir_emit_code(ir_ctx *ctx, size_t *size_ptr)
{
	uint32_t _b, b, n, target;
	ir_block *bb;
	ir_ref i;
	ir_insn *insn;
	uint32_t *rule;
	ir_backend_data data;
	dasm_State **Dst;
	int ret;
	void *entry;
	size_t size;
	ir_ref igoto_dup_ref = IR_UNUSED;
	uint32_t igoto_dup_block = 0;

	data.ra_data.cc = ir_get_call_conv_dsc(ctx->flags);
	data.ra_data.unused_slot_4 = 0;
	data.ra_data.unused_slot_2 = 0;
	data.ra_data.unused_slot_1 = 0;
	data.ra_data.handled = NULL;
	data.rodata_label = 0;
	data.jmp_table_label = 0;
	data.resolved_label_syms = 0;
	ctx->data = &data;

	if (!ctx->live_intervals) {
		ctx->stack_frame_size = 0;
		ctx->call_stack_size = 0;
		ctx->used_preserved_regs = 0;
		ir_allocate_unique_spill_slots(ctx);
	}

	if (ctx->fixed_stack_frame_size != -1) {
		if (ctx->fixed_stack_red_zone) {
			IR_ASSERT(ctx->fixed_stack_red_zone == ctx->fixed_stack_frame_size + ctx->fixed_call_stack_size);
		}
		if (ctx->stack_frame_size > ctx->fixed_stack_frame_size) {
			// TODO: report error to caller
#ifdef IR_DEBUG_MESSAGES
			fprintf(stderr, "IR Compilation Aborted: ctx->stack_frame_size > ctx->fixed_stack_frame_size at %s:%d\n",
				__FILE__, __LINE__);
#endif
			ctx->data = NULL;
			ctx->status = IR_ERROR_FIXED_STACK_FRAME_OVERFLOW;
			return NULL;
		}
		ctx->stack_frame_size = ctx->fixed_stack_frame_size;
		ctx->call_stack_size = ctx->fixed_call_stack_size;
	}

	Dst = &data.dasm_state;
	data.dasm_state = NULL;
	dasm_init(&data.dasm_state, DASM_MAXSECTION);
	dasm_setupglobal(&data.dasm_state, dasm_labels, ir_lb_MAX);
	dasm_setup(&data.dasm_state, dasm_actions);
	/* labels for each block + for each constant + rodata label + jmp_table label + for each entry + exit_table label */
	dasm_growpc(&data.dasm_state, ctx->cfg_blocks_count + 1 + ctx->consts_count + 1 + 1 + 1 + ctx->entries_count + 1);
	if (data.dasm_state->status != DASM_S_OK) {
		IR_ASSERT(data.dasm_state->status == DASM_S_NOMEM);
		dasm_free(&data.dasm_state);
		ctx->data = NULL;
		ctx->status = IR_ERROR_TOO_LARGE;
		return NULL;
	}

	data.emit_constants = ir_bitset_malloc(ctx->consts_count);

	if (!(ctx->flags & IR_SKIP_PROLOGUE)) {
		ir_emit_prologue(ctx);
	}
	if (ctx->flags & IR_FUNCTION) {
		ir_emit_load_params(ctx);
	}

	if (UNEXPECTED(!ctx->cfg_schedule)) {
		uint32_t *list = ctx->cfg_schedule = ir_mem_malloc(sizeof(uint32_t) * (ctx->cfg_blocks_count + 2));
		for (b = 0; b <= ctx->cfg_blocks_count; b++) {
			list[b] = b;
		}
		list[ctx->cfg_blocks_count + 1] = 0;
	}

	for (_b = 1; _b <= ctx->cfg_blocks_count; _b++) {
		b = ctx->cfg_schedule[_b];
		bb = &ctx->cfg_blocks[b];
		IR_ASSERT(!(bb->flags & IR_BB_UNREACHABLE));
		if ((bb->flags & (IR_BB_START|IR_BB_ENTRY|IR_BB_EMPTY)) == IR_BB_EMPTY) {
			continue;
		}
		if (bb->flags & IR_BB_ALIGN_LOOP) {
			|	.align IR_LOOP_ALIGNMENT
		}
		|=>b:

		i = bb->start;
		insn = ctx->ir_base + i;
		if (bb->flags & IR_BB_ENTRY) {
			uint32_t label = ctx->cfg_blocks_count + ctx->consts_count + 4 + insn->op3;

			|=>label:
			ir_emit_prologue(ctx);
			ctx->entries[insn->op3] = i;
		}

		/* skip first instruction */
		n = ir_insn_len(insn);
		i += n;
		insn += n;
		rule = ctx->rules + i;

		while (i <= bb->end) {
			if (!((*rule) & (IR_FUSED|IR_SKIPPED)))
			switch ((*rule) & IR_RULE_MASK) {
				case IR_VAR:
				case IR_PARAM:
				case IR_PI:
				case IR_PHI:
				case IR_SNAPSHOT:
				case IR_VA_END:
					break;
				case IR_MUL_PWR2:
				case IR_DIV_PWR2:
				case IR_MOD_PWR2:
					ir_emit_mul_div_mod_pwr2(ctx, i, insn);
					break;
				case IR_SDIV_PWR2:
					ir_emit_sdiv_pwr2(ctx, i, insn);
					break;
				case IR_SMOD_PWR2:
					ir_emit_smod_pwr2(ctx, i, insn);
					break;
				case IR_SHIFT:
					ir_emit_shift(ctx, i, insn);
					break;
				case IR_SHIFT_CONST:
					ir_emit_shift_const(ctx, i, insn);
					break;
				case IR_CTPOP:
					ir_emit_ctpop(ctx, i, insn);
					break;
				case IR_OP_INT:
					ir_emit_op_int(ctx, i, insn);
					break;
				case IR_OP_FP:
					ir_emit_op_fp(ctx, i, insn);
					break;
				case IR_BINOP_INT:
					ir_emit_binop_int(ctx, i, insn);
					break;
				case IR_BINOP_FP:
					ir_emit_binop_fp(ctx, i, insn);
					break;
				case IR_CMP_INT:
					ir_emit_cmp_int(ctx, i, insn);
					break;
				case IR_CMP_FP:
					ir_emit_cmp_fp(ctx, i, insn);
					break;
				case IR_SEXT:
					ir_emit_sext(ctx, i, insn);
					break;
				case IR_ZEXT:
					ir_emit_zext(ctx, i, insn);
					break;
				case IR_TRUNC:
					ir_emit_trunc(ctx, i, insn);
					break;
				case IR_BITCAST:
				case IR_PROTO:
					ir_emit_bitcast(ctx, i, insn);
					break;
				case IR_INT2FP:
					ir_emit_int2fp(ctx, i, insn);
					break;
				case IR_FP2INT:
					ir_emit_fp2int(ctx, i, insn);
					break;
				case IR_FP2FP:
					ir_emit_fp2fp(ctx, i, insn);
					break;
				case IR_COPY_INT:
					ir_emit_copy_int(ctx, i, insn);
					break;
				case IR_COPY_FP:
					ir_emit_copy_fp(ctx, i, insn);
					break;
				case IR_CMP_AND_BRANCH_INT:
					ir_emit_cmp_and_branch_int(ctx, b, i, insn, _ir_next_block(ctx, _b));
					break;
				case IR_CMP_AND_BRANCH_FP:
					ir_emit_cmp_and_branch_fp(ctx, b, i, insn, _ir_next_block(ctx, _b));
					break;
				case IR_GUARD_CMP_INT:
					ir_emit_guard_cmp_int(ctx, b, i, insn);
					break;
				case IR_GUARD_CMP_FP:
					ir_emit_guard_cmp_fp(ctx, b, i, insn);
					break;
				case IR_IF_INT:
					ir_emit_if_int(ctx, b, i, insn, _ir_next_block(ctx, _b));
					break;
				case IR_COND:
					ir_emit_cond(ctx, i, insn);
					break;
				case IR_SWITCH:
					ir_emit_switch(ctx, b, i, insn);
					break;
				case IR_MIN_MAX_INT:
					ir_emit_min_max_int(ctx, i, insn);
					break;
				case IR_OVERFLOW:
					ir_emit_overflow(ctx, i, insn);
					break;
				case IR_OVERFLOW_AND_BRANCH:
					ir_emit_overflow_and_branch(ctx, b, i, insn, _ir_next_block(ctx, _b));
					break;
				case IR_END:
				case IR_LOOP_END:
					if (bb->flags & IR_BB_OSR_ENTRY_LOADS) {
						ir_emit_osr_entry_loads(ctx, b, bb);
					}
					if (bb->flags & IR_BB_DESSA_MOVES) {
						ir_emit_dessa_moves(ctx, b, bb);
					}
					do {
						ir_ref succ = ctx->cfg_edges[bb->successors];

						if (UNEXPECTED(bb->successors_count == 2)) {
							if (ctx->cfg_blocks[succ].flags & IR_BB_ENTRY) {
								succ = ctx->cfg_edges[bb->successors + 1];
							} else {
								IR_ASSERT(ctx->cfg_blocks[ctx->cfg_edges[bb->successors + 1]].flags & IR_BB_ENTRY);
							}
						} else {
							IR_ASSERT(bb->successors_count == 1);
						}
						target = ir_skip_empty_target_blocks(ctx, succ);
						if (target != _ir_next_block(ctx, _b)) {
							|	b =>target
						}
					} while (0);
					break;
				case IR_RETURN_VOID:
					ir_emit_return_void(ctx);
					break;
				case IR_RETURN_INT:
					ir_emit_return_int(ctx, i, insn);
					break;
				case IR_RETURN_FP:
					ir_emit_return_fp(ctx, i, insn);
					break;
				case IR_CALL:
					ir_emit_call(ctx, i, insn);
					break;
				case IR_TAILCALL:
					ir_emit_tailcall(ctx, i, insn);
					break;
				case IR_IGOTO_DUP:
					if (bb->flags & IR_BB_DESSA_MOVES) {
						ir_emit_dessa_moves(ctx, b, bb);
					}
					IR_ASSERT(!igoto_dup_ref && !igoto_dup_block);
					igoto_dup_ref = i;
					igoto_dup_block = b;
					b = ctx->cfg_edges[bb->successors];
					bb = &ctx->cfg_blocks[b];
					i = bb->start;
					insn = &ctx->ir_base[i];
					rule = &ctx->rules[i];
					break;
				case IR_IGOTO:
					if ((ctx->ir_base[insn->op1].op == IR_MERGE || ctx->ir_base[insn->op1].op == IR_LOOP_BEGIN)
					 && (ctx->rules[ctx->ir_base[insn->op1].op1] & IR_RULE_MASK) == IR_IGOTO_DUP
					 && igoto_dup_ref) {
						ir_emit_ijmp(ctx, i, insn);
						b = igoto_dup_block;
						bb = &ctx->cfg_blocks[b];
						i = igoto_dup_ref;
						insn = &ctx->ir_base[i];
						rule = &ctx->rules[i];
						igoto_dup_block= 0;
						igoto_dup_ref = 0;
						break;
					}
					IR_ASSERT(!igoto_dup_ref && !igoto_dup_block);
					IR_FALLTHROUGH;
				case IR_IJMP:
					ir_emit_ijmp(ctx, i, insn);
					break;
				case IR_REG_BINOP_INT:
					ir_emit_reg_binop_int(ctx, i, insn);
					break;
				case IR_VADDR:
					ir_emit_vaddr(ctx, i, insn);
					break;
				case IR_VLOAD:
				case IR_VLOAD_v:
					ir_emit_vload(ctx, i, insn);
					break;
				case IR_VSTORE:
				case IR_VSTORE_v:
					ir_emit_vstore(ctx, i, insn);
					break;
				case IR_RLOAD:
					ir_emit_rload(ctx, i, insn);
					break;
				case IR_RSTORE:
					ir_emit_rstore(ctx, i, insn);
					break;
				case IR_LOAD_INT:
					ir_emit_load_int(ctx, i, insn);
					break;
				case IR_LOAD_FP:
					ir_emit_load_fp(ctx, i, insn);
					break;
				case IR_STORE_INT:
					ir_emit_store_int(ctx, i, insn);
					break;
				case IR_STORE_FP:
					ir_emit_store_fp(ctx, i, insn);
					break;
				case IR_ALLOCA:
					ir_emit_alloca(ctx, i, insn);
					break;
				case IR_VA_START:
					ir_emit_va_start(ctx, i, insn);
					break;
				case IR_VA_COPY:
					ir_emit_va_copy(ctx, i, insn);
					break;
				case IR_VA_ARG:
					ir_emit_va_arg(ctx, i, insn);
					break;
				case IR_AFREE:
					ir_emit_afree(ctx, i, insn);
					break;
				case IR_BLOCK_BEGIN:
					ir_emit_block_begin(ctx, i, insn);
					break;
				case IR_BLOCK_END:
					ir_emit_block_end(ctx, i, insn);
					break;
				case IR_FRAME_ADDR:
					ir_emit_frame_addr(ctx, i);
					break;
				case IR_EXITCALL:
					ir_emit_exitcall(ctx, i, insn);
					break;
				case IR_GUARD:
				case IR_GUARD_NOT:
					ir_emit_guard(ctx, i, insn);
					break;
				case IR_GUARD_OVERFLOW:
					ir_emit_guard_overflow(ctx, i, insn);
					break;
				case IR_TLS:
					ir_emit_tls(ctx, i, insn);
					break;
				case IR_TRAP:
					|	brk
					break;
				default:
					IR_ASSERT(0 && "NIY rule/instruction");
					ir_mem_free(data.emit_constants);
					dasm_free(&data.dasm_state);
					ctx->data = NULL;
					ctx->status = IR_ERROR_UNSUPPORTED_CODE_RULE;
					return NULL;
			}
			n = ir_insn_len(insn);
			i += n;
			insn += n;
			rule += n;
		}
	}

	if (ctx->deoptimization_exits) {
		uint32_t exit_table_label = ctx->cfg_blocks_count + 1 + ctx->consts_count + 1 + 1 + 1 + ctx->entries_count;

		|=>exit_table_label:
		for (i = 0; i < ctx->deoptimization_exits; i++) {
			const void *exit_addr = ctx->get_exit_addr(i);

			if (!exit_addr) {
				ctx->data = NULL;
				return 0;
			}
			|	b &exit_addr
		}
	}

	if (data.rodata_label) {
		|.rodata
	}
	IR_BITSET_FOREACH(data.emit_constants, ir_bitset_len(ctx->consts_count), i) {
		insn = &ctx->ir_base[-i];
		if (IR_IS_TYPE_FP(insn->type)) {
			int label = ctx->cfg_blocks_count + i;

			if (!data.rodata_label) {
				data.rodata_label = ctx->cfg_blocks_count + ctx->consts_count + 2;

				|.rodata
				|=>data.rodata_label:
			}
			if (insn->type == IR_DOUBLE) {
				|.align 8
				|=>label:
				|.long insn->val.u32, insn->val.u32_hi
			} else {
				IR_ASSERT(insn->type == IR_FLOAT);
				|.align 4
				|=>label:
				|.long insn->val.u32
			}
		} else if (insn->op == IR_STR) {
			int label = ctx->cfg_blocks_count + i;
			const char *str = ir_get_str(ctx, insn->val.str);
			int i = 0;

			if (!data.rodata_label) {
				data.rodata_label = ctx->cfg_blocks_count + ctx->consts_count + 2;

				|.rodata
				|=>data.rodata_label:
			}
			|.align 8
			|=>label:
			while (1) {
				char c;
				uint32_t w = 0;
				int j;

				for (j = 0; j < 4; j++) {
					c = str[i];
					if (!c) {
						break;
					}
					w |= c << (8 * j);
					i++;
				}
				|	.long w
				if (!c) {
					break;
				}
			}

		} else {
			IR_ASSERT(0);
		}
	} IR_BITSET_FOREACH_END();
	if (data.rodata_label) {
		|.code
	}
	ir_mem_free(data.emit_constants);

	if (ctx->status) {
		dasm_free(&data.dasm_state);
		ctx->data = NULL;
		return NULL;
	}

	if (data.dasm_state->status != DASM_S_OK) {
		IR_ASSERT(data.dasm_state->status == DASM_S_NOMEM);
		dasm_free(&data.dasm_state);
		ctx->data = NULL;
		ctx->status = IR_ERROR_TOO_LARGE;
		return NULL;
	}

	ret = dasm_link(&data.dasm_state, size_ptr);
	if (ret != DASM_S_OK) {
		IR_ASSERT(ret == DASM_S_NOMEM);
		dasm_free(&data.dasm_state);
		ctx->data = NULL;
		ctx->status = (ret == DASM_S_NOMEM) ? IR_ERROR_TOO_LARGE : IR_ERROR_LINK;
		return NULL;
	}
	size = *size_ptr;

	if (ctx->code_buffer) {
		entry = ctx->code_buffer->pos;
		entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 16);
		if (size > (size_t)((char*)ctx->code_buffer->end - (char*)entry)) {
			ctx->data = NULL;
			ctx->status = IR_ERROR_CODE_MEM_OVERFLOW;
			return NULL;
		}
		ctx->code_buffer->pos = (char*)entry + size;
	} else {
		entry = ir_mem_mmap(size);
		if (!entry) {
			dasm_free(&data.dasm_state);
			ctx->data = NULL;
			ctx->status = IR_ERROR_CODE_MEM_OVERFLOW;
			return NULL;
		}
		ir_mem_unprotect(entry, size);
	}

	if (ctx->deoptimization_exits) {
		uint32_t exit_table_label = ctx->cfg_blocks_count + 1 + ctx->consts_count + 1 + 1 + 1 + ctx->entries_count;

		ctx->deoptimization_exits_base = (const void*)((char*)entry + dasm_getpclabel(&data.dasm_state, exit_table_label));
	}

	ir_current_ctx = ctx;
	ret = dasm_encode(&data.dasm_state, entry);
	if (ret != DASM_S_OK) {
		IR_ASSERT(0);
		dasm_free(&data.dasm_state);
		if (ctx->code_buffer) {
			if (ctx->code_buffer->pos == (char*)entry + size) {
				/* rollback */
				ctx->code_buffer->pos = (char*)entry - size;
			}
		} else {
			ir_mem_unmap(entry, size);
		}
		ctx->data = NULL;
		ctx->status = IR_ERROR_ENCODE;
		return NULL;
	}

	if (data.jmp_table_label) {
		uint32_t offset = dasm_getpclabel(&data.dasm_state, data.jmp_table_label);
		ctx->jmp_table_offset = offset;
	} else {
		ctx->jmp_table_offset = 0;
	}
	if (data.rodata_label) {
		uint32_t offset = dasm_getpclabel(&data.dasm_state, data.rodata_label);
		ctx->rodata_offset = offset;
	} else {
		ctx->rodata_offset = 0;
	}

	if (ctx->entries_count) {
		/* For all entries */
		i = ctx->entries_count;
		do {
			ir_insn *insn = &ctx->ir_base[ctx->entries[--i]];
			uint32_t offset = dasm_getpclabel(&data.dasm_state, ctx->cfg_blocks_count + ctx->consts_count + 4 + insn->op3);
			insn->op3 = offset;
		} while (i != 0);
	}

	if ((ctx->flags2 & IR_HAS_BLOCK_ADDR) && ctx->loader && ctx->loader->add_label) {
		for (b = 1, bb = &ctx->cfg_blocks[b]; b <= ctx->cfg_blocks_count; bb++, b++) {
			ir_insn *insn = &ctx->ir_base[bb->start];

			if (insn->op == IR_BEGIN && insn->op2) {
				IR_ASSERT(ctx->ir_base[insn->op2].op == IR_LABEL);
				ctx->ir_base[insn->op2].val.u32_hi = 0;
				ctx->loader->add_label(ctx->loader, ir_get_str(ctx, ctx->ir_base[insn->op2].val.str),
					(char*)entry + dasm_getpclabel(&data.dasm_state, ir_skip_empty_target_blocks(ctx, b)));
			}
		}
	} else if (data.resolved_label_syms) {
		for (b = 1, bb = &ctx->cfg_blocks[b]; b <= ctx->cfg_blocks_count; bb++, b++) {
			ir_insn *insn = &ctx->ir_base[bb->start];

			if (insn->op == IR_BEGIN && insn->op2) {
				IR_ASSERT(ctx->ir_base[insn->op2].op == IR_LABEL);
				ctx->ir_base[insn->op2].val.u32_hi = 0;
			}
		}
	}

	dasm_free(&data.dasm_state);

	if (ctx->code_buffer) {
		size = (char*)ctx->code_buffer->pos - (char*)entry;
	}

	ir_mem_flush(entry, size);

	if (!ctx->code_buffer) {
		ir_mem_protect(entry, size);
	}

	ctx->data = NULL;
	return entry;
}

const void *ir_emit_exitgroup(uint32_t first_exit_point, uint32_t exit_points_per_group, const void *exit_addr, ir_code_buffer *code_buffer, size_t *size_ptr)
{
	void *entry;
	size_t size;
	uint32_t i;
	dasm_State **Dst, *dasm_state;
	int ret;

	IR_ASSERT(code_buffer);
	IR_ASSERT(aarch64_may_use_b(code_buffer, exit_addr));

	Dst = &dasm_state;
	dasm_state = NULL;
	dasm_init(&dasm_state, DASM_MAXSECTION);
	dasm_setupglobal(&dasm_state, dasm_labels, ir_lb_MAX);
	dasm_setup(&dasm_state, dasm_actions);

	|	bl >2
	|1:
	for (i = 1; i < exit_points_per_group; i++) {
		|	bl >2
	}
	|2:
	|	adr Rx(IR_REG_INT_TMP), <1
	|	sub Rx(IR_REG_INT_TMP), lr, Rx(IR_REG_INT_TMP)
	|	lsr Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #2
	if (first_exit_point) {
		|	add Rx(IR_REG_INT_TMP), Rx(IR_REG_INT_TMP), #first_exit_point
	}
	|	b &exit_addr

	ret = dasm_link(&dasm_state, &size);
	if (ret != DASM_S_OK) {
		IR_ASSERT(0);
		dasm_free(&dasm_state);
		return NULL;
	}

	entry = code_buffer->pos;
	entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 16);
	if (size > (size_t)((char*)code_buffer->end - (char*)entry)) {
		return NULL;
	}
	code_buffer->pos = (char*)entry + size;

	ir_current_ctx = NULL;
	ret = dasm_encode(&dasm_state, entry);
	if (ret != DASM_S_OK) {
		IR_ASSERT(0);
		dasm_free(&dasm_state);
		if (code_buffer->pos == (char*)entry + size) {
			/* rollback */
			code_buffer->pos = (char*)entry - size;
		}
		return NULL;
	}

	dasm_free(&dasm_state);

	ir_mem_flush(entry, size);

	*size_ptr = size;
	return entry;
}

static int ir_add_veneer(dasm_State *Dst, void *buffer, uint32_t ins, int *b, uint32_t *cp, ptrdiff_t offset)
{
	ir_ctx *ctx = ir_current_ctx;
	const void *addr, *veneer = NULL;
	ptrdiff_t na;
	int n, m;

	IR_ASSERT(ctx && ctx->code_buffer);

	if ((ins >> 16) == DASM_REL_A) {
		addr = (void*)((((ptrdiff_t)(*(b-1))) << 32) | (unsigned int)(*(b-2)));
		if (ctx->get_veneer) {
			veneer = ctx->get_veneer(ctx, addr);
		}
	} else {
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	}

	if (veneer) {
		na = (ptrdiff_t)veneer - (ptrdiff_t)cp + 4;
		n = (int)na;

		/* check if we can jump to veneer */
		if ((ptrdiff_t)n != na) {
			/* pass */
		} else if (!(ins & 0xf800)) {  /* B, BL */
			if ((n & 3) == 0 && ((n+0x08000000) >> 28) == 0) {
				return n;
			}
		} else if ((ins & 0x800)) {  /* B.cond, CBZ, CBNZ, LDR* literal */
			if ((n & 3) == 0 && ((n+0x00100000) >> 21) == 0) {
				return n;
			}
		} else if ((ins & 0x3000) == 0x2000) {  /* ADR */
			/* pass */
		} else if ((ins & 0x3000) == 0x3000) {  /* ADRP */
			/* pass */
		} else if ((ins & 0x1000)) {  /* TBZ, TBNZ */
			if ((n & 3) == 0 && ((n+0x00008000) >> 16) == 0) {
				return n;
			}
		}
	}

	veneer = ctx->code_buffer->pos;
	if ((char*)ctx->code_buffer->end - (char*)veneer < 4 ) {
		IR_ASSERT(0 && "too long jmp distance" && "jit buffer overflow");
		return 0; /* jit_buffer_size overflow */
	}

	na = (ptrdiff_t)veneer - (ptrdiff_t)cp + 4;
	n = (int)na;

	/* check if we can jump to veneer */
	if ((ptrdiff_t)n != na) {
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	} else if (!(ins & 0xf800)) {  /* B, BL */
		if ((n & 3) != 0 || ((n+0x08000000) >> 28) != 0) {
			IR_ASSERT(0 && "too long jmp distance");
			return 0;
		}
	} else if ((ins & 0x800)) {  /* B.cond, CBZ, CBNZ, LDR* literal */
		if ((n & 3) != 0 || ((n+0x00100000) >> 21) != 0) {
			IR_ASSERT(0 && "too long jmp distance");
			return 0;
		}
	} else if ((ins & 0x3000) == 0x2000) {  /* ADR */
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	} else if ((ins & 0x3000) == 0x3000) {  /* ADRP */
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	} else if ((ins & 0x1000)) {  /* TBZ, TBNZ */
		if ((n & 3) != 0 || ((n+0x00008000) >> 16) != 0) {
			IR_ASSERT(0 && "too long jmp distance");
			return 0;
		}
	} else if ((ins & 0x8000)) {  /* absolute */
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	} else {
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	}

	/* check if we can use B to jump from veneer */
	na = (ptrdiff_t)cp + offset - (ptrdiff_t)veneer - 4;
	m = (int)na;
	if ((ptrdiff_t)m != na) {
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	} else if ((m & 3) != 0 || ((m+0x08000000) >> 28) != 0) {
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	}

	if (!ctx->set_veneer || !ctx->set_veneer(ctx, addr, veneer)) {
		IR_ASSERT(0 && "too long jmp distance");
		return 0;
	}

	/* generate B instruction */
	*(uint32_t*)veneer = 0x14000000 | ((m >> 2) & 0x03ffffff);
	ctx->code_buffer->pos = (char*)ctx->code_buffer->pos + 4;

	return n;
}

bool ir_needs_thunk(const ir_code_buffer *code_buffer, void *addr)
{
	return !aarch64_may_use_b(code_buffer, addr);
}

void *ir_emit_thunk(ir_code_buffer *code_buffer, void *addr, size_t *size_ptr)
{
	void *entry;
	size_t size;
	dasm_State **Dst, *dasm_state;
	int ret;

	Dst = &dasm_state;
	dasm_state = NULL;
	dasm_init(&dasm_state, DASM_MAXSECTION);
	dasm_setupglobal(&dasm_state, dasm_labels, ir_lb_MAX);
	dasm_setup(&dasm_state, dasm_actions);

	|.code
	|	movz Rx(IR_REG_INT_TMP), #((uint64_t)(addr) & 0xffff)
	|	movk Rx(IR_REG_INT_TMP), #(((uint64_t)(addr) >> 16) & 0xffff), lsl #16
	|	movk Rx(IR_REG_INT_TMP), #(((uint64_t)(addr) >> 32) & 0xffff), lsl #32
	|	movk Rx(IR_REG_INT_TMP), #(((uint64_t)(addr) >> 48) & 0xffff), lsl #48
	|	br Rx(IR_REG_INT_TMP)

	ret = dasm_link(&dasm_state, &size);
	if (ret != DASM_S_OK) {
		IR_ASSERT(0);
		dasm_free(&dasm_state);
		return NULL;
	}

	entry = code_buffer->pos;
	entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 4);
	if (size > (size_t)((char*)code_buffer->end - (char*)entry)) {
		dasm_free(&dasm_state);
		return NULL;
	}

	ret = dasm_encode(&dasm_state, entry);
	if (ret != DASM_S_OK) {
		dasm_free(&dasm_state);
		return NULL;
	}

	*size_ptr = size;
	code_buffer->pos = (char*)code_buffer->pos + size;

	dasm_free(&dasm_state);
	ir_mem_flush(entry, size);

	return entry;
}

void ir_fix_thunk(void *thunk_entry, void *addr)
{
	uint32_t *code = thunk_entry;
	IR_ASSERT((code[0] & 0xffe00000) == 0xd2800000
		&& (code[1] & 0xffe00000) == 0xf2a00000
		&& (code[2] & 0xffe00000) == 0xf2c00000
		&& (code[3] & 0xffe00000) == 0xf2e00000
		&& (code[4] & 0xfffffc1f) == 0xd61f0000);

	code[0] = (code[0] & 0xffe0001f) | (uint32_t)((uint64_t)(addr) & 0xffff) << 5;
	code[1] = (code[1] & 0xffe0001f) | (uint32_t)(((uint64_t)(addr) >> 16) & 0xffff) << 5;
	code[2] = (code[2] & 0xffe0001f) | (uint32_t)(((uint64_t)(addr) >> 32) & 0xffff) << 5;
	code[3] = (code[3] & 0xffe0001f) | (uint32_t)(((uint64_t)(addr) >> 48) & 0xffff) << 5;

	ir_mem_flush(code, sizeof(uint32_t) * 4);
}
